Have you ever been using an app and a certain feature just seemed to be overwhelmingly sluggish compared to the rest of the app? I'm not here to judge (I totally am), but a lot of times scroll view animations can really suffer in apps due to developers using techniques that are less than ideal.
If the app in question is your app, then I wouldn't be a good friend if I didn't point out that you have a problem.
How to Catch Your App in the Act
Sometimes you can bust out Time Profiler and do some profiling to figure out what exactly is going wrong, but a lot of times the problem won't be super obvious from your stack traces.
If this is the case, then it might be time to reach for the Core Animation Instrument.
This instrument is nifty because, not only does it give you a (decent) real-time readout of your app's fps, but it also provides you with a set of render debug options that will show you when your code is indirectly causing problems for the GPU.
They can all be pretty useful, but one of the most handy options is the Color Offscreen-Rendered Yellow option.
One fun thing about these options is that you can turn them on in Instruments, and then switch to any app on your phone to see how it's doing.
In this example you can see that the native Twitter app is doing some pretty aggressive offscreen rendering while loading the stretchy profile header. The video is (unfortunately) a real-time capture of the animation on my iPhone 5.
As you can probably guess, this is not an ideal experience for people using your app.
So What Exactly is Offscreen Rendering?
This concept is one of the more confusing ideas when considering performance on iOS, so I'll do my best to try to help clear up some of that confusion.
On the CPU: Offscreen rendering, or offscreen drawing can just mean what happens when you need to draw a bitmap in memory using software instead of rendering directly to the screen.
For example, writing your own draw method with Core Graphics means your rendering will technically be done in software (offscreen) as opposed to being hardware accelerated like it is when you use a normal CALayer. This is why manually rendering a UIImage with a CGContext is slower than just assigning the image to a UIImageView.
This type of offscreen rendering can be advantageous at times but should be undertaken only alongside careful testing and measurement.
On the GPU: There's a very specific type of offscreen rendering that can occur when you ask a CALayer to draw something, but haven't given it enough information.
This means that when the Render Server goes to render your layer hierarchy it will get to a layer subtree that it doesn't fully know how to render yet.
This forces it to stall and switch contexts from its normal "onscreen" rendering to "offscreen" rendering in order to fully figure out how to draw what it needs to. Once its done, it can switch back to onscreen drawing and proceed as normal.
The real damage is done by the two context switches! These can really add up if you have many views being animated that force this kind of GPU stall.
This is what the Core Animation Instrument is trying to show us when it colors layers yellow.
There's a few common scenarios where this will happen and pretty straightforward fixes for each.
Drawing (View Shaped) Shadows
When drawing shadows, it's really easy to use the CALayer shadow properties. You can technically draw a shadow by using just the offset, color, radius and opacity properties.
Unfortunately, if you do, you'll force the type of very-bad-for-your-app offscreen rendering we're trying to avoid because the GPU won't automatically know the shape of the shadow.
In this case, just make sure you always set the shadowPath property to tell the layer what shape your shadow will be. If it isn't possible to use this property, then don't use any of these properties at all.
But wait, there's more!
While that strategy isn't bad, one thing to keep in mind is that the drawn shadow is dynamic.
The vast majority of the time, you won't be animating a view's shape and expecting the shadow to animate along with it.
When this is the case, you can just have your designer do the hard work and provide you with a good shadow image.
This way, they can make the shadow look exactly the way they want, and you can apply it to your view by creating a stretchable version of the image in code and then using a UIImageView that you can place directly behind the view you want to give a shadow to.
Make sure to use the .stretch option as the resizing mode.
This will take whatever image you were given, and define how far in the non-stretched edges go.
The middle will be stretched, but this shouldn't matter since you only care about the edges sticking out from under your view.
Ideally, the image you use will be as small as possible to reduce the amount of computation necessary to load and render the image.
As far as text goes, both of these methods can be used to draw generic blob shaped shadows, which is fine as long as your designer approves.
Drawing (Text Shaped) Shadows
Alternatively, if you're trying to draw drop shadows behind text, then you've got a little more work to do.
The most straightforward option is to opt for using an NSShadow along with an NSAttributedString.
Drawing Round Corners
This can be another big source of offscreen rendering in your feed. Luckily, you can once again avoid this offscreen render pass by using a UIBezierPath to round your corners.
To do so, you can just write a UIImage extension to get pre-drawn round images like so:
Like I mentioned earlier, this is technically "offscreen" in some sense, but there shouldn't be a back and forth with GPU stalls to worry about, thus the corner rounding becomes inconsequential.
Using the .shouldRasterize Property
I mentioned this in a earlier post, but the tl;dr is that you need to be really sure it will help your app.
Odds are it won't and instead it will introduce an offscreen render pass on every frame of any animation involving that layer.
At the end of the day, performance is a critical aspect of how enjoyable your app is and with a little extra thought you can make sure your app isn't slowing down unnecessarily.
Have you found any other nifty ways of avoiding offscreen rendering? Let me know in the comments!