Irbisa · Cheatsheet

iOS Performance & Memory

ARC, retain cycles, Instruments, main thread, image and table optimization

Senior Developer · 24 questions

  1. 01

    How does ARC work in Swift, and how is it different from a garbage collector?

    ARC frees a class instance the moment its strong reference count drops to zero, using retain and release calls the compiler inserts.

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

    What is the difference between the stack and the heap, and which Swift values live where?

    The stack is per-thread memory where allocating is just moving a pointer and everything is freed when the function returns.

  3. 03

    What is a retain cycle, where does it come from in iOS code, and how do you break it?

    A retain cycle is two or more objects holding strong references to each other, so no count ever reaches zero and all of them leak.

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

    Which of the four closures in this view controller create a retain cycle, and which ones only delay deallocation?

    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 and D are cycles; A and B are not.

  5. 05

    Find the bug: the Memory Graph shows one more OTPViewController alive every time the user opens and closes the code screen.

    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}

    The run loop retains the scheduled timer and the timer retains its target, self, so closing the screen never drops the controller's count to zero.

  6. 06

    What is a memory leak in an iOS app, and why does the Leaks instrument miss most of them?

    A leak is memory the app no longer needs but never frees.

  7. 07

    How should an iOS app react to a memory warning, and why use NSCache instead of a Dictionary?

    Free everything you can rebuild (image caches, decoded data, the heavy state of screens that are not visible), because the system may kill the process next.

    // 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

    Memory climbs to 1.5 GB while this export loop runs and drops the moment it returns. Why, and how do you fix it?

    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))  }}

    The loop fills an autorelease pool that is drained only when the current run-loop turn or work item ends, so every iteration's temporary images and data stay alive until the whole loop returns.

  9. 09

    Why do images cause most memory problems in iOS apps, and how do you downsample them?

    A decoded image costs width × height × 4 bytes however small the file is, so a 12-megapixel photo takes about 48 MB in memory even as a 2 MB JPEG.

    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

    The user pops this screen, requests keep firing and deinit never prints. What is this Task doing?

    @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}

    The Task holds self strongly for as long as its body runs, and the for await loop never ends because the stream never finishes.

  11. 11

    How do you find a performance problem with Instruments instead of guessing?

    Reproduce the slow case on a real device with a Release build, record it with the template that matches the symptom, and change one thing at a time, comparing the same trace before and after.

    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

    What is the difference between a hang and a hitch, and how do you keep the main thread responsive?

    A hitch is a frame shown late: visible stutter in a scroll or animation.

    // 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

    How do you make a UITableView or UICollectionView scroll smoothly?

    Keep each cell's work inside the frame budget (about 16 ms at 60 Hz and 8 ms at 120 Hz): reuse cells, load and decode images off the main thread at display size, and avoid layout and rendering work that repeats every frame.

    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

    What is offscreen rendering, which layer properties trigger it, and how do you find it?

    Offscreen rendering is the GPU drawing a layer subtree into a separate buffer and compositing it back, because it cannot draw it in one pass.

    // 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

    Users report crashes you cannot reproduce. How do you get from a crash report to the line of code?

    Symbolicate the report with the dSYM of that exact build, then read the exception type and the crashed thread's stack.

    # 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

    What does EXC_BAD_ACCESS mean, and which Xcode tools help you find its cause?

    The app touched memory it must not: a freed object, a dangling pointer or an unmapped address.

  17. 17

    A crash report shows exception code 0x8badf00d and no frame of your code. What killed the app, and what usually causes it?

    The watchdog killed it: the app did not finish a lifecycle transition, such as launch or returning to the foreground, within its time limit, so the system sent SIGKILL and stamped the report 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

    How do you track performance on real users' devices and stop a regression before it ships?

    Pick a few user-visible numbers and measure them in two places: performance tests in CI on a fixed device catch a regression before release, and MetricKit plus the Xcode Organizer show what real devices experience after it.

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

    How does Core Animation turn your UI changes into a frame, and why does a spinner keep spinning while the main thread is frozen?

    Your app only describes each frame: at the end of a run-loop turn it commits the changed layer tree to the render server, a separate process that renders it on the GPU and runs the animations.

  20. 20

    Xcode's memory gauge shows 480 MB, Allocations shows 90 MB, and the app still gets killed for memory. Which number is real?

    The gauge. The system's memory limit applies to phys_footprint (all dirty plus compressed memory of the process), while Allocations mostly shows what went through malloc and the object runtimes, and misses large memory regions that are not malloc heap.

    // 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

    How do you measure and reduce app launch time?

    Measure cold launch to the first frame on real devices, then cut the two phases separately: the work dyld and the runtime do before main, and your own code from main until the first screen is drawn.

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

    QA says the app drains the battery. How do you find the cause, and what do you actually change?

    Energy goes into waking the hardware (the cellular radio, GPS, the GPU and the CPU) more than into running code, so the fixes are about doing work less often and in batches.

  23. 23

    The app's download size grows with every release. How do you find what takes the space and cut it?

    Measure what users actually download (App Store Connect's per-device sizes or Xcode's App Thinning Size Report), then attack the largest part, usually assets and then third-party code.

  24. 24

    What is thread explosion, and how does Swift Concurrency avoid it as long as you never block a task?

    Thread explosion happens when work dispatched to a concurrent queue blocks, so GCD starts another thread for the next item, and then another.

    // 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)  }…