Saurabh 😎
WWDC 2012: iOS App Performance: Graphics and Animations
2 parts to performance:
Responsiveness - animation begins immediately after user action
Smooth - no dropped frames during animation
Scrolling is a special type of animation
Performance workflow:
- Measure and get baseline using Instruments
- Form hypothesis (which may include taking more measurements)
- Make a change
- Measure again
- Repeat!
Especially important to be methodical for graphics/animations since some techniques can help or hurt (so can't just blindly follow advice)
Every UIView
is backed by a CALayer
- View layout is really layer layout
drawRect:
draws into layer backing store- Layout and drawing happens in your app
Layer properties and animations in render server (backboard in iOS) - Changes are sent to render server in
CA::Transaction::commit()
(called implicitly for you at end of run loop if you create an animation or modify the layer tree)
3 steps to animation:
- Create animation and update view hierarchy (in your app process)
e.g.UIView.animateWithDuration
that callsaddSubview:
in the animation block - Prepare and commit animation (in your app process, but called implicitly for you)
4 phases:- Layout - setup views
- Expensive view creation and layer graph management
- May need to do I/O to create the views
- Usually CPU bound, but sometimes even I/O bound
- Display - draws the views
- Each of the
drawRect:
gets called - Usually CPU bound
- Each of the
- Prepare - does CoreAnimation work
- Image decoding, i.e. updating layer's bitmaps with image contents
- Note that in Instruments, this may show up in a different thread
- Commit - packages up layers to render server
- May be expensive for very complex layer tree
- Layout - setup views
- Render each frame (render server)
- Usually GPU bound, but also competes for CPU with your app
What delays an animation? (remember that more delays = less responsiveness)
- Slow layout (e.g. complex hierarchy, I/O to get data)
- Drawing - string drawing and image decoding are expensive
Note: all drawing and layout is done upfront; even if a view doesn't appear until frame 15, will still do layout/draw at beginning
First need to determine whether you bottleneck is CPU bound vs GPU bound
- Core Graphics drawing and image decoding happens on CPU
- Although by the time an animation starts, these have already happened (except for scrolling)
- Render server also needs some CPU per layer, per frame
- CPU works scales linearly with # of layers
- Actual rendering is GPU bound
- Check device utilization in OpenGL ES Instrument - if 100%, then likely GPU-bound animation
If GPU bound, use Core Animation instrument
Color Blended Layers
- Starting with iPhone 3GS, GPU can handle overdraw of ~2.5 at 60fps (i.e. GPU can draw to each pixel 2.5 times - and this is on slowest device)
- 2 solutions:
- Make sure every layer is marked as opaque
- Flatten the view hierarchy
- But not a magic solution - more CPU up-front in client for less render server work (so tradeoffs responsiveness for smoothness)
- Basic technique: draw into single view instead of using subviews
- However, a large drawRect can hurt responsiveness, so may need to trade off responsiveness and smoothness
- Alternative technique:
setShouldRasterize
- Instead of blending layers on every frame, GPU will first do one offscreen rendering pass, rasterize bitmap, and then re-use rasterized bitmap
- Cache is invalidated when any sub-layer changes (e.g. if you rasterize parent layer, and change one of the subviews, entire parent rasterization cache gets busted)
- Verify with "Color cache hits/misses" in Instruments
- Make sure to measure - this can sometimes hurt performance, especially if you're not actually GPU-bound
Scrolling
- For most animations, you setup the animation once, and then render server renders each frame
- For scrolling, we do a separate animation for every scroll update
- Every 16ms, we have to: (1) calculate new scroll position, (2) prepare and commit animation (layout + drawing), (3) render frame
- Table view cells are only drawn once, but every time a new cell enters the screen, it has to do layout+drawing
- For fast scrolling, can have multiple new table cells per frame - so each table view cell has to draw+layout in much less than 16ms
- Tips:
- Reuse cells and views
- Minimize layout and drawing time
- Speculative work - e.g. kick off background work to start fetching data a few cells ahead
- Flatten view hierarchy (if you're GPU bound - super important to test and iterate, since easy to flatten too much and go from GPU-bound to CPU-bound)
Final tip: different devices may have different bottlenecks - some CPU-bound, some GPU-bound, so test on variety of devices