Getting a bitmap from CALayers, any alternative to renderInContext
Asked Answered
I

2

7

I have many CALayers which are created on the fly while my app is running, and I need to be able to produce a single bitmap of these, which will later be masked.

When I need to create the mask, the CALayers are already drawn to the background (also using shouldRasterize = YES) , and using renderInContext I am able to get a bitmap. However, as the amount of CAlayers increases, the pause caused by renderInContext gets longer and longer. Is there an alternative I can use to renderInContext, or an alternative way I can use it to stop my app temporarily freezing?

The ideal would be to access the already drawn pixel data directly from memory/buffer/cache without using OpenGL, but I am unsure if this is possible with CoreAnimation.

Thanks, any additional information at all would be very useful!

Insinuation answered 27/8, 2011 at 18:40 Comment(0)
P
8

Rob is right about renderInContext: being the right method to use here. Render in context does actually render the layer's pixel data into a context. Here's a sample application that will draw 10,000 layers on a background thread...

The application does the following:

  1. Create's a UIView
  2. Adds 10,000 layers to that view's layer
  3. Rendering begins when you touch the screen (i.e. it's an iOS sample app)
  4. Creates a background thread
  5. Renders the UIView's layer into a context (which in turn renders its sublayers)
  6. Creates a UIImage with the content of the render context
  7. Adds the new image to the screen in a UIImageView
  8. It does all this while running a timer on the main thread, showing that the background thread actually doesn't block the main thread

Here is the code...

First, create a subview with a lot of layers:

@implementation C4WorkSpace {
    UIView *v;
    dispatch_queue_t backgroundRenderQueue;
    CFTimeInterval beginTime;
    NSTimer *timer;
    NSInteger timerCallCount;
}

-(void)setup {
    //create a view to hold a bunch of CALayers
    v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 400, 400)];
    v.center = CGPointMake(384,512);

    //create a buch of CALayers and add them to a view
    for(int i = 0; i < 10000; i++) {
        CALayer *l = [[CALayer alloc] init];
        l.frame = CGRectMake([self random:390],[self random:390],10,10);
        l.backgroundColor = [UIColor blueColor].CGColor;
        l.borderColor = [UIColor orangeColor].CGColor;
        l.borderWidth = 2.0f;
        [v.layer addSublayer:l];
    }
    //add the view to the application's main view
    [self.view addSubview:v];
}

-(NSInteger)random:(NSInteger)value {
    srandomdev();
    return ((NSInteger)random())%value;
}

Second, create a method that will start a timer and then triggers the render...

-(void)touchesBegan {
    timer = [NSTimer scheduledTimerWithTimeInterval:0.03f target:self selector:@selector(printTime) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [self render];
}

-(void)printTime {
    NSLog(@"%d (main thread running)",++timerCallCount);
}

Third, create a render loop with a callback method that puts an image on the screen after rendering is completed.

-(void)render {
    NSLog(@"render was called");
    //create the queue
    backgroundRenderQueue = dispatch_queue_create("backgroundRenderQueue",DISPATCH_QUEUE_CONCURRENT);    
    //create a async call on the background queue
    dispatch_async(backgroundRenderQueue, ^{
        //create a cgcontext
        NSUInteger width = (NSUInteger)v.frame.size.width;
        NSUInteger height = (NSUInteger)v.frame.size.height;
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        NSUInteger bytesPerPixel = 4;
        NSUInteger bytesPerRow = bytesPerPixel * width;
        unsigned char *rawData = malloc(height * bytesPerRow);
        NSUInteger bitsPerComponent = 8;
        CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

        //render the layer and its subviews
        [v.layer renderInContext:context];

        //create a callback async on the main queue when rendering is complete
        dispatch_async(dispatch_get_main_queue(), ^{
            //create an image from the context
            UIImage *m = [UIImage imageWithCGImage:CGBitmapContextCreateImage(context)];
            UIImageView *uiiv = [[UIImageView alloc] initWithImage:m];
            //add the image view to the main view
            [self.view addSubview:uiiv];
            CGColorSpaceRelease(colorSpace);
            CGContextRelease(context);
            NSLog(@"rendering complete");
            [timer invalidate];
        });
    });
}

NOTE: if your layers aren't all in the same sublayer, you can easily call a for loop that translates the context to and from the origin of each CALayer and draw each layer individually into the context itself

When I run this I get the following output:

2013-03-18 07:14:28.617 C4iOS[21086:907] render was called
2013-03-18 07:14:28.648 C4iOS[21086:907] 1 (main thread running)
2013-03-18 07:14:28.680 C4iOS[21086:907] 2 (main thread running)
2013-03-18 07:14:28.709 C4iOS[21086:907] 3 (main thread running)
2013-03-18 07:14:28.737 C4iOS[21086:907] 4 (main thread running)
2013-03-18 07:14:28.767 C4iOS[21086:907] 5 (main thread running)
2013-03-18 07:14:28.798 C4iOS[21086:907] 6 (main thread running)
2013-03-18 07:14:28.828 C4iOS[21086:907] 7 (main thread running)
2013-03-18 07:14:28.859 C4iOS[21086:907] 8 (main thread running)
2013-03-18 07:14:28.887 C4iOS[21086:907] 9 (main thread running)
2013-03-18 07:14:28.917 C4iOS[21086:907] 10 (main thread running)
2013-03-18 07:14:28.948 C4iOS[21086:907] 11 (main thread running)
2013-03-18 07:14:28.978 C4iOS[21086:907] 12 (main thread running)
2013-03-18 07:14:29.010 C4iOS[21086:907] 13 (main thread running)
2013-03-18 07:14:29.037 C4iOS[21086:907] 14 (main thread running)
2013-03-18 07:14:29.069 C4iOS[21086:907] 15 (main thread running)
2013-03-18 07:14:29.097 C4iOS[21086:907] 16 (main thread running)
2013-03-18 07:14:29.130 C4iOS[21086:907] 17 (main thread running)
2013-03-18 07:14:29.159 C4iOS[21086:907] 18 (main thread running)
2013-03-18 07:14:29.189 C4iOS[21086:907] 19 (main thread running)
2013-03-18 07:14:29.217 C4iOS[21086:907] 20 (main thread running)
2013-03-18 07:14:29.248 C4iOS[21086:907] 21 (main thread running)
2013-03-18 07:14:29.280 C4iOS[21086:907] 22 (main thread running)
2013-03-18 07:14:29.309 C4iOS[21086:907] 23 (main thread running)
2013-03-18 07:14:29.337 C4iOS[21086:907] 24 (main thread running)
2013-03-18 07:14:29.369 C4iOS[21086:907] 25 (main thread running)
2013-03-18 07:14:29.397 C4iOS[21086:907] 26 (main thread running)
2013-03-18 07:14:29.405 C4iOS[21086:907] rendering complete
Photoelasticity answered 18/3, 2013 at 13:34 Comment(0)
S
3

renderInContext: is the best tool here, but you don't need to run it on the main thread. Just move this to a background thread and it'll stop freezing your app.

Sauropod answered 27/8, 2011 at 20:20 Comment(3)
This may cause memory leaks if you put renderInContext in the background. Some of the quartz2d ref's are not released even though it should be released auotmatically. Instruments will not show where the leaks are but you will eventually get a memory warning, then crash.Romanfleuve
renderInContext is also very slow, I was looking for an alternative to this but so far could not find one.Romanfleuve
drawing is supposed to be thread safe. You should be able to do this on a background thread with a CGImageContext. You need to CGContextRelease any context you create. If you think its broken file a radar with Apple. radar.apple.comDuwalt

© 2022 - 2024 — McMap. All rights reserved.