iOS Performance & Memory
Irbisa · cheatsheetSeptember 13, 2026

iOS Performance & Memory

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

Senior Developer20 itemscompressed for a skim
  1. 01

    How does ARC (Automatic Reference Counting) work in Swift?

    Medium

    ARC is a compile-time mechanism: the compiler inserts the retain and release calls, so an object is deallocated the instant its last strong reference goes away, with no collector and no pause.

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

    What is a retain cycle? How do you detect and prevent them?

    Medium

    A retain cycle is two (or more) reference-counted objects holding strong references to each other so refcount never hits zero.

    // Bug: store result of Combine sink without [weak self]
    class VM: ObservableObject {
      @Published var items: [String] = []
      private var bag = Set<AnyCancellable>()
      init() {
        api.publisher
    …
  3. 03

    How do you profile an iOS app with Instruments?

    Medium

    Instruments is Apple's profiler suite, attached to a running app from Xcode with Product → Profile.

    import OSLog
    
    let signposter = OSSignposter(subsystem: "com.example.app", category: "feed")
    
    func fetchFeed() async throws -> [Post] {
      let state = signposter.beginInterval("fetchFeed")
    …
  4. 04

    How do you keep the main thread responsive in iOS apps?

    Medium

    The main thread draws every frame and handles every touch, so anything slow there is visible: over roughly 16ms you drop a frame, and over 250ms the system calls it a hang.

    // ❌ Blocking the main thread
    @MainActor
    func loadFeedBadly() {
      let data = try? Data(contentsOf: hugeURL)             // synchronous I/O on main
      feed = (try? JSONDecoder().decode([Post].self, from: data!)) ?? []
    }
    …
  5. 05

    How do you optimize UITableView / UICollectionView scrolling performance?

    Hard

    Smooth scrolling means every frame's work fits in the frame budget: about 16ms at 60Hz, 8ms on a 120Hz ProMotion display.

    // Pre-decode image cache
    final class ImageCache {
      static let shared = ImageCache()
      private let cache = NSCache<NSURL, UIImage>()
      func image(for url: URL) async -> UIImage? {
        if let img = cache.object(forKey: url as NSURL) { return img }
    …
  6. 06

    How do you analyze and improve app launch time?

    Hard

    App launch splits into two measurable phases with different fixes: pre-main work done by dyld and the runtime, and everything from main until your first useful frame.

    import OSLog
    
    let signposter = OSSignposter(subsystem: "com.example.app", category: "launch")
    
    @main
    struct MyApp: App {
    …
  7. 07

    The photo grid crashes with no stack trace and the app dies in the background. What is going on?

    Hard

    That is not a crash but a memory kill: iOS terminates a process whose footprint crosses the device limit, and it arrives as a jetsam event rather than as a crash report pointing at your code.

    func thumbnail(from url: URL, maxPixel: CGFloat, scale: CGFloat) -> UIImage? {
      let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
      guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else { return nil }
    
      let options: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
    …
  8. 08

    A SwiftUI list stutters while scrolling. How do you find the cause and what usually fixes it?

    Hard

    Find out what is re-running before changing anything: the SwiftUI instrument counts body evaluations and the Animation Hitches instrument shows which frames missed their deadline.

    // Slow row: work in body, unstable identity, full-size image
    struct BadRow: View {
      let post: Post
      var body: some View {
        let formatter = DateFormatter()          // rebuilt on every body call
        formatter.dateStyle = .medium
    …
  9. 09

    QA says the app drains the battery. What do you actually change?

    Medium

    Energy cost is driven by how often you wake the radio, the GPU and the CPU rather than by how much code runs, so the fixes are batching and scheduling, not micro-optimisation.

    // Batch events; flush on background or when the buffer fills
    actor AnalyticsQueue {
      private var buffer: [Event] = []
    
      func track(_ event: Event) async {
        buffer.append(event)
    …
  10. 10

    The app is 300 MB in the store and installs are dropping. Where does the size go?

    Medium

    Start from the App Store Connect size report, which gives download and install size per device class, because the size of the archive on your Mac is not what a user actually downloads.

    // On-Demand Resources: tagged content is downloaded on first use
    final class TutorialAssets {
      private var request: NSBundleResourceRequest?
    
      func load() async throws -> URL {
        let request = NSBundleResourceRequest(tags: ["tutorial-videos"])
    …
  11. 11

    What is thread explosion, and how do you get priorities right under Swift Concurrency?

    Hard

    Thread explosion happens when blocked work keeps arriving on a concurrent queue and GCD spawns a fresh thread for every blocked one.

    // Thread explosion: 200 blocked threads, none of them making progress
    for url in urls {
      DispatchQueue.global().async {
        let data = try? Data(contentsOf: url)     // blocks a thread for the whole transfer
        handle(data)
      }
    …
  12. 12

    How do you stop a performance regression from ever reaching users?

    Medium

    Turn the numbers you care about into a gate and a dashboard: a measured budget in CI catches the regression before release, and MetricKit tells you what real devices see after it.

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

    Appending one row to a 20 000-element array inside a model struct takes 40ms and gets slower every time. What is Swift copying?

    Medium

    Something else is holding a reference to the array's storage at the moment you mutate it, so copy-on-write duplicates all 20 000 elements instead of appending in place.

    struct Row { let id: UUID; var flag: Bool }
    
    // Slow: the undo entry shares the buffer, so the next append copies all of it
    final class SlowEditor {
      private(set) var rows: [Row] = []
      private var undo: [[Row]] = []
    …
  14. 14

    The user pops the screen, but requests keep firing and the view model's deinit never runs. What did that Task do?

    Medium

    An unstructured Task { } holds a strong reference to everything its body captured and is cancelled by nobody, so the view model stays alive and its work stays scheduled until the body actually returns.

    // Leaks: strong self, unstructured, and the loop never ends
    final class BadModel {
      var posts: [Post] = []
    
      init(feed: Feed) {
        Task {
    …
  15. 15

    A crash report shows exception code 0x8badf00d and no crashing frame in your code. What actually killed the app?

    Medium

    The watchdog killed it: the app failed to finish a lifecycle transition inside its time budget, so the system sent SIGKILL and stamped the report 0x8badf00d.

    // Ships a watchdog kill: launch parks the main thread on the network
    func application(_ app: UIApplication,
                     didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      let semaphore = DispatchSemaphore(value: 0)
      var remote: Config?
      RemoteConfig.fetch { remote = $0; semaphore.signal() }
    …
  16. 16

    The app is completely frozen — no taps, no scrolling — yet the spinner keeps spinning smoothly. Why?

    Medium

    Because your main thread does not draw that animation: it described it once, and a separate process — the render server — interpolates every frame from that description.

    // One animation description, handed to the render server once
    let spinner = UIActivityIndicatorView(style: .medium)
    spinner.startAnimating()
    
    // Blocking main does not stop it: taps die, the spinner keeps going
    Thread.sleep(forTimeInterval: 5)
    …
  17. 17

    Time Profiler shows the main thread idle at 20%, yet the card list still drops frames on an older phone. Where is the time going?

    Hard

    Not in your code — the frames are being lost after the commit, in the render server and on the GPU, which is exactly what a CPU sampler cannot see.

    // Expensive cell: an offscreen pass for the shadow, another for the mask
    final class BadCardCell: UICollectionViewCell {
      let photo = UIImageView()
    
      func style() {
        layer.shadowOpacity = 0.2
    …
  18. 18

    Xcode's memory gauge says 480 MB, Instruments Allocations says 90 MB, and the app still gets jetsammed. Which number is real?

    Hard

    The gauge is: jetsam charges you phys_footprint, which is dirty plus compressed memory including mappings Allocations never shows, while Allocations only reports the heap you got through malloc and the Swift and Objective-C runtimes.

    import Foundation
    import os
    
    // The number jetsam actually uses, readable from inside the app
    func physFootprint() -> UInt64 {
      var info = task_vm_info_data_t()
    …
  19. 19

    The hottest frames in your Time Profiler trace are swift_retain, swift_release and objc_msgSend. What is the app really spending its time on?

    Hard

    Per-object bookkeeping in a hot loop: atomic reference counting and dynamic dispatch that the optimiser was unable to remove because it could not see enough of the code to prove it was safe.

    // Slow shape: boxed existentials, an open class, retain traffic per element
    class Shape {                              // subclassable -> vtable dispatch
      func area() -> Double { 0 }
    }
    
    func totalArea(_ shapes: [any Renderable]) -> Double {   // boxed + witness table
    …
  20. 20

    Memory climbs 4 MB a minute on a screen the user is only watching, and the Leaks instrument finds nothing. What is growing?

    Hard

    A buffer with no bound: a producer is outrunning its consumer and the queued values are perfectly reachable, so a leak detector — which only reports memory with no path from a root — has nothing to say about them.

    // Unbounded by default: a fast producer queues everything the consumer misses
    let (readings, continuation) = AsyncStream.makeStream(of: Reading.self)
    
    // Bounded: keep the freshest value, drop the rest — what a UI consumer wants
    let (latest, feed) = AsyncStream.makeStream(
      of: Reading.self,
    …