-[CALayer setContents:] slower in iOS 4.2?
-[CALayer setContents:] slower in iOS 4.2?
- Subject: -[CALayer setContents:] slower in iOS 4.2?
- From: Julian Wood <email@hidden>
- Date: Mon, 31 Jan 2011 17:12:44 -0700
We have a game engine of sorts, developed under 3.2. It did use NSTimer to fire off the game loop, but now uses CADisplayLink, at 15fps. The game loop basically sends messages to as many as 15 sprites, telling them to update their position and their frame. A sprite is in essence a CALayer, and we use -[CALayer setContents:] and -[CALayer setPosition:] to update those properties. All sprites are updated in a CATransaction, and everything is performed on the main thread. The image used for contents is obtained from a UIImage spriteAtlas. The atlas can be quite large: 1000x1600. It contains all the animation frames for a sprite. We grab the appropriate image portion from the spriteAtlas using CGImageCreateWithImageInRect(CGImageRef image, CGRect rect). In general, we haven't been hitting the memory limit on the device with this approach, and when it does, it recovers nicely.
Now this all worked perfectly in 3.2, but in iOS > 4, we have performance problems, on the device only. Basically the sprites are very laggy and the animation is choppy, until resources have fully loaded (it can take from 1s to 30s to fully settle down). The culprit would appear to be -[CALayer setContents:], which can take on the order of 0.3s-0.8s, the first time the image is used. The problem is then exacerbated as we re-enter the gameloop, with resources not in place.
So to solve this, we have tried several approaches (apart from waiting for a possibly never-coming performance fix to -[CALayer setContents:]):
1. Preload image assets. One would think this would be easy, but nothing seems to preload a CGImageRef until it is actually painted on the screen. Or at the very least, there is no time penalty until you try to paint an image on the screen. Nothing we have tried has improved this situation. The docs for -[UIImage CGImage] state: "If the image data has been purged because of memory constraints, invoking this method forces that data to be loaded back into memory. Reloading the image data may incur a performance penalty." I had assumed this to mean that the image will be fully loaded into memory, but our results would suggest that it doesn't actually get loaded until it is used (eg in setContents:). We're using +[UIImage imageNamed:] to load the image, and so far the cache inherent to that method has been good to us.
2. Invoke -[CALayer setContents:] on a background thread. This does not change the situation at all. I suspect that -[CALayer setContents:] simply grabs the main thread and does its work there, regardless of the thread it is invoked from, though I can't find any docs that say this explicitly.
[spriteLayer performSelectorInBackground:@selector(setContents:) withObject:(id)frameImage];
vs
[spriteLayer setContents:(id)frameImage];
3. Block until the image has been loaded. Basically, we skip any updates on the sprite until a flag appears, indicating the image has been loaded. Since the image loading is asynchronous, AFAICT, we just set the flag before and after -[CALayer setContents:].
Again, this does not improve performance, which says to me that setContents on other instances of CALayer on the screen are being affected, even though their imageContents are loaded and cached. I can only imagine this is because everything is being invoked on the same thread, on the same runloop.
One final note - my CADisplayLink timer does not seem very reliable - ie, I get between 2 and 200 fps, when I should only ever get 15fps, assuming it is tied directly to hardware. Perhaps because it is also on the main runloop, it is suffering as well? While these anomalies are rare, they occur exactly when trying to load a new sprite atlas image. It seems like this alone could be causing the problem. I'm looking now at running it on another runloop or thread. NSTimer doesn't change anything appreciably, either.
timer_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(gameloop)];
[timer_ setFrameInterval:4]; // 15fps
[timer_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
I measure fps like this in the gameloop:
double startTime = CACurrentMediaTime();
double fps = 1/(startTime - lastTime_);
lastTime_ = startTime;
ILog (@"fps: %f", fps);
So I feel like I'm flailing around a bit here now. I know what the problem is, but have been unable to find an approach that will improve the situation. It may be that we have to split up our sprite atlases, but I wanted to ask for advice from the list first.
Any thoughts or suggestions would be greatly appreciated! Thanks,
Julian
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden