drawViewHierarchyInRect:afterScreenUpdates:延迟其他动画



在我的应用程序中,我使用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 */

我在开发过程中注意到,我的许多动画在使用我的应用程序一段时间后都被延迟了,也就是说,与新推出的应用程序相比,我的视图在明显的(但不到一秒钟)暂停后才开始动画。

经过一些调试,我注意到仅仅使用屏幕更新设置为YESdrawViewHierarchyInRect: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 convert​Time:CACurrentMediaTime() from​Layer: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
    });
});

最新更新