跪拜 Guibai
← All articles
JavaScript

iOS Drawing Performance: Why Core Graphics Slows to a Crawl and How to Fix It

By 悟空瞎说 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A drawing app that feels smooth for three strokes and janky at thirty is a memory and threading problem, not a rendering problem. The techniques here—hardware layers, dirty-rect clipping, and asynchronous drawing—apply to any interactive iOS view where content accumulates, from signature panels to annotation tools.

Summary

A full-screen software redraw on a Retina iPad allocates 12 MB of memory (2048×1536×4 bytes) and blocks the main thread. That cost repeats every time a view calls `drawRect:`, making interactive drawing applications degrade linearly as content accumulates. The core fix is to avoid Core Graphics entirely when possible: `CAShapeLayer`, `CATextLayer`, and `CAGradientLayer` offload vector drawing to the GPU and eliminate the backing store overhead.

When Core Graphics is unavoidable—such as stamping a chalk-texture brush along a touch path—the bottleneck shifts to overdraw. Marking only the affected rectangle with `setNeedsDisplayInRect:` and intersecting it against the dirty rect inside `drawRect:` prevents the entire stroke history from being re-rendered on every frame. The technique turns a linear slowdown into near-constant performance.

For cases where drawing still stalls the UI, iOS offers two asynchronous paths. `CATiledLayer` splits work across background threads per tile, while the `drawsAsynchronously` property (iOS 6+) defers Core Graphics commands to a background queue even though the delegate method runs on the main thread. Both keep the app responsive during heavy redraws, though `drawsAsynchronously` benefits most from frequently-updated views like table cells.

Takeaways
Software drawing via `drawRect:` allocates a full bitmap backing store (width × height × 4 bytes) on every redraw; a Retina iPad full-screen view costs 12 MB per refresh.
`CAShapeLayer`, `CATextLayer`, and `CAGradientLayer` perform the same vector drawing on the GPU, avoiding both the memory allocation and the main-thread CPU cost.
Replacing a `UIBezierPath`-based line-drawing implementation with `CAShapeLayer` keeps frame rates stable until paths become extremely complex.
When Core Graphics is required, calling `setNeedsDisplayInRect:` with only the changed area and checking `CGRectIntersectsRect` inside `drawRect:` prevents re-rendering the entire stroke history.
`CATiledLayer` invokes `drawLayer:inContext:` on multiple background threads simultaneously, one per tile, enabling asynchronous updates without blocking the main thread.
The `drawsAsynchronously` property (iOS 6+) queues Core Graphics drawing commands to a background thread even though the delegate callback runs on the main thread, best suited for frequently redrawn views like table cells.
Conclusions

The 12 MB figure for a single full-screen software redraw explains why UIKit aggressively caches backing stores and why Apple steers developers toward GPU-backed layers—the memory pressure alone makes `drawRect:` a last resort.

Switching from `UIBezierPath` to `CAShapeLayer` is not just a performance win; it changes the programming model from imperative redraw-on-dirty to declarative path assignment, which sidesteps the overdraw problem entirely.

Dirty-rectangle clipping is a manual optimization that Core Animation cannot infer from custom drawing code, making it a rare case where the developer must explicitly tell the framework what changed.

The distinction between `CATiledLayer`'s true multi-threaded drawing and `drawsAsynchronously`'s deferred command queue matters: the former parallelizes work, while the latter only moves execution off the main thread without splitting the workload.

Concepts & terms
Backing store
A bitmap memory buffer allocated for a layer or view when software drawing is triggered via `drawRect:` or `drawLayer:inContext:`. Its size equals width × height × 4 bytes (RGBA). The buffer is erased and reallocated on every redraw, making it expensive for large or frequently-updated views.
Dirty rectangle
The sub-region of a view that has actually changed and needs redrawing. By passing this rectangle to `setNeedsDisplayInRect:` and clipping drawing to it inside `drawRect:`, an app avoids re-rendering unchanged content. Core Animation cannot compute dirty rectangles automatically for custom drawing code.
drawsAsynchronously
A `CALayer` property introduced in iOS 6. When enabled, Core Graphics drawing commands issued inside `drawLayer:inContext:` are queued and executed on a background thread after the delegate method returns, keeping the main thread responsive. Most effective for views that redraw frequently.
From the discussion

The discussion questions the article's relevance, dismissing its content as outdated material copied from an older source.

The article's screenshots and content appear to be recycled from an older era of iOS development, undermining its contemporary value.
Featured comments
samuelandkevin

This screenshot of the simulator and Xcode version looks like it was copied from somewhere. A relic of a bygone era.

qo_tn

Why does this feel like an archaeological article?

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