在我的应用程序中,我使用drawViewHierarchyInRect:afterScreenUpdates:
来获得视图的模糊图像(使用苹果的UIImage
类别UIImageEffects)。
我的代码如下:
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */
我在开发过程中注意到,我的许多动画在使用我的应用程序一段时间后都被延迟了,也就是说,与新推出的应用程序相比,我的视图在明显的(但不到一秒钟)暂停后才开始动画。
经过一些调试,我注意到仅仅使用屏幕更新设置为YES
的drawViewHierarchyInRect:afterScreenUpdates:
就造成了这种延迟。如果在使用会话期间从未发送过此消息,则不会出现延迟。使用NO
作为屏幕更新参数也使延迟消失。
奇怪的是,这个模糊代码与延迟的动画完全无关。中的动画不使用drawViewHierarchyInRect:afterScreenUpdates:
,它们是CAKeyframeAnimation
动画。仅仅发送这条消息(屏幕更新设置为YES
)的行为似乎就在全球范围内影响了我的应用程序中的动画。
怎么回事?
(我制作了演示效果的视频:有动画延迟和没有动画延迟。注意导航栏中"Check!"语音气泡出现的延迟。)
更新
我创建了一个示例项目来说明这个潜在的bug。https://github.com/timarnold/AnimationBugExample
第2号更新
我收到了苹果公司的回复,确认这是一个错误。请参阅下面的答案。
我用我的一张苹果开发者支持票向苹果询问了我的问题。
事实证明,这是一个已确认的错误(雷达编号17851775)。他们对正在发生的事情的假设如下:
drawViewHierarchyInRect:afterScreenUpdates:方法尽可能多地在GPU上执行操作,并且在另一个过程中,大部分工作可能会发生在应用程序的地址空间之外。将YES作为afterScreenUpdates:parameter传递给drawViewHierarchyInRect:afterScrenUpdates:将导致核心动画刷新任务和渲染任务中的所有缓冲区。正如你所想象的,在这些情况下,还有很多其他的内部因素。工程理论认为,这很可能是这台机器中的一个缺陷,与你所看到的效果有关。
相比之下,方法renderInContext:在应用程序的地址空间内执行操作,并且不使用基于GPU的进程来执行工作。在大多数情况下,这是一个不同的代码路径,如果它对您有效,那么这是一种合适的解决方法。该路由不如不使用基于GPU的任务那样高效。此外,它对于屏幕捕捉来说并不准确,因为它可能会排除由GPU任务管理的模糊和其他核心动画功能。
他们还提供了一个变通办法。他们建议不要:
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */
我应该做这个
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */
希望这对某人有帮助!
我使用swift尝试了所有最新的快照方法。其他方法在后台对我不起作用。但以这种方式拍摄快照对我很有效。
创建一个带有参数的扩展视图图层和视图边界。
extension UIView {
func asImage(viewLayer: CALayer, viewBounds: CGRect) -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: viewBounds)
return renderer.image { rendererContext in
viewLayer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(viewBounds.size)
viewLayer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}
用法
DispatchQueue.main.async {
let layer = self.selectedView.layer
let bounds = self.selectedView.bounds
DispatchQueue.global(qos: .background).async {
let image = self.selectedView.asImage(viewLayer: layer, viewBounds: bounds)
}
}
我们需要在主线程中计算层和边界,然后其他操作将在后台线程中工作。它将提供流畅的用户体验,而不会在UI中出现任何延迟或中断。
为什么有这行(来自示例应用程序):
animation.beginTime = CACurrentMediaTime();
只要去掉它,一切都会如你所愿。
通过将动画时间显式设置为CACurrentMediaTime()
,可以忽略层树中可能存在的时间变换。要么根本不设置(默认情况下动画将启动now
),要么使用时间转换方法:
animation.beginTime = [view.layer convertTime:CACurrentMediaTime() fromLayer:nil];
当您调用afterScreenUpdates:YES
时,UIKit会将时间转换添加到层树中,以防止正在进行的动画中的跳跃,否则这将由中间的CoreAnimation提交引起。如果要在特定时间(而不是now
)开始动画,请使用上面提到的时间转换方法。
同时,强烈建议使用-[UIView snapshotViewAfterScreenUpdates:]
和朋友,而不是-[UIView drawViewHierarchyInRect:]
家人(最好指定NO
作为afterScreenUpdates
部分)。在大多数情况下,您并不真正需要持久的图像,视图快照才是您真正想要的。使用视图快照而不是渲染图像有以下好处:
- 速度快2x10倍
- 使用的内存减少2到3倍
- 它将始终使用正确的颜色空间和缓冲区格式(例如,在具有宽彩色屏幕的设备上)
- 它将使用正确的比例和方向,所以你不需要考虑如何定位你的图像,使其看起来好看
- 它可以更好地使用辅助功能(例如,使用智能反转颜色)
- 视图快照还将正确捕获进程外和安全的视图(而
drawViewHierarchyInRect
将把它们渲染为黑色或白色)
当afterScreenUpdates
参数设置为YES时,系统必须等到所有挂起的屏幕更新都发生后才能渲染视图。
如果你同时开始动画,那么渲染和动画可能试图同时进行,这会导致延迟。
为了防止这种情况发生,可能值得稍后尝试启动动画。显然,不会太晚,因为这会使对象失败,但一个小的dispatch_after间隔值得尝试。
您是否尝试过在后台线程上运行代码?以下是使用gcd:的示例
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//background thread
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_sync(dispatch_get_main_queue(), ^(void) {
//update ui on main thread
});
});