如何在后台渲染CALayer

8
我需要从我的应用程序中保存截图,因此我设置了以下代码,它可以正常工作:
- (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...
}

然而,当屏幕图像变得复杂(有很多视图)时,renderInContext在iPad 3上可能需要高达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崩溃。我尝试首先检查layer!=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.是否有其他渲染复杂图层而不锁定界面的方法?
更新:根据Rob Napier的建议,我重新编写了基于CALayerCopyable类别的代码,使用initWithLayer。简单地复制图层仍然给出了纯白色的输出,所以我添加了一个递归复制所有子层的方法。但是,我仍然得到纯白色的输出:
#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

2
你的后台线程不能触及UIWindow。你需要直接传递尺寸。 - Lily Ballard
好的,谢谢。为了我目前的测试目的,我现在正在硬编码窗口大小。根据我最终采用的解决方案,我会想出一种动态传递窗口大小的好方法。然而,这并不影响我目前的问题。 - arlomedia
你最后处理这个问题的方法还不错吗? - Ben Williams
不...我仍然在主线程上进行渲染,这导致我的界面显示不流畅。 - arlomedia
我认为你无法从后台线程访问renderInContext... - Alfie Hanssen
1个回答

2
为此,我会使用initWithLayer:而不是创建自己的复制方法。 initWithLayer:专门用于创建“图层的阴影副本,例如用于presentationLayer方法”。您可能还需要创建子图层的副本。我无法立即记住initWithLayer:是否为您完成了这项工作。但是,initWithLayer:是Core Animation的工作方式,因此它针对此类问题进行了优化。

我尝试了单独使用initWithLayer方法,而不是我的复制方法,但最终仍然得到了一个纯白色的图像。我还尝试将其替换为我的复制方法中的[CALayer layer],然后像以前一样复制所有属性,但仍然得到了一个纯白色的图像。文档说“不要使用此方法初始化具有现有层内容的新层”,所以我不确定是否值得追求。您认为我应该尝试遍历原始层的子层,并使用initWithLayer将它们复制到新层中吗? - arlomedia
我尝试复制所有的子层,但仍然得到了白色输出。我已经更新了问题,并附上了我尝试过的代码。 - arlomedia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接