如何在后台渲染CALayer

时间:2012-08-29 20:32:29

标签: ios multithreading core-graphics calayer copy-constructor

我需要从我的应用中保存屏幕截图,所以我设置了这样的代码,这有效:

- (void)renderScreen {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];

    CGSize outputSize = keyWindow.bounds.size;
    UIGraphicsBeginImageContext(outputSize);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSaveGState(context);
    CALayer *layer = [keyWindow layer];
    [layer renderInContext:context];
    CGContextRestoreGState(context);

    UIImage *screenImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // now save the screen image, etc...
}

但是,当屏幕图像变得复杂(许多视图)时,在iPad 3上,renderInContext最多可能需要0.8秒,并且用户界面在此期间会锁定,这会干扰其他一些功能。所以我将渲染移动到后台线程,如下所示:

- (void)renderScreen {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    CALayer *layer = [keyWindow layer];
    [self performSelectorInBackground:@selector(renderLayer:) withObject:layer];
}

- (void)renderLayer:(CALayer *)layer {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];

    CGSize outputSize = keyWindow.bounds.size;
    UIGraphicsBeginImageContext(outputSize);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSaveGState(context);
    [layer renderInContext:context];
    CGContextRestoreGState(context);

    UIImage *screenImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // now save the screen image, etc...
}

允许接口再次平滑运行,但偶尔会导致renderInContext行上的EXC_BAD_ACCESS崩溃。我首先尝试检查图层!= nil和[layer respondsToSelector:@selector(renderInContext :)],这样我就可以避免崩溃,但这两个条件总是返回true。

然后我阅读this SO comment,声明在后台操作运行之前图层可能会发生变异,并建议将图层的副本发送到后台操作。 This SO answerthis one让我开始了,最后我用这个类别为CALayer添加了一个复制方法:

#import "QuartzCore/CALayer.h"

@interface CALayer (CALayerCopyable)
- (id)copy;
@end

@implementation CALayer (CALayerCopyable)

- (id)copy {
    CALayer *newLayer = [CALayer layer];
    newLayer.actions = [self.actions copy];
    newLayer.anchorPoint = self.anchorPoint;
    newLayer.anchorPointZ = self.anchorPointZ;
    newLayer.backgroundColor = self.backgroundColor;
    //newLayer.backgroundFilters = [self.backgroundFilters copy]; // iOS 5+
    newLayer.borderColor = self.borderColor;
    newLayer.borderWidth = self.borderWidth;
    newLayer.bounds = self.bounds;
    //newLayer.compositingFilter = self.compositingFilter; // iOS 5+
    newLayer.contents = [self.contents copy];
    newLayer.contentsCenter = self.contentsCenter;
    newLayer.contentsGravity = [self.contentsGravity copy];
    newLayer.contentsRect = self.contentsRect;
    //newLayer.contentsScale = self.contentsScale; // iOS 4+
    newLayer.cornerRadius = self.cornerRadius;
    newLayer.delegate = self.delegate;
    newLayer.doubleSided = self.doubleSided;
    newLayer.edgeAntialiasingMask = self.edgeAntialiasingMask;
    //newLayer.filters = [self.filters copy]; // iOS 5+
    newLayer.frame = self.frame;
    newLayer.geometryFlipped = self.geometryFlipped;
    newLayer.hidden = self.hidden;
    newLayer.magnificationFilter = [self.magnificationFilter copy];
    newLayer.mask = [self.mask copy]; // property is another CALayer
    newLayer.masksToBounds = self.masksToBounds;
    newLayer.minificationFilter = [self.minificationFilter copy];
    newLayer.minificationFilterBias = self.minificationFilterBias;
    newLayer.name = [self.name copy];
    newLayer.needsDisplayOnBoundsChange = self.needsDisplayOnBoundsChange;
    newLayer.opacity = self.opacity;
    newLayer.opaque = self.opaque;
    newLayer.position = self.position;
    newLayer.rasterizationScale = self.rasterizationScale;
    newLayer.shadowColor = self.shadowColor;
    newLayer.shadowOffset = self.shadowOffset;
    newLayer.shadowOpacity = self.shadowOpacity;
    newLayer.shadowPath = self.shadowPath;
    newLayer.shadowRadius = self.shadowRadius;
    newLayer.shouldRasterize = self.shouldRasterize;
    newLayer.style = [self.style copy];
    //newLayer.sublayers = [self.sublayers copy]; // this line makes the screen go blank
    newLayer.sublayerTransform = self.sublayerTransform;
    //newLayer.superlayer = self.superlayer; // read-only
    newLayer.transform = self.transform;
    //newLayer.visibleRect = self.visibleRect; // read-only
    newLayer.zPosition = self.zPosition;
    return newLayer;
}

@end

然后我更新了renderScreen以将图层的副本发送到renderLayer:

- (void)renderScreen {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    CALayer *layer = [keyWindow layer];
    CALayer *layerCopy = [layer copy];
    [self performSelectorInBackground:@selector(renderLayer:) withObject:layerCopy];
}

当我运行此代码时,所有屏幕图像都是纯白色。显然我的复制方法不正确。那么有人可以帮助我解决以下任何可能的解决方案吗?

  1. 如何为真正有效的CALayer编写复制方法?
  2. 如何检查传递给后台进程的图层是否是renderInContext的有效目标?
  3. 在不锁定界面的情况下渲染复杂图层的任何其他方法吗?
  4. 更新:我根据Rob Napier建议使用initWithLayer重写了我的CALayerCopyable类别。简单地复制图层仍然给我一个纯白色输出,所以我添加了一个方法来递归复制所有子图层。然而,我仍然得到纯白色输出:

    #import "QuartzCore/CALayer.h"
    
    @interface CALayer (CALayerCopyable)
    - (id)copy;
    - (NSArray *)copySublayers:(NSArray *)sublayers;
    @end
    
    @implementation CALayer (CALayerCopyable)
    
    - (id)copy {
        CALayer *newLayer = [[CALayer alloc] initWithLayer:self];
        newLayer.sublayers = [self copySublayers:self.sublayers];
        return newLayer;
    }
    
    - (NSArray *)copySublayers:(NSArray *)sublayers {
        NSMutableArray *newSublayers = [NSMutableArray arrayWithCapacity:[sublayers count]];
        for (CALayer *sublayer in sublayers) {
            [newSublayers addObject:[sublayer copy]];
        }
        return [NSArray arrayWithArray:newSublayers];
    }
    
    @end
    

1 个答案:

答案 0 :(得分:2)

为此,我使用initWithLayer:而不是创建自己的复制方法。 initWithLayer:明确用于创建“图层的卷影副本,例如,用于presentationLayer方法。”

您可能还需要创建子图层的副本。我不记得initWithLayer:是否为你做了这件事。但是initWithLayer:是Core Animation的工作方式,所以它针对这样的问题进行了优化。