AgentSkillsCN

iOS Performance

针对高性能 iOS 应用的内存管理、性能剖析、优化技巧以及 Instruments 工具的使用方法

SKILL.md
--- frontmatter
name: iOS Performance
description: Memory management, profiling, optimization techniques, and Instruments usage for high-performance iOS apps
version: 1.0.0

iOS Performance Optimization

Comprehensive guide to building high-performance iOS applications with proper memory management, profiling, and optimization techniques.

Memory Management

ARC (Automatic Reference Counting)

Strong vs Weak vs Unowned

swift
// STRONG (default) - Increases retain count
class Parent {
    var child: Child?  // Strong reference
}

// WEAK - Does NOT increase retain count, becomes nil when deallocated
class Child {
    weak var parent: Parent?  // Weak to prevent retain cycle
}

// UNOWNED - Does NOT increase retain count, crashes if accessed after deallocation
class Customer {
    let card: CreditCard  // Customer owns the card

    init() {
        card = CreditCard(holder: self)
    }
}

class CreditCard {
    unowned let holder: Customer  // Card always has a holder
}

When to Use Each

Reference TypeUse WhenRisk
strongObject should stay aliveRetain cycles
weakOptional reference, may become nilNone
unownedNon-optional, same or longer lifetimeCrash if wrong

Retain Cycle Detection

Common Retain Cycle Patterns

swift
// ❌ RETAIN CYCLE - Closure captures self strongly
class ViewController: UIViewController {
    var onComplete: (() -> Void)?

    func setup() {
        onComplete = {
            self.doSomething()  // Strong capture!
        }
    }
}

// ✅ FIXED - Weak capture
class ViewController: UIViewController {
    var onComplete: (() -> Void)?

    func setup() {
        onComplete = { [weak self] in
            self?.doSomething()
        }
    }
}

Delegate Retain Cycles

swift
// ❌ RETAIN CYCLE
protocol MyDelegate {
    func didComplete()
}

class Parent: MyDelegate {
    var child: Child?

    init() {
        child = Child()
        child?.delegate = self  // Strong reference back!
    }
}

class Child {
    var delegate: MyDelegate?  // Strong!
}

// ✅ FIXED - Weak delegate
protocol MyDelegate: AnyObject {  // Must be AnyObject for weak
    func didComplete()
}

class Child {
    weak var delegate: MyDelegate?  // Weak!
}

Timer Retain Cycles

swift
// ❌ RETAIN CYCLE - Timer retains target
class ViewController: UIViewController {
    var timer: Timer?

    override func viewDidLoad() {
        timer = Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,        // Timer retains self!
            selector: #selector(tick),
            userInfo: nil,
            repeats: true
        )
    }
}

// ✅ FIXED - Use closure-based timer with weak capture
class ViewController: UIViewController {
    var timer: Timer?

    override func viewDidLoad() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.tick()
        }
    }

    deinit {
        timer?.invalidate()
    }
}

Memory Leak Checklist

swift
// Add to your ViewControllers to verify deallocation
deinit {
    print("✅ \(Self.self) deallocated")
}

Common Leak Sources:

  • Closures without [weak self]
  • Delegates not marked weak
  • NotificationCenter observers not removed
  • Timers not invalidated
  • DispatchWorkItem not cancelled
  • Strong references in singletons

Performance Profiling

Instruments Usage

Time Profiler

bash
# Launch Time Profiler
xcrun xctrace record --template "Time Profiler" --launch -- /path/to/app.app

What to look for:

  • Functions taking > 16ms (60fps = 16.67ms per frame)
  • Main thread blocking
  • Excessive CPU usage

Allocations

bash
# Track memory allocations
xcrun xctrace record --template "Allocations" --launch -- /path/to/app.app

What to look for:

  • Memory growth over time (leaks)
  • Transient allocations (excessive object creation)
  • Large allocations

Leaks

bash
# Detect memory leaks
xcrun xctrace record --template "Leaks" --launch -- /path/to/app.app

In-Code Profiling

swift
// Measure execution time
func measureTime(_ label: String, block: () -> Void) {
    let start = CFAbsoluteTimeGetCurrent()
    block()
    let end = CFAbsoluteTimeGetCurrent()
    print("⏱ \(label): \((end - start) * 1000)ms")
}

// Usage
measureTime("Load Data") {
    loadData()
}

// Using os_signpost for Instruments
import os.signpost

let log = OSLog(subsystem: "com.app", category: "Performance")

func loadData() {
    os_signpost(.begin, log: log, name: "Load Data")
    defer { os_signpost(.end, log: log, name: "Load Data") }

    // ... work
}

UIKit Optimization

UITableView/UICollectionView

swift
// ✅ OPTIMIZED TableView
final class OptimizedTableViewController: UIViewController {

    private let tableView: UITableView = {
        let tv = UITableView()
        tv.rowHeight = UITableView.automaticDimension
        tv.estimatedRowHeight = 60  // Provide estimate!
        return tv
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Register cells for reuse
        tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")

        // Prefetching for smooth scrolling
        tableView.prefetchDataSource = self
    }
}

