Reputation: 3552
Can anyone explain why redrawing to an offscreen CGLayer would cause rendering to slow down over time? Let me show you a test I've created to illustrate the problem.
@implementation DrawView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
//setup frame rate monitoring
fps = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 100, 20)];
fps.textColor = [UIColor whiteColor];
fps.font = [UIFont boldSystemFontOfSize:15];
fps.text = @"0 fps";
[self addSubview:fps];
frames = 0;
lastRecord = [NSDate timeIntervalSinceReferenceDate];
//create a cglayer and draw the background graphic to it
CGContextRef context = UIGraphicsGetCurrentContext();
cacheLayer = CGLayerCreateWithContext(context, self.bounds.size, NULL);
CGImageRef background = [[UIImage imageNamed:@"background.jpg"] CGImage];
CGContextRef cacheContext = CGLayerGetContext(cacheLayer);
CGContextDrawImage(cacheContext, CGRectMake(0, 0, 768, 1024), background);
//initialize cgimage stamp
stamp = [[UIImage imageNamed:@"stamp.png"] CGImage];
stampTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60 target:self selector:@selector(stamp) userInfo:nil repeats:YES];
}
return self;
}
- (void) stamp {
//calculate fps
NSTimeInterval interval = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval diff = interval-lastRecord;
if (diff > 1.0) {
float rate = frames/diff;
frames = 0;
lastRecord = [NSDate timeIntervalSinceReferenceDate];
fps.text = [NSString stringWithFormat:@"%0.1f fps", rate];
}
//stamp the offscreen cglayer with the cgimage graphic
CGRect stampRect = CGRectMake(0, 0, 200, 200);
CGContextRef cacheContext = CGLayerGetContext(cacheLayer);
CGContextDrawImage(cacheContext, stampRect, stamp);
[self setNeedsDisplayInRect:stampRect];
}
- (void)drawRect:(CGRect)dirtyRect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawLayerInRect(context, self.bounds, cacheLayer);
frames++;
}
When I run this test in the ipad simulator or device it starts at 40 fps and drops at a constant rate over the course of 10 seconds until it's running at like 3 fps. Why is this happening? Shouldn't this run at a constant framerate? What kind of solution would allow me to 'stamp' an image over and over while maintaining a constant framerate?
Upvotes: 1
Views: 3827
Reputation: 299345
Your problem is that you created your layer with a NULL context. UIGraphicsGetCurrentContext()
is only valid during the draw loop. You call it outside the draw loop so it's NULL and so the layer can't cache anything. It still surprises me how badly this trashes performance over time. I suspect there might be a bug in CoreGraphics; you'd think this would just be slow; not "ever slowing." But still, it's not designed to work this way.
If you create a bitmap context and use that for your layer, you'll get your 60fps. You don't need to abandon CGLayer here.
All I did to get back to 60fps is to replace this:
CGContextRef context = UIGraphicsGetCurrentContext();
with this:
CGContextRef context = CreateBitmapContext(self.bounds.size);
Where CreateBitmapContext()
is a function that returns the same thing your createBitmapContext
sets up.
Upvotes: 5
Reputation: 3552
I found the solution, although seemingly counter-intuitive, it's faster to create a cached bitmap context and then copy an image out of it to draw to the current context.
- (void) createBitmapContext {
// Create the bitmap context
void * bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;
CGSize size = self.bounds.size;
// Declare the number of bytes per row. Each pixel in the bitmap in this
// example is represented by 4 bytes; 8 bits each of red, green, blue, and
// alpha.
bitmapBytesPerRow = (size.width * 4);
bitmapByteCount = (bitmapBytesPerRow * size.height);
// Allocate memory for image data. This is the destination in memory
// where any drawing to the bitmap context will be rendered.
bitmapData = malloc( bitmapByteCount );
if (bitmapData == NULL)
{
//return nil;
}
cacheContext = CGBitmapContextCreate (bitmapData, size.width, size.height,8,bitmapBytesPerRow,
CGColorSpaceCreateDeviceRGB(),kCGImageAlphaNoneSkipFirst);
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
//setup frame rate monitoring
fps = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 100, 20)];
fps.textColor = [UIColor whiteColor];
fps.font = [UIFont boldSystemFontOfSize:15];
fps.text = @"0 fps";
[self addSubview:fps];
frames = 0;
lastRecord = [NSDate timeIntervalSinceReferenceDate];
//create a bitmap context and draw the background graphic to it
[self createBitmapContext];
CGImageRef background = [[UIImage imageNamed:@"background.jpg"] CGImage];
CGContextDrawImage(cacheContext, CGRectMake(0, 0, 768, 1024), background);
//initialize cgimage stamp
stamp = [[UIImage imageNamed:@"stamp.png"] CGImage];
stampTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60 target:self selector:@selector(stamp) userInfo:nil repeats:YES];
}
return self;
}
- (void) stamp {
//calculate fps
NSTimeInterval interval = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval diff = interval-lastRecord;
if (diff > 1.0) {
float rate = frames/diff;
frames = 0;
lastRecord = [NSDate timeIntervalSinceReferenceDate];
fps.text = [NSString stringWithFormat:@"%0.1f fps", rate];
}
//stamp the offscreen bitmap context with the cgimage graphic
CGRect stampRect = CGRectMake(0, 0, 200, 200);
CGContextDrawImage(cacheContext, stampRect, stamp);
[self setNeedsDisplayInRect:stampRect];
}
- (void)drawRect:(CGRect)dirtyRect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGImageRef cacheImage = CGBitmapContextCreateImage(cacheContext);
CGContextDrawImage(context, self.bounds, cacheImage);
CGImageRelease(cacheImage);
frames++;
}
Upvotes: 0