drawViewHierarchyInRect:afterScreenUpdates:延迟其他动画

时间:2014-04-18 15:55:19

标签: ios objective-c cocoa-touch core-animation

在我的应用中,我使用drawViewHierarchyInRect:afterScreenUpdates:来获取我视图的模糊图像(使用Apple的UIImage类别UIImageEffects)。

我的代码如下所示:

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */

我在开发过程中注意到,在使用我的应用程序之后,我的许多动画都被延迟了,也就是说,与应用程序的全新启动相比,我的视图在显着(但不到一秒钟)暂停之后开始动画

经过一些调试后,我注意到仅将drawViewHierarchyInRect:afterScreenUpdates:屏幕更新设置为YES的行为导致此延迟。如果在使用会话期间从未发送此消息,则永远不会出现延迟。使用NO作为屏幕更新参数也会使延迟消失。

奇怪的是,这个模糊的代码完全不相关(据我所知)延迟动画。有问题的动画不使用 drawViewHierarchyInRect:afterScreenUpdates:,它们是CAKeyframeAnimation动画。仅发送此消息的行为(屏幕更新设置为YES)似乎已在我的应用中全局影响了动画。

发生了什么事?

(我已创建了说明效果的视频:withwithout动画延迟。请注意导航中出现的延迟#34; Check!"语音气泡栏。)

更新

我创建了一个示例项目来说明这个潜在的错误。 https://github.com/timarnold/AnimationBugExample

更新编号2

我收到Apple的回复,证明这是一个错误。请参阅答案below

5 个答案:

答案 0 :(得分:68)

我使用了一张Apple开发者支持服务单向Apple询问我的问题。

事实证明这是一个确认的错误(雷达编号17851775)。他们对正在发生的事情的假设如下:

  

方法drawViewHierarchyInRect:afterScreenUpdates:尽可能在GPU上执行其操作,并且大部分工作可能会在另一个进程的应用程序地址空间之外发生。将YES作为afterScreenUpdates:参数传递给drawViewHierarchyInRect:afterScreenUpdates:将导致Core Animation刷新任务和渲染任务中的所有缓冲区。正如您可能想象的那样,在这些情况下还会发生很多其他内部事情。工程理论认为它很可能是这种与你所看到的效果有关的机器中的一个错误。

     

相比之下,方法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 */

希望这对某人有帮助!

答案 1 :(得分:1)

您是否尝试在后台线程上运行代码? 下面是一个使用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
    });
});

答案 2 :(得分:1)

afterScreenUpdates参数设置为YES时,系统必须等到所有挂起的屏幕更新都发生之后才能呈现视图。

如果您同时启动动画,那么渲染和动画可能会一起发生,这会导致延迟。

为了防止这种情况,可能值得尝试稍后启动动画。很明显以后不会因为那会打败对象,但是一个小的dispatch_after间隔值得尝试。

答案 3 :(得分:1)

为什么你有这一行(来自你的示例应用程序):

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)。在大多数情况下,您并不真正需要持久性映像,而视图快照就是您真正想要的。使用视图快照而不是渲染图像具有以下好处:

  • 快2x-10x
  • 使用2x-3x更少的内存
  • 它将始终使用正确的色彩空间和缓冲格式(例如,在具有宽色屏幕的设备上)
  • 它将使用正确的比例和方向,因此您无需考虑如何定位图像以使其看起来很好。
  • 更好地使用辅助功能(例如使用智能反转颜色)
  • 查看快照还会正确捕获进程外和安全视图(而drawViewHierarchyInRect会将其呈现为黑色或白色)。

答案 4 :(得分:1)

我使用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延迟或中断。