【问题标题】:drawViewHierarchyInRect:afterScreenUpdates: delays other animationsdrawViewHierarchyInRect:afterScreenUpdates: 延迟其他动画
【发布时间】:2014-06-03 04:07:18
【问题描述】:

在我的应用中,我使用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 动画延迟。请注意导航栏中“检查!”对话气泡出现的延迟。)

更新

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

第 2 条更新

我收到了 Apple 的回复,确认这是一个错误。见答案below

【问题讨论】:

  • 我遇到了类似的问题,模糊会导致 UI 更新严重滞后。我们通过确保在主线程上完成动画来解决它。这有帮助吗? - 有用的视频顺便说一句。但不确定到底要寻找什么。
  • 看看“检查!”需要多长时间游戏视图控制器出现后出现的对话气泡
  • 如果你使用 afterScreenUpdates:NO,它不会滞后。

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


【解决方案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 中出现任何延迟或中断。

【讨论】:

  • 这对于减少延迟非常有用,但显然它不会捕获图像视图。
  • 实际上问题不是图像视图,而是动画图像视图。
【解决方案2】:

为什么会有这行(来自您的示例应用):

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

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

【讨论】:

    【解决方案3】:

    我使用我的一张 Apple 开发人员支持票向 Apple 询问我的问题。

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

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

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

    希望这对某人有帮助!

    【讨论】:

    • 我实际上注意到了一个不同的问题,它是由相同的调用/解决方案引起和解决的。在我的情况下,绘图调用似乎会在 iPhone 6 和 6 Plus 上以不正确的比例暂时将视图层次绘制到屏幕上。我猜它会在按比例缩小之前以原始分辨率绘制屏幕,​​但它会在屏幕上显示。上面的代码再次解决了这个问题。
    • @TheCodingArt 我也看过这个
    • 如果你将UIScreen.scale而不是0.0作为最后一个参数传递给UIGraphicsBeginImageContextWithOptions,你会看到同样的缺陷吗?
    • @verec 我不知道,这是个好问题。我已经很长时间没有看到这个错误了,所以我不确定它是否容易重现(或者它是否仍然是一个问题)。你还发现这个问题吗?
    • 如果您没有对视图层应用 3D 变换,这种解决方法非常好。不幸的是,renderInContext 不支持 3D 变换。
    【解决方案4】:

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

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

    可能值得尝试稍后启动动画以防止这种情况发生。显然不会太晚,因为那样会破坏对象,但是一个小的 dispatch_after 间隔值得尝试。

    【讨论】:

    • 这是有道理的,只是动画发生的时间与屏幕截图完全不同
    【解决方案5】:

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

    【讨论】:

    • 我不确定我是否看到这可能是相关的或可能是一个解决方案
    • 我认为不可能在后台运行它。
    • 我认为你应该在主线程上调用 drawViewHierarchyInRect,因为它与 UIKit 相关
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-05-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多