Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting a bitmap from CALayers, any alternative to renderInContext

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!

like image 291
user915774 Avatar asked Aug 27 '11 18:08

user915774


2 Answers

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
like image 153
C4 - Travis Avatar answered Nov 10 '22 08:11

C4 - Travis


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.

like image 23
Rob Napier Avatar answered Nov 10 '22 08:11

Rob Napier