extension OptimizedTableViewController: UITableViewDataSourcePrefetching {
    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        // Preload images or data for upcoming cells
        for indexPath in indexPaths {
            imageLoader.prefetch(url: items[indexPath.row].imageURL)
        }
    }

    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
        // Cancel preloading for cells no longer needed
        for indexPath in indexPaths {
            imageLoader.cancel(url: items[indexPath.row].imageURL)
        }
    }
}

Cell Optimization

swift
final class OptimizedCell: UITableViewCell {

    // Use layer properties for shadows (GPU accelerated)
    override func awakeFromNib() {
        super.awakeFromNib()

        // Rasterize complex views
        layer.shouldRasterize = true
        layer.rasterizationScale = UIScreen.main.scale

        // Opaque views are faster
        backgroundColor = .white
        isOpaque = true
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        // Cancel any pending image loads
        imageView?.image = nil
        imageLoadTask?.cancel()
    }
}

Image Optimization

swift
// ✅ Downscale images to display size
extension UIImage {
    func downscaled(to size: CGSize) -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { _ in
            draw(in: CGRect(origin: .zero, size: size))
        }
    }
}

// ✅ Load images asynchronously
final class AsyncImageLoader {
    private let cache = NSCache<NSString, UIImage>()
    private var tasks: [URL: URLSessionTask] = [:]

    func load(url: URL, into imageView: UIImageView) {
        // Check cache first
        if let cached = cache.object(forKey: url.absoluteString as NSString) {
            imageView.image = cached
            return
        }

        // Load asynchronously
        let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
            guard let data = data, let image = UIImage(data: data) else { return }

            // Cache the image
            self?.cache.setObject(image, forKey: url.absoluteString as NSString)

            DispatchQueue.main.async {
                imageView.image = image
            }
        }
        tasks[url] = task
        task.resume()
    }

    func cancel(url: URL) {
        tasks[url]?.cancel()
        tasks[url] = nil
    }
}

View Hierarchy Optimization

swift
// ❌ SLOW - Deep view hierarchy
view.addSubview(container1)
container1.addSubview(container2)
container2.addSubview(container3)
container3.addSubview(label)

// ✅ FAST - Flat hierarchy
view.addSubview(label)

// ❌ SLOW - Transparent layers
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)

// ✅ FAST - Opaque layers where possible
view.backgroundColor = .black
view.alpha = 0.5  // Or use solid color if acceptable

Concurrency Performance

Main Thread Protection

swift
// ✅ Keep UI work on main thread
func updateUI(with data: Data) {
    DispatchQueue.main.async {
        self.label.text = String(data: data, encoding: .utf8)
    }
}

// ✅ Heavy work off main thread
func processData() {
    Task.detached(priority: .userInitiated) {
        let result = await self.heavyComputation()
        await MainActor.run {
            self.updateUI(with: result)
        }
    }
}

Batch Operations

swift
// ❌ SLOW - Individual updates
for item in items {
    tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}

// ✅ FAST - Batch updates
tableView.performBatchUpdates {
    let indexPaths = items.indices.map { IndexPath(row: $0, section: 0) }
    tableView.insertRows(at: indexPaths, with: .automatic)
}

Network Performance

Request Optimization

swift
final class OptimizedNetworkClient {
    private let session: URLSession

    init() {
        let config = URLSessionConfiguration.default

        // Connection pooling
        config.httpMaximumConnectionsPerHost = 6

        // Caching
        config.urlCache = URLCache(
            memoryCapacity: 50_000_000,  // 50MB memory
            diskCapacity: 100_000_000     // 100MB disk
        )
        config.requestCachePolicy = .returnCacheDataElseLoad

        // Timeouts
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 300

        session = URLSession(configuration: config)
    }

    func fetch<T: Decodable>(_ url: URL) async throws -> T {
        var request = URLRequest(url: url)

        // Enable compression
        request.setValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")

        let (data, _) = try await session.data(for: request)
        return try JSONDecoder().decode(T.self, from: data)
    }
}

Core Data Performance

swift
// ✅ Batch fetching
let request = NSFetchRequest<Entity>(entityName: "Entity")
request.fetchBatchSize = 20  // Fetch in batches
request.returnsObjectsAsFaults = true  // Lazy loading

// ✅ Background context for heavy operations
let backgroundContext = persistentContainer.newBackgroundContext()
backgroundContext.perform {
    // Heavy operations here
    try? backgroundContext.save()
}

// ✅ Batch delete (much faster than individual deletes)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try context.execute(deleteRequest)

Performance Checklist

Launch Time

  • Defer non-essential work with DispatchQueue.main.async
  • Lazy-load view controllers
  • Use static/let for constants
  • Minimize work in application(_:didFinishLaunchingWithOptions:)

Scrolling

  • Reuse cells properly
  • Use estimated heights
  • Prefetch data
  • Avoid transparent views
  • Cache computed values

Memory

  • Use [weak self] in closures
  • Make delegates weak
  • Invalidate timers
  • Remove notification observers
  • Profile with Instruments

Network

  • Enable HTTP caching
  • Use compression
  • Batch requests where possible
  • Cancel unnecessary requests

Quick Wins

IssueSolution
Slow scrollingEnable cell reuse, prefetching
High memoryDownscale images, check leaks
Slow launchDefer work, lazy load
UI freezesMove work off main thread
Battery drainReduce background work, use efficient APIs