I have subclassed a UILabel with the following code, which works fine - but any animations involving the subclass runs a lot slower than normal UILabels. I'm assuming Quartz is the culprit, but is there anything I could do to speed things up a bit?
- (void)drawTextInRect:(CGRect)rect
{
CGSize shadowOffset = self.shadowOffset;
UIColor *textColor = self.textColor;
// Establish the Quartz 2D drawing destination:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1);
CGContextSetLineJoin(context, kCGLineJoinRound);
// Draw the label’s outline:
CGContextSetTextDrawingMode(context, kCGTextStroke);
self.textColor = [UIColor whiteColor];
[super drawTextInRect:rect];
// Draw the label:
CGContextSetTextDrawingMode(context, kCGTextFill);
self.textColor = [UIColor textColor];
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];
self.shadowOffset = shadowOffset;
}
What #MobileOverlord said is certainly applicable, especially the parts about about profiling.
I will note that setting shouldRasterize=YES is not a catch-all solution (why wouldn't Apple have set it on as default if that were the case?). Yes, it can improve scrolling performance, but it can do so at the expense of memory use since you can end up with a bunch of large images sitting around in cache.
It also incurs overhead at the time of creation, I believe (but would have to check to be sure) including an off-screen rendering pass to actually create the rasterized copy. Depending on how the layer is used, this could actually hurt performance.
An additional factor to consider is whether your view has any transparency. If you can guarantee to the frameworks that your view is opaque (cf. setOpaque/isOpaque), they can optimize rendering by not considering all the complexities associated with alpha channels, etc. Similar considerations apply to CALayer.
Finally, outside the block of code you showed, did you do anything sneaky to the backing layer (e.g. set a shadow or corner radius)? That's a quick way to kill performance on animation too.
After you are finished drawing your label you can call shouldRasterize on it's layer and it should speed up your animation.
shouldRasterize A Boolean that indicates whether the layer is rendered
as a bitmap before compositing. Animatable
#property BOOL shouldRasterize Discussion When the value of this
property is YES, the layer is rendered as a bitmap in its local
coordinate space and then composited to the destination with any other
content. Shadow effects and any filters in the filters property are
rasterized and included in the bitmap. However, the current opacity of
the layer is not rasterized. If the rasterized bitmap requires scaling
during compositing, the filters in the minificationFilter and
magnificationFilter properties are applied as needed.
When the value of this property is NO, the layer is composited
directly into the destination whenever possible. The layer may still
be rasterized prior to compositing if certain features of the
compositing model (such as the inclusion of filters) require it.
The default value of this property is NO.
From CALayer Class Reference
The simulator is always going to give you way better results than a device will because it is able to use the full processing power and memory of your system. You'll usually get flawed results this way. Whenever you are doing CoreGraphics drawing in conjunction with CoreAnimation it is important to test the results on a real device.
For this you can try to run your app in Instruments Core Animation Tool to try to find culprits. Check out my tutorial on it.
Instruments – Optimizing Core Animation
Related
I'm working on a custom view, that has some specific Core Graphics drawings. I want to handle the view's autoresizing as efficiently as possible.
If I have a vertical line drawn in UIView, and the view's width stretches, the line's width will stretch with it. I want to keep the original width, therefore I redraw each time in -layoutSubviews:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// ONLY drawing code ...
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self setNeedsDisplay];
}
This works fine, however I don't think this is a efficient approach - unless CGContext drawing is blazing fast.
So is it really fast? Or is there better way to handle view's autoresizing? (CALayer does not support autoresizing on iOS).
UPDATE :
this is going to be a reusable view. And its task is to draw visual representation of data, supplied by the dataSource. So in practice there could really be a lot of drawing. If it is impossible to get this any more optimized, then there's nothing I can do... but I seriously doubt I'm taking the right approach.
It really depends on what you mean by "fast" but in your case the answer is probably "No, CoreGraphics drawing isn't going to give you fantastic performance."
Whenever you draw in drawRect (even if you use CoreGraphics to do it) you're essentially drawing into a bitmap, which backs your view. The bitmap is eventually sent over to the lower level graphics system, but it's a fundamentally slower process than (say) drawing into an OpenGL context.
When you have a view drawing with drawRect it's usually a good idea to imagine that every call to drawRect "creates" a bitmap, so you should minimize the number of times you need to call drawRect.
If all you want is a single vertical line, I would suggest making a simple view with a width of one point, configured to layout in the center of your root view and to stretch vertically. You can color that view by giving it a background color, and it does not need to implement drawRect.
Using views is usually not recommended, and drawing directly is actually preferred, especially when the scene is complex.
If you see your drawing code is taking a considerable toll, steps to optimize drawing further is to minimize drawing, by either only invalidating portions of the view rather than entirely (setNeedsDisplayInRect:) or using tiling to only draw portions.
For instance, when a view is resized, if you only need to draw in the areas where the view has changed, you can monitor and calculate the difference in size between current and previous layout, and only invalidate regions which have changed. Edit: It seems iOS does not allow partial view drawing, so you may need to move your drawing to a CALayer, and use that as the view's layer.
CATiledLayer can also give a possible solution, where you can cache and preload tiles and draw required tiles asynchronously and concurrently.
But before you take drastic measures, test your code in difficult conditions and see if your code is performant enough. Invalidating only updated regions can assist, but it is not always straightforward to limit drawing to a provided rectangle. Tiling adds even more difficulty, as the tiling mechanism requires learning, and elements are drawn on background threads, so concurrency issues also come in play.
Here is an interesting video on the subject of optimizing 2D drawing from Apple WWDC 2012:
https://developer.apple.com/videos/wwdc/2012/?include=506#506
I'm trying to draw a graphic equaliser for an iOS project.
The equaliser will have 7 bars, representing different frequency bands, than move up and down based on real-time audio data.
Can anyone suggest the best way to approach this in iOS?
New frequency data comes in at about 11Hz, and so the bars would have to animate to a new size 11 times per second.
Do I create a UIView for each bar and dynamically resize it's frame height?
Do I draw the bars as thick CGStrokes and redraw them within the parent view as needed?
Another option?
Thanks in advance
You want to use Core Animation. The basic principle is to create a bunch of "layer" objects, which can either be bitmap images, vector shapes, or text. Each layer is stored on the GPU and most operations can be animated at 60 frames per second.
Think of layers like a DOM node in a HTML page, they can be nested inside each other and you can apply attributes to each one similar to CSS. The list of attributes available matches everything the GPU can do efficiently.
It sounds like you want vector shapes. Basically you create all your shapes at startup, for example in the awakeFromNib method of a UIView subclass. For simple rectangles use CALayer and set a background colour. For more complicated shapes create a CAShapeLayer and a UIBezierPath, then apply it with shapeLayer.path = bezierPath.CGPath;.
Then, whenever you want to change something, you apply those changes to the layer object. For example, here I'm rotating a layer with a 1 second linear animation:
[CATransaction begin];
[CATransaction setAnimationDuration:1];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
[self.needleLayer setValue:[NSNumber numberWithFloat:DegreesToRadians(degrees) forKeyPath:#"transform.rotation.z"];
[CATransaction commit];
// you'll want to declare this somewhere
CGFloat DegreesToRadians(CGFloat degrees)
{
return degrees * M_PI / 180;
}
More complicated animations, eg a series of changes scheduled to execute back to back, can be done using a CAKeyframeAnimation: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html
Note Core Animation only does 2D graphics. Apple has Scene Kit which is basically the same thing for 3D, but so far it's only available on OS X. Hopefully iOS 8 will include it, but until then if you want 3D graphics on iOS you need to use Open GL.
CALayers which you resize on demand would probably be the most efficient way to do this, if the bars are solid colours. This allows you to optionally animate between sizes as well.
View resizing triggers off layout cycles, which you don't want. Drawing using CG calls is pretty slow.
Obviously the only real way to find out is to profile (on a device) using instruments and the core animation tool. But from experience, layer sizing is faster than drawing.
Definitely not a UIView for each - instead, a single UIView for the entire equalizer. Fill in the drawRect method with the appropriate CG calls to draw whatever is required. You can queue the view to refresh as needed with the appropriate data. Tapping into CADisplayLink will help you get the frame-rate you're looking for.
https://developer.apple.com/library/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/Reference/Reference.html
NOTE: You can also subclass CALayer and draw in it if you prefer something lighter-weight than UIView but I think you'll be fine with the former.
I have a UITableViewCell in which inside it I have 5 UILabel a UIButton and a UIImageView that fills out the cell as background. The performance seems to be a bit slow, so I was thinking of using CoreGraphics to improve it. Is it true that using CoreGraphics instead of UILabel as subViews will make things much faster? If yes why?
I have the following code to draw shadow on the cells:
[self.layer setBorderColor:[UIColor blackColor].CGColor];
[self.layer setShadowColor:[UIColor blackColor].CGColor];
[self.layer setShadowRadius:10.0];
[self.layer setCornerRadius:5.0];
[self.layer setShadowPath:[[UIBezierPath bezierPathWithRect:self.frame] CGPath]];
In general, (as Gavin indicated) I would say that you have to first confirm that the subviews are indeed causing a jitter in your scrolling.
When I'm testing UITableViewCell scrolling performance, I often use the Time Profiler in Instruments. Switch to Objective-C Only in the left-hand panel, and look at what is taking the most time on your Main Thread. If you see lots of time spent on rearranging (layout) or drawing of subviews, you may need to use CoreGraphics. If the time instead is spent on allocation/deallocation, then you may want to examine how your subviews are reused if at all. If they are not being reused, then of course this can cause performance problems.
Then of course, you should look at compositing. If your subviews are not opaque (identify this through the CoreAnimation instrument), then the performance may be seriously impacted.
Also of note -- realize that shadows are expensive to draw, and depending on your implementation, they may be redrawing on every frame! Your best option is to make sure that any CALayer shadows are fully rasterized, and have a path defined so live computations from the pixel mask don't have to be made.
If finally, you identify that the layout and redrawing of each subview individually is causing the slowdown, then I have a couple of points/explanations:
Your implementation of the drawing routine for your table view cell will probably be slower than the highly optimized drawing that Apple has written for its views. So you won't win any battles re-implementing drawing of UIImageView itself. Performance gains instead come from two places when drawing with CoreGraphics: a.) Pre-rendering of previously non-opaque views, and reduction of time spent in the layout phase of the view drawing cycle - this reduces the workload on the GPU/CPU when scrolling. b.) Reduction in time switching CG contexts for individual view drawing. Each element now draws into the same graphics context at the same time, reducing switching costs.
Drawing in drawRect using CoreGraphics on the main thread draws using the CPU, and depending on how complex your cells are, this may cause jitters of its own. Instead, consider drawing in a background thread to a separate CGContext, then dispatching a worker to insert the contents of the drawing as a CGImageRef into a CALayer, or as a UIImage in a UIImageView. There is a naive implementation on GitHub: https://github.com/mindsnacks/MSCachedAsyncViewDrawing
If you do decide to go with background CoreGraphics drawing, be warned that at present (December 2012), I believe there is a bug in the NSString drawing categories when drawing on background threads that results in a fallback to webkit which is absolutely not thread safe. This will cause a crash, so for the present time, make sure that the asynchronous drawing is done in a serial GCD/NSOperation queue.
On the simulator, Debug→Color Blended Layers. Red is bad, green is good.
More accurately, red means that the GPU needed to do an alpha blend. The main cost is in the memory bandwidth required to draw the pixels twice and possibly re-fetch the extra texture. Completely transparent pixels are really bad.
Three fixes (all of which reduce the amount of red), which you should consider before diving into Core Graphics:
Make views opaque when possible. Labels with the background set to [UIColor clearColor] can often be set to a flat colour instead.
Make views with transparency as small as possible. For labels, this involves using -sizeToFit/-sizeThatFits: and adjusting layout appropriately.
Remove the alpha channel from opaque images (e.g. if your cell background is an image) — some image editors don't do this, and it means the GPU needs to perform an alpha test and might need to render whatever's behind the image.
Additionally, turn on and Color Offscreen-Rendered (possibly after turning off Color Blended Layers so it's easier to see). Offscreen-rendered stuff appears in yellow, and usually means that you've applied a layer mask. These can be very bad for performance; I don't know if CoreAnimation caches the masked result.
Finally, you can make CoreAnimation rasterize the cell by setting cell.layer.shouldRasterize = YES (you might also need cell.layer.rasterizationScale = [[UIScreen mainScreen].scale on retina devices; I forget if this is done automatically). The main benefit is that it's easy, and it's also more efficient than rendering the image view yourself in Core Graphics. (The benefit for labels is reduced since the text needs to be rendered on the CPU.)
Also note that view animations will be affected. I forget what setting CALayer.shouldRasterize does (it might re-rasterize it every frame of the animation, which is a bit wasteful when it'll only be drawn to screen once), but using Core Graphics will (by default) stretch the rendered content during the animation. See CALayer.contentsGravity.
What evidence do you have to suggest that what you have in the views is causing the performance issues? This is a deep black hole that can suck you in, so be sure you know the problem is where you think it is.
Are you preloading all your data? Have you pre-downloaded the images? What you're describing shouldn't be causing a slow down in UITableViewCell. Apple developers are much smarter than you and I so make sure you've got the data to back up your decision!
I've also seen a lagging UITableViewCell within the simulator with no noticeable difference on real hardware.
It is true that using CoreGraphics can speed up your draw performance but it can also slow it down if you do it wrong! Have a look at the Apple Tutorial on Advanced Table View Cells for how to perform the technique.
I recently came across this brilliant article about improving scroll performance with UITableViewCells: http://engineering.twitter.com/2012/02/simple-strategies-for-smooth-animation.html -- While many great tips can be found in this article, there is one in particular that has me intrigued:
Tweets in Twitter for iPhone 4.0 have a drop shadow on top of a subtle textured background. This presented a challenge, as blending is expensive. We solved this by reducing the area Core Animation has to consider non-opaque, by splitting the shadow areas from content area of the cell.
Using the iOS Simulator, clicking Debug - Color Blended Layers would reveal something like this:
The areas marked in red are blended, and the green area is opaque. Great. What the article fails to mention is: How do I implement this? It is my understanding that a UIView is either opaque or it's not. It seems to me that the only way to accomplish this would be with subviews, but the article explicitly states that as being a naive implementation:
Instead, our Tweet cells contain a single view with no subviews; a single drawRect: draws everything.
So how do I section off what is opaque, and what is not in my single drawRect: method?
In the example you show, I don't believe they're showing a background through the view. I think they're simulating a background in core graphics. In other words, in each cell they draw a light gray color for the background. They then draw the shadow (using transparency), and finally they draw the rest of the opaque content on the top. I could be wrong, but I don't believe you can make portions of the view transparent. If so, I'd be very, very interested in it because I use core graphics all the time, but I avoid rounded corners because blending the entire view for it just doesn't seem to be worth it.
Update
After doing some more research and looking through Apple's docs, I don't believe it's possible for only part of a view to be opaque. Also, after reading through Twitter's blog post, I don't think they are saying that they did so. Notice that when they say:
Instead, our Tweet cells contain a single view with no subviews; a single drawRect: draws everything.
They were specifically talking about UILabel and UIImageView. In other words, instead of using those views they're drawing the image directly using Core Graphics. As for the UILabels, I personally use Core Text since it has more font support but they may also be using something simpler like NSString's drawAtPoint:withFont: method. But the main point they're trying to get across is that the content of the cell is all one CG drawing.
Then they move to a new section: Avoid Blending. Here they make a point of saying that they avoid blending by:
splitting the shadow areas from content area of the cell.
The only way to do this is to use different views. There are two approaches they could be using, but first note that the cell dividers are themselves overlays (provided by the tableView). The first way is to use multiple views inside the cell. The second way is to underlay/overlay the shadows/blended-views behind/over the cells by inserting the appropriate views into the UIScrollView. Given their previous statement about having only one view/drawRect for each cell, this is probably what they're doing. Each method will have its challenges, but personally I think it would be easier to split the cell into 3 views (shadow, content, shadow). It would make it a lot easier to handle first/last cell situations.
I'd have to guess something along these lines
http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_shadows/dq_shadows.html
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10.0f];
CGContextSaveGState(context);
CGRect leftRect = CGRectZero;
CGContextClipToRect(context, leftRect );
CGContextSetBlendMode(context, kCGBlendModeNormal);
// draw shadow
// Call the function CGContextSetShadow, passing the appropriate values.
// Perform all the drawing to which you want to apply shadows.
CGContextSetShadowWithColor(context, CGSizeMake(1.0f, 1.0f), 10.0f, [UIColor blackColor].CGColor);
CGContextAddPath(context, path.CGPath);
CGContextDrawPath(context, kCGPathStroke);
CGContextRestoreGState(context);
CGContextSaveGState(context);
CGRect middleSection = CGRectZero;
CGContextClipToRect(context, middleSection);
CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
CGContextFillRect(context, self.bounds);
// draw opaque
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextRestoreGState(context);
My opinion is: Don't let Core Animation draw shadows using the various layer properties. Just draw a prerendered image to both sides, which is in fact a shadow. To factor variable height of a cell in a stretch draw may do the trick.
EDIT:
If the background is plain a prerendered shadow can be applied to both sides without know it is affecting visual appeal.
In case that is not applicable the tableview has to be shrunk to be of the size without the shadow. Then the shadow can be blended without doing it for every cell but just "on top". It really doesn't scroll. This will only work if the shadow is without any "texture", else one will notice it's just applied on top.
This isn't in reference to any particular code, but I've noticed that when I have a UIView that has a shadow added to it's layer, the animation when rotating between interface orientations becomes much more laggy/choppy.
Has anyone noticed this issue or found a workaround?
When using shadows, the shadowPath property of CALayer makes a very (!) noticable difference in performance, especially on the New iPad. Although I agree that disabling shadows when changing the display orientation is a good idea, it may be worth a try to just use shadowPath (if you do not use it already). Although the path can be any valid CGPathRef, in most cases this is what you want:
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath;
There are things which are very expensive in terms of CPU time. Check it out in Instruments some time.
shadows
bezier paths
bezier paths with dashes (really expensive)
Thats not a comprehensive list. I suspect gradients will be there too.
If you find these things are degrading your animation or redraw you will need to toggle them in the UIViewController methods.
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//disable shadows + expensive drawing
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
//enable shadows + expensive drawing
}