I've been working on optimizing scrolling performance (f.p.s.) of an image heavy news feed table view.
I noticed that when I added in the following, which I expected would slow things down, performance actually felt better, i.e. less stuttering.
[UIView transitionWithView:self
duration:0.3f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{ self.image = image; }
completion:NULL];
I'm willing to believe that the cross dissolve effect gives the illusion of smoother scrolling, but I'd also buy it that the animation/drawing is being dispatched off of the main thread (is that even possible), freeing up the UI to focus on the other aspects of scrolling minus drawing the image on the canvas.
So what gives, am I imagining things?
It's difficult to profile Core Animation for scrolling since it's dependent on network (images are URL based), scroll speed, velocity etc. so this is a purely anecdotal observation. Any recommendation for doing some more consistent scrolling profiling would be welcomed.
UIView Doc for reference.
Related
I'm building a title sequence for our game, where each title is a roughly half-screen-sized retina image which I'm displaying using a UIImageView.
The title sequence has simple 3 stages as it gradually grows and fades in/out:
// 1. Fade in and grow
[UIView animateWithDuration:1.0f animations:^{
titleImageView.alpha = 1.0f;
titleImageView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} completion:^(BOOL finished) {
// 2. Stay opaque, grow a little more
[UIView animateWithDuration:2.0f animations:^{
titleImageView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
} completion:^(BOOL finished) {
// 3. Fade out, grow even further
[UIView animateWithDuration:2.0f animations:^{
titleImageView.alpha = 0.0f;
titleImageView.transform = CGAffineTransformMakeScale(1.3f, 1.3f);
} completion:nil];
}];
}];
At the start of each animation stage, there's a stutter as a frame or two is dropped. It's especially noticeable on older hardware such as iPhone 4 and iPad 3, but it's even noticeable on an iPad Air, which is surprising.
Some deductions:
It's got nothing to do with the loading of the UIImage itself, because I've tried pre-loading the data and ensuring that the PNG has been decompressed. Also the stutter happens at every stage of the animation, even after it has been on screen for a while.
I used the Time Profiler instrument and noticed that CA seemed to be copying around PNG data in the background each time it stuttered, although I'm not sure why / what for. Surely the CALayers shouldn't need to re-create any image data on CPU each time the transform is changed?
EDIT: Also note that I tried it without animations (just setting those transform properties, also without alpha changes), and got the same results.
Also note that I have some OpenGL ES graphics going on in the background (it's a game with UIKit controls in the foreground), but that hasn't caused problems in the past...
So, it's slightly ugly, but I solved it with two approaches simultaneously:
I converted the cascading UIView animations into a single CALayer keyframed animation so that they're continuous rather than stopping at each stage, calling the completion handler, and starting a new animation. Perhaps this stops them from "settling" and re-rendering in their new scale, as Brian Nickel pointed out.
But this didn't prevent the very first stutter as it showed each new UIImage. To solve this, I created all UIImageViews (6 of them in total) up-front, added them to the superview, and set their alphas to zero. The images aren't huge, and there aren't too many of them, so this seemed okay.
My app, which is a game, includes a CADisplayLink timer which calls a function that instructs about 20 calls to UIView setCenter: for the various objects on screen every frame.
Time profiling it, this accounts for about 30% of all activity in the game and drastically reduces performance on older devices (anything lower than 5th generation ipod touch or iphone).
Are there any lightweight, low-overhead alternatives I can use to move objects (specifically UIViews) around the screen every frame?
EDIT:
Just to clarify, the center property of these UIViews must be set EVERY FRAME. I have a number of tiles that represent the ground in my game. They zip across the screen, only to be replaced by new tiles. After fiddling with the code for a couple hours to change the UIViews to CAlayers, I have it working at absolutely no performance gain. There surely is a better way to do this.
Some code to give a general idea of what is going on:
for(Object* o in gameController.entities){
[o step:curTimeMS];
}
gameController is, as one would think, a class that takes care of the main game functions. It includes its list of entities, which are all the objects on-screen. The step method on each of these entities is a virtual function, so it is specific to each entity - the curTimeMS variable is simply a timestamp so the object can calculate its delta position. In essence, each entity updates its layer.position property every frame, moving it at an appropriate speed across the screen.
I would recommend SpriteKit. It is a very powerful game / 2d animation framework created by apple.. Cocos2D is also a very powerful framework of similar type. You can create a new SpriteKit game straight from XC
If you want to stay in house with just UIKit stuff, check out UIView block based animations. Here is the jist of it.
[UIView animateWithDuration:numberOfSecondsTakenToAnimate animations: ^{
// do you animation here. i.e.: move view frame, adjust color.
} completions: ^(BOOL complete) {
// when the animation is complete, code in this block is executed.
}];
I just remembered Core Graphics. It is used in tandem with UIViews to create simple 2d graphics and is very powerful and very fast. Here is the jist of that.
CGContextRef cntxt = UIGraphicsGetCurrentContext();
CGContextBeginPath(cntxt);
CGContextMoveToPoint(cntxt, <x>, <y>);
CGContextAddLineToPoint(cntxt, <x>, <y>);
CGContextClosePath(cntxt);
[[UIColor <color>] setFill];
[[UIColor <color>] setStroke];
CGContextDrawPath(cntxt, kCGPathFillStroke);
Note: things in < > are variables / values specified by you.
If you want to go all out, take the time to learn Open GL. Beware, I have heard that this is extremely hard to learn.
If you need performance, do not use UIView. It is not designed to be fast.
Instead, have a single UIView that takes up the whole screen, with a bunch of CALayer objects inside the one view. Move the layers around.
CALayer works by talking direct to the GPU, so it's very fast. Perhaps even faster than OpenGL. UIView is using CALayer internally so they both behave approximately the same. The only real difference is any change to the position of a CALayer will be animated by default. You can easily turn the animation off, although in a game you probably want animation.
Your other option, of course, is to use OpenGL. But that's a lot more work.
EDIT: here is some sample code for changing the position of a layer properly: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html#//apple_ref/doc/uid/TP40004514-CH3-SW8
so what I want is to do the same thing Clear (http://www.realmacsoftware.com/clear/) does.
If I pan up or down it makes a new rectangle appear with a 3D visual effect.
I did manage to do this by using a translation and rotation transform along with changing the m34 property.
The problem is that since I move 2 views (like Clear moving the rectangle that is showing on screen, and the rectangle appearing with the 3D visual effect), when the user stop touching the screen I use a
[UIView animateWithDuration:0.5
delay:0
options: UIViewAnimationCurveEaseOut
animations:^{
...
}
completion:nil];
to restore the views to a consistent state, either showing the new rectangle, or not (sliding it down), I can see the black background between the 2 views, meaning that the 2 rectangles are not being redrawn at same time.
How do I solve that?
It's difficult to answer your specific question without more details, but you can look at https://github.com/mpospese/MPFoldTransition for inspiration.
Mark Pospesel developed this wrapper to perform these kinds of folding transforms, and describes the process behind them in detail in his "Anatomy of a folding animation" blog post.
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 am trying out different looks of a little game I am writing to play with animation on iOS.
My goal is this to have a grid of tiles which based on gameplay changes the display of each tile to one of a set of images. I'd like each tile (up to 24x24) to flip around when its face changes. As the game progresses, more and more tiles need to be flipped at the same time. In the first implementation where I tried to flip them simultaneously the animation got very jerky.
I changed my approach to not flip them all at once, but just a few at a time, by scheduling the animation for each tile with a slightly increasing delay per tile, so that when say the 10th tile starts animating, the first one is already done. It takes little while longer for the whole process to finish, but also leads to a nice visual ripple-effect.
However, one problem remains: At the beginning of a game move, when the player picks a new color, it takes a few fractions of a second on the device, before the animation starts. This gets worse as the game progresses and more flips need to be scheduled per move, up to the point where the animation seems to hang and then completes almost instantly without any of the frames in between being actually discernible.
This is the code (in my UIView game grid subclass) that triggers the flipping of relevant tiles. (I removed an optimization that skips tiles, because it only matters in the early stages of the game).
float delay = 0.0f;
for (NSUInteger row=0; row<numRows; row++) {
for (NSUInteger col=0; col<numCols; col++) {
delay += 0.03f;
[self updateFlippingImageAtRow:row col:col delay:delay animated:YES];
}
}
The game grid view has an NSArray of tile subviews which are addressed using the row and col variables in the loop above.
updateFlippingImageAtRow:col:delay:animated is a method in my FlippingImageView (also a subclass of UIView) boils down to this (game logic omitted):
-(void)animateToShow:(UIImage*)image
duration:(NSTimeInterval)time
delay:(float)delay
completion:(void (^)(BOOL finished))completion
{
[UIView animateWithDuration:time
delay:delay
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft
forView:self
cache:YES];
self.frontImage = image;
}
completion:completion
];
}
Works fine, however, I conclude from the Instruments measuring which tells me that my time is spent in the animation block, that as the game goes on and the number of tiles to flip goes up, that the number of animations that get scheduled at the very beginning of the operation is a problem, and that the system then tries to catch up by dropping frames.
Any suggestions how to improve the performance of this? The exact timing of the animation is not really important.
You can think about doing this with CoreAnimation and CALayers instead of UIViews. It is incredebly powerful and optimized framework.
It's not an easy thing, you'll have to recode at least some of your classes (view hierarchy and hit tests are the first things that come to my mind), but it's worth a try and it's rather painless process, because CALayer is very similar to UIView.
Next step is OpenGL. It definitely can operate several hundreds of objects in realtime, but it requires much more work to do.
You might want to try using CoreAnimation instead. One way to do the flip animation would be:
Create a CABasicAnimation that animates the first half of the flip (rotation along the y axis).
In the delegate method animationDidStop:finished: you set the new image and then create a new animation that animates the second half.
You simply apply the animation to the layer property of your view.
Note that you can use setValue:forKey: to "annotate" an animation (remember what object the animation is about). When you add an animation to a layer it gets copied, not retained, so you can't identify it by simply comparing pointer values.