Irbisa · Шпаргалка

Производительность iOS и память

ARC, retain cycles, Instruments, главный поток, оптимизация изображений и таблиц

Senior-разработчик · 24 вопроса

  1. 01

    Как работает ARC в Swift и чем он отличается от сборщика мусора?

    ARC освобождает экземпляр класса в тот момент, когда счётчик сильных ссылок на него падает до нуля; вызовы retain и release вставляет компилятор.

    final class Session {  let id: String  init(id: String) { self.id = id }  deinit { print("\(id) freed") }}…
  2. 02

    Чем стек отличается от кучи и какие значения Swift где хранятся?

    Стек служит памятью конкретного потока: выделение в нём сводится к простому сдвигу указателя, а всё освобождается при возврате из функции.

  3. 03

    Что такое retain cycle, откуда он берётся в iOS-коде и как его разорвать?

    Retain cycle возникает, когда два или больше объекта держат друг друга сильными ссылками, поэтому ни один счётчик не доходит до нуля и все они утекают.

    @MainActorprotocol ProfileViewDelegate: AnyObject {    // AnyObject is what makes weak legal  func profileDidSave()} final class ProfileView: UIView {…
  4. 04

    Какие из четырёх замыканий в этом view controller создают retain cycle, а какие только откладывают освобождение?

    final class ProfileViewController: UIViewController {  var onSave: (() -> Void)?  private let saveButton = UIButton(type: .system)   override func viewDidLoad() {    super.viewDidLoad()    view.addSubview(saveButton)    UIView.animate(withDuration: 0.3) { self.view.alpha = 1 }                    // A    URLSession.shared.dataTask(with: URL(string: "https://example.com/feed")!) { _, _, _ in self.reload() }.resume()  // B    onSave = { self.reload() }                                                  // C    saveButton.addAction(UIAction { _ in self.reload() }, for: .touchUpInside)  // D  }   func reload() {}}

    C и D создают циклы, A и B не создают.

  5. 05

    Найдите баг: Memory Graph показывает на один живой OTPViewController больше после каждого открытия и закрытия экрана ввода кода.

    final class OTPViewController: UIViewController {  private var timer: Timer?  private var secondsLeft = 60   override func viewDidLoad() {    super.viewDidLoad()    timer = Timer.scheduledTimer(timeInterval: 1, target: self,                                 selector: #selector(tick), userInfo: nil, repeats: true)  }   @objc private func tick() {    secondsLeft -= 1    title = "Resend the code in \(secondsLeft) s"  }  deinit { timer?.invalidate() }  // the timer must not outlive the screen}

    Run loop удерживает запланированный таймер, а таймер удерживает свою цель self, поэтому после закрытия экрана счётчик контроллера не падает до нуля.

  6. 06

    Что такое утечка памяти в iOS-приложении и почему инструмент Leaks пропускает большинство утечек?

    Утечкой называют память, которая приложению больше не нужна, но так и не освобождается.

  7. 07

    Как iOS-приложение должно реагировать на memory warning и почему NSCache, а не Dictionary?

    Освободить всё, что можно пересоздать (кэши картинок, декодированные данные, тяжёлое состояние невидимых экранов), потому что система может убить процесс следующим шагом.

    // NSCache is thread-safe, so sharing this wrapper across threads is finefinal class ThumbnailCache: @unchecked Sendable {  private let cache = NSCache<NSURL, UIImage>()  private var observer: NSObjectProtocol?   init() {…
  8. 08

    Пока работает этот цикл экспорта, память растёт до 1,5 ГБ и падает сразу после его завершения. Почему и как это исправить?

    func exportThumbnails(_ urls: [URL], to folder: URL) throws {  for url in urls {                                   // 3 000 photos    guard let image = UIImage(contentsOfFile: url.path),          let thumb = image.preparingThumbnail(of: CGSize(width: 512, height: 512)),          let data = thumb.jpegData(compressionQuality: 0.8)    else { continue }    try data.write(to: folder.appendingPathComponent(url.lastPathComponent))  }}

    Цикл заполняет autorelease pool, который очищается только в конце текущего оборота run loop или рабочего блока, поэтому временные картинки и данные каждой итерации живут до завершения всего цикла.

  9. 09

    Почему большинство проблем с памятью в iOS-приложениях связано с изображениями и как их даунсемплить?

    Декодированное изображение занимает ширина × высота × 4 байта, каким бы маленьким ни был файл, поэтому 12-мегапиксельное фото занимает около 48 МБ в памяти, даже если JPEG весит 2 МБ.

    func thumbnail(at url: URL, pointSize: CGSize, scale: CGFloat) -> UIImage? {  // Do not cache the full-size image while reading the file  let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary  guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else { return nil }   let maxPixels = max(pointSize.width, pointSize.height) * scale…
  10. 10

    Пользователь закрывает этот экран, запросы продолжают уходить, а deinit так и не печатается. Что делает этот Task?

    @MainActorfinal class FeedModel {  var posts: [Post] = []   init(feed: FeedService) {    Task {      for await update in feed.updates {   // a stream that never finishes        self.posts = update      }    }  }   deinit { print("FeedModel freed") }      // never prints}

    Task сильно держит self, пока выполняется его тело, а цикл for await не заканчивается, потому что стрим никогда не завершается.

  11. 11

    Как найти проблему производительности с помощью Instruments, а не угадывать?

    Воспроизведите медленный сценарий на реальном устройстве в Release-сборке, запишите его шаблоном, подходящим под симптом, и меняйте по одной вещи за раз, сравнивая один и тот же трейс до и после.

    import OSLog let signposter = OSSignposter(subsystem: "com.example.app", category: "feed")let log = Logger(subsystem: "com.example.app", category: "feed") func loadFeed() async throws -> [Post] {…
  12. 12

    Чем hang отличается от hitch и как сохранить отзывчивость главного потока?

    Hitch означает кадр, показанный с опозданием: заметное подёргивание при скролле или анимации.

    // wrong: synchronous I/O and decoding on the main thread@MainActorfunc loadFeedBadly() {  let data = try? Data(contentsOf: feedURL)  feed = (try? JSONDecoder().decode([Post].self, from: data ?? Data())) ?? []}…
  13. 13

    Как добиться плавного скролла в UITableView или UICollectionView?

    Укладывайте работу каждой ячейки в бюджет кадра (около 16 мс при 60 Гц и 8 мс при 120 Гц): переиспользуйте ячейки, загружайте и декодируйте картинки вне главного потока в размере отображения и избегайте работы с layout и рендерингом, которая повторяется каждый кадр.

    final class PhotoCell: UICollectionViewCell {  let imageView = UIImageView()  private var loadTask: Task<Void, Never>?   func configure(url: URL, loader: ImageLoader) {    loadTask = Task { [weak self] in…
  14. 14

    Что такое offscreen rendering, какие свойства слоя его вызывают и как его найти?

    Offscreen rendering означает, что GPU рисует поддерево слоёв в отдельный буфер и потом композитит его обратно, потому что не может нарисовать его за один проход.

    // Expensive: an offscreen pass for the shadow and another for the clipped subtreefinal class BadCardCell: UICollectionViewCell {  func style() {    layer.shadowOpacity = 0.2    layer.shadowRadius = 12             // no shadowPath -> GPU renders first to find the shape    contentView.layer.cornerRadius = 16…
  15. 15

    Пользователи жалуются на крэши, которые у вас не воспроизводятся. Как дойти от крэш-репорта до строки кода?

    Символизируйте репорт dSYM-файлом именно этой сборки, а затем прочитайте тип исключения и стек упавшего потока.

    # Which build is this? Must match the UUID in the report's Binary Imagesdwarfdump --uuid MyApp.app.dSYM # Resolve one frame by hand: -l is the app's load address from Binary Imagesxcrun atos -arch arm64 \  -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \…
  16. 16

    Что означает EXC_BAD_ACCESS и какие инструменты Xcode помогают найти причину?

    Приложение обратилось к памяти, к которой нельзя: к освобождённому объекту, висячему указателю или неотображённому адресу.

  17. 17

    В крэш-репорте код исключения 0x8badf00d и ни одного кадра вашего кода. Что убило приложение и что обычно к этому приводит?

    Его убил watchdog: приложение не завершило переход жизненного цикла (запуск или возврат на передний план) за отведённое время, поэтому система прислала SIGKILL и пометила отчёт кодом 0x8badf00d («ate bad food»).

    // wrong: launch parks the main thread until the server answersfunc applicationWrong(_ app: UIApplication,                 didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {  let semaphore = DispatchSemaphore(value: 0)  let box = ConfigBox()                // a reference box, so the closure can fill it  RemoteConfig.fetch { box.value = $0; semaphore.signal() }…
  18. 18

    Как отслеживать производительность на устройствах реальных пользователей и не выпустить регрессию в релиз?

    Выберите несколько заметных пользователю показателей и измеряйте их в двух местах: тесты производительности в CI на одном и том же устройстве ловят регрессию до релиза, а MetricKit и Xcode Organizer показывают, что видят реальные устройства после него.

    import XCTest final class LaunchPerformanceTests: XCTestCase {  func testColdLaunch() {    measure(metrics: [XCTApplicationLaunchMetric(), XCTMemoryMetric()]) {      XCUIApplication().launch()…
  19. 19

    Как Core Animation превращает изменения UI в кадр и почему спиннер продолжает крутиться, когда главный поток завис?

    Приложение только описывает кадр: в конце итерации run loop оно коммитит изменённое дерево слоёв в render server, отдельный процесс, который рисует его на GPU и сам проигрывает анимации.

  20. 20

    Индикатор памяти Xcode показывает 480 МБ, Allocations показывает 90 МБ, а приложение всё равно убивают из-за памяти. Какая цифра настоящая?

    Цифра индикатора.

    // The number the memory limit is checked against, read from inside the appfunc physFootprint() -> UInt64 {  var info = task_vm_info_data_t()  var count = mach_msg_type_number_t(    MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size)  let status = withUnsafeMutablePointer(to: &info) {…
  21. 21

    Как измерить и сократить время запуска приложения?

    Измерьте холодный запуск до первого кадра на реальных устройствах и сокращайте две фазы по отдельности: работу dyld и рантайма до main и ваш собственный код от main до отрисовки первого экрана.

    import OSLog let launchSignposter = OSSignposter(subsystem: "com.example.app", category: "launch") @mainstruct MyApp: App {…
  22. 22

    Тестировщики говорят, что приложение сажает батарею. Как найти причину и что на самом деле менять?

    Энергия уходит в основном на пробуждение железа (сотового модуля, GPS, GPU и CPU), а не на выполнение кода, поэтому исправления сводятся к тому, чтобы делать работу реже и пачками.

  23. 23

    Размер загрузки приложения растёт с каждым релизом. Как найти, что занимает место, и сократить его?

    Измерьте то, что пользователи реально скачивают (размеры по устройствам в App Store Connect или App Thinning Size Report из Xcode), и атакуйте самую большую часть: обычно это ресурсы, затем сторонний код.

  24. 24

    Что такое thread explosion и как Swift Concurrency его избегает, при условии, что вы не блокируете задачу?

    Взрыв потоков (thread explosion) случается, когда работа, отправленная на конкурентную очередь, блокируется, и GCD запускает ещё один поток для следующего элемента, потом ещё один.

    // Thread explosion: hundreds of threads, most of them blockedfor url in urls {  DispatchQueue.global().async {    let data = try? Data(contentsOf: url)     // blocks a thread for the whole transfer    handle(data)  }…