I make extensive use of -drawRect: to do some nice animations. A timer tries to fire 30 times per second with -setNeedsDisplay, but it feels like just 20 times. Also I can't use -setNeedsDisplayInRect: because the animation covers the entire thing.
Would it help to take some of those drawing operations out of -drawRect: and move them to a subview? -drawRect has to do less then, but instead the OS will have more work with compositing views.
Is there a rule of thumb which one is more worse? I remember from an apple text that they claimed Core Animation doesn't redraw during animation. So is that their secret of speed? Using subviews in animations?
Much of this will be similar to my answer to your other question:
Compositing is faster, by far. On the iPhone, content is drawn into a CALayer (or into a UIView's backing CALayer) using Quartz drawing calls or from a bitmapped image. This layer is then rasterized and effectively cached as an OpenGL texture on the GPU.
This drawing and caching operation is very expensive, but once the layer is on the GPU, it can be moved around, scaled, rotated, etc. in a hardware-accelerated manner. All the GPU has to do while the layer is animating is to composite it with the other onscreen layers every frame. This is why Core Animation can do 50 layers animating around the screen at 60 FPS on even the oldest iPhone models.
Layers and views only redraw themselves when prompted, or if resized when their needsDisplayOnBoundsChange property is set to YES. The drawing system is set up this way because of how expensive it is to redraw and recache the layer contents. If you can, avoid redrawing your layer content regularly, but instead split it into layers or views that you can animate around individually using Core Animation. If you need to animate a changing shape, look to CAShapeLayer, which makes this much more efficient than simply redrawing every frame.
As a worst case, yes, you can take the portions of your view that are static and move them to one view, then have a subview which has only the changing portion of your drawing within it. Performance won't be great, but it will be much better than if you had to redraw everything within the view. Compositing overhead will be negligible compared to the expense of drawing.
Related
Exactly what performance-critical things should I have an eye out for?
I want a list with as many examples as possible. Or a list of best practices.
Offscreen rendering / Rendering on the CPU
The biggest bottlenecks to graphics performance are offscreen rendering and blending – they can happen for every frame of the animation and can cause choppy scrolling.
Offscreen rendering (software rendering) happens when it is necessary to do the drawing in software (offscreen) before it can be handed over to the GPU. Hardware does not handle text rendering and advanced compositions with masks and shadows.
The following will trigger offscreen rendering:
Any layer with a mask (layer.mask)
Any layer with layer.masksToBounds / view.clipsToBounds being true
Any layer with layer.allowsGroupOpacity set to YES and layer.opacity is less than 1.0
When does a view (or layer) require offscreen rendering?
Any layer with a drop shadow (layer.shadow*).
Tips on how to fix: https://markpospesel.wordpress.com/tag/performance/
Any layer with layer.shouldRasterize being true
Any layer with layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsEdgeAntialiasing
Any layer with layer.borderWith and layer.borderColor?
Missing reference / proof
Text (any kind, including UILabel, CATextLayer, Core Text, etc).
Most of the drawings you do with CGContext in drawRect:. Even an empty implementation will be rendered offscreen.
Blending
resizableImage can cause blending.
Avoiding blended layers when using resizableImages on iOS
Any layer which is not opaque and has a backgroundColor with alpha less than 1.0
Any layer with alpha less than 1.0
Any layer with layer.content or any UIImageView with a UIImage having an alpha channel
Layout
The following things will trigger layoutSubviews to be called on a UIView:
Changing bounds triggers on the same view and superview
Changing frame triggers on the same view and superview
Changing transform or layer.transform triggers on superview
Note: I'm referring to real changes where values actually are changing
Contradictory these changes does not trigger layoutSubviews to be called: center, layer.position, layer.zPosition, layer.anchorPoint, layer.anchorPointZ.
Reference: https://github.com/hfossli/LayoutSubviewsInconsistency
General tips for improving performance
Oftentimes it is better to blend than to render offscreen.
Consider using drawRect: instead of having a view with multiple labels and subviews.
Draw on a background queue to a UIImage or CGImageRef.
Draw to a CGLayer (which is cached better on GPU compared to UIImage), and draw whatever you want into it.
Update, don't: http://iosptl.com/posts/cglayer-no-longer-recommended/
Flatten your hierarchy
Reuse views – don't create and add new ones while scrolling
Have opaque views with solid background color
Avoid setting alpha and layer.opacity to less than 1.0
Enable layer.shouldRasterize (use with care). I like to avoid this personally, but it performs faster in some occasions since rasters of the layer will be cached and reused. Remember if you enable shouldRasterize on layers that changing their content or sublayers contents frequently will cause the performance to drop, since iOS will keep rasterizing the layer on each change.
Links
http://iosinjordan.tumblr.com/post/56778173518/help-my-tables-dont-scroll-smoothly
https://developer.apple.com/library/IOS/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/DrawingTips/DrawingTips.html
I have a UIScrollView that contains a custom UIView. In my UIView I am overriding drawRect to draw a path using CGContextStrokePath. I would like to slightly alter the way the zoom works. Pinch zooming out will show more of the paths on the screen. This is what I want but i want the line width to stay the same not shrink as you zoom out so that they are still clear to the user.
I thought I would just do this (myUIView zoom target is called _lineView)
-(void)scrollViewDidZoom:(UIScrollView *)pScrollView
{
_lineView.zoomScale = _scrollView.zoomScale;
[_lineView setNeedsDisplay];
}
and then just calculate an appropriate line stroke size in my _lineView drawRect method to give the effect of constant line width as you zoom out.
This is really slow and I have read that this is expected as drawrect is not optimised to be called many times a second.
I then started looking at using a GLKView instead and just rendering the whole thing in opengl directly. The problem with this is I will have to implement all of the zooming and panning myself (with all the lovely zoom and pan bounce effects you get for free in UiScrollView). I will also have to implement all the controls I want to use in opengl, buttons etc.
Is there a way to do this whilst still using Quartz2d? I feel like opengl will give me lots of power but it will take me much longer to get the rest of my app done if I go down that route.
I figured this out. I found a simple way to do this that does not slow down the zoom / bouncing animations at all and is very fast and fluid.
Quartz has a class called CAShapeLayer that lets you do some pretty cool stuff. Among these is being able to set a CGPath property and specify a linewidth. Changes are reflected in the view.
So i basically call shapeLayer.lineWidth from my scrollViewDidZoom method and it does exactly what i need.
What I would do if I were you is to use default zooming behavior while zooming (which does not redraw, but instead just applies a transform to the zoomed view, which can be done by the GPU very, very quickly, but as you have noticed can lead to inferior quality).
Then, when the user finished zooming, redraw the whole view as you do now. The appropriate delegate method is scrollViewDidEndZooming:withView:atScale:.
This way, you have fast (but slightly ugly) zooming, and nice (but slightly, probably unnoticeably, slower) display after the zoom is finished.
Currently I have a horizontal UIScrollView populated with UIViews. The views are dequeued when moving off screen and reused. Each UIView is subclassed to draw images using drawInRect with [someView setNeedsDisplay] being called when the view enters the screen.
[_image drawInRect:imageRect];
My gradients, text, shapes, etc all load smoothly but as soon as I draw an image I suffer a noticeable performance hit. The size of the image doesn't seem to matter because the scroll view always lags. (I get the same result when using a UIImageView as well.) All of my images are loaded beforehand too.
Is there a better way to draw images that won't result in poor performance?
If you're just drawing images, you might consider using CALayers with CGImageRef contents—drawRect: overrides are going to be slower than CALayer compositing. Another thing to watch out for is alpha-blended images, which require frequent recompositing, or images that aren't aligned to integral pixels. The Improving Image Drawing Performance on iOS tech note is a great place to start on this kind of thing. Once you have the basics in hand, the Core Animation instrument in Instruments will be of great help to you—it watches out for blending, copied images, misaligned images, and screen updates.
I have an EAGLView which I am resizing in an animation, with the animation driven by an NSTimer that calls a draw function. As I resize the EAGLView, I need to adjust the projection for the view to maintain the aspect ratio of the contents. The draw function looks like this:
gameView.frame = newFrame;
[gameView setFramebuffer];
[self updateProjection];
/* Drawing to the EAGLView, gameView, here, then... */
[gameView presentFramebuffer];
Run like this, though, the contents of the EAGLView appear to "stair-step" down the animation. When I record it and look at it frame by frame, it's clear that the projection is adjusted, then the view is resized a very short time later (less than one animation frame).
I suspect that the reason is that the change in the frame of gameView is being deferred, and that in the meantime the updated framebuffer is making its way to the screen. I've tried using CATransactions to get the frame update to take effect immediately, but as I kind of expected for a UIView change, that did nothing. I suppose I could modify the viewport and leave the EAGLView full-frame, but I worry that that might just leave me with synchronization issues elsewhere (say, with updates to any overlaid CALayers).
Does this seem like a reasonable assessment of the problem? How can I prevent it -- that is, how can I best ensure that the framebuffer presentation coincides with the change in the actual frame of the EAGLView (and other CA elements)?
Thanks!
I encountered a similar issue with a custom EAGLView I wrote. The problem was that when changing orientation, the view got resized and the contents stretched, and only after the animation, the scene with the right proportions was displayed.
I think yours is the same problem, and I don't think you can force CoreAnimation to wait for your view to update before rendering an animation frame, because of the way it works (CA isn't aware of the drawing mechanism of the view or the layer on which operates).
But you can easily bypass the problem: open you xib file containing the view, switch to the attributes inspector on the right. Select your EAGLView (the same applies to GLKView), and under the section View there's the Mode attribute. If you're creating the view programmatically, set the contentModeproperty.
This value describes how the view contents are managed during an animation. Until the buffer is displayed, the old scene gets resized according to that mode; perhaps you can find a mode that fits the animation you need to achieve.
If you're worried about proportions, the center content mode may work, but the part of scene that hasn't been rendered yet will appear empty during the animation.
Unfortunately the redraw mode won't necessarely make your EAGLView refresh: the view is actually redrawn, but potentially with the old contents of the color buffer. It's a problem of getting the buffer filled at the right time.
You could try to resize and prerender a frame big enough to cover the whole animation before starting it; or you could try to render a frame to an UIImage, replace the view on the fly, animate it, the place the EAGLView back, but this may seriously affect performance.
The way I solved my issue was making simply the EAGLView big enough.
Regarding the blending of other CALayers, as far as I tried, I encountered no synchronization issue; the EAGLView updates when it can, and the other layers too. Just set the right properties to CAEAGLLayer if you're using alpha channel, and rememeber that blending the layers every time OpenGL updates your scene may be expensive in terms of performance.
I have a UIView with a large number of CALayers (~1000), each of which has a small image as its contents. This UIView is a subview of a scrollview (actually it's a subview of another view which is a subview of the scrollview). This draws relatively quickly at first (couple seconds). However when I scroll in the scrollview the frame rate is very low.
Is it trying to redraw the contents of each CALayer every time I scroll? Is there a way to disable this? Is something else going on?
Note: I set clearsContextBeforeDrawing to NOo n my UIView and this helped somewhat but it's still much slow than I would expect
The layers should not be redrawn, but they will be composited using the GPU. Try using the Sampler and OpenGL ES instruments in Instruments to see if your bottleneck is in the CPU or GPU. You can log extra detail within the OpenGL ES instrument by tapping on the "i" next to the instrument name and selecting the extra parameters (Tiler Utilization, Renderer Utilization, etc.). If one of the OpenGL values is near 100%, then it's the compositing that's holding you back. However, if the sampler is showing a number of calls to -drawRect: or similar methods, then your layers are being redrawn (for some reason) and that could be the problem.
Additionally, you can use the Core Animation instrument to color non-opaque layers, which can lower your display framerate.
Set the needsDisplayOnBoundsChange property on your layers to NO:
[layer setNeedsDisplayOnBoundsChange:NO];