谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)

这是 「谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色」 的下半部分。如未阅读上半部分文章,请点击 Dive Into iOS Render 专题 进行查看。

小试牛刀之 CADisplayLink

POP 库基于 CADisplayLink 注册 VSync 信号,如何注册的呢?VSync 信号到底是个啥呢?首先,我们打印一下 CADisplayLink 的触发堆栈:我们发现这是一个从 Port 转发过来处理的 source1,我们打印一下 Runloop,发现比没有 CADisplayLink 的 Runloop 多了一个 source1:

2 : {
    signalled = No, valid = Yes, order = -1, 
    context = {
        valid = Yes, port = 440b, source = 0x282dc8000, 
        callout = _ZL22display_timer_callbackP12__CFMachPortPvlS1_ (0x187592b2c), 
        context = 
    }
}

有这样一个 source1 被加入了 Runloop。CADisplaylink 也是基于这个 port完成了 VSync 信号的注册工作。产生 VSync 信号的进程,每 16.7ms 进行一次到这个 port 的 mach msg 发送工作,从而不断的激活本 App 的 Runloop ,触发一个 item,完成本 App 对 VSync 的感知。这里说点题外的,看到item,很自然的联想到这里是不是一个链表之类的数据结构。实际上,我们可以多次添加不同的 CADisplayLink instance 到 Runloop 中,就像这样:

CADispalyLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawSomething)];
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
CADispalyLink *link2 = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawSomething2)];
[link2 addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

但是如果这样写:

CADispalyLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawSomething)];
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
CADispalyLink *link2 = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawSomething)];
[link2 addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

绿色高亮部分的差异会造成什么呢?drawSomeThing 在一个 VSync message 里被触发了两次。原因是:[CADisplayLink displayLinkWithTarget:self selector:@selector(drawSomeThing)] 这个方法内部的 [CADisplayLink displayLinkWithDisplay:target:selector:] 调用新生成了一个 DisplayLinkItem (下图汇编代码中的 v8)。

谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)_第1张图片

调用 [CADisplayLink addToRunLoop:forMode:]时,内部:


谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)_第2张图片

这里的逻辑只检测了:

  • 如果这个 displayLink 已经被加入到其他 Runloop 中,直接抛出 exception;

  • 如果重复添加某个 mode,则直接 return(Runloop 仍然为 link 的 Runloop)所以新的 item 会被顺利加入,然后每次 VSync 触发两次。

下面我们总结一下基于CoreAnimation的Animation和基于POP的Animation的区别,可以用以下流程图解答:谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)_第3张图片总结一下:

  • CADisplayLink 创建了 port,使 App 能够感知 Vsync信号(不管经过多少了中间步骤)

  • 感知 VSync 信号使我们能够 16.7ms 更新一次 UI

  • 16.7ms 更新 UI 即可完成动画效果

但是显而易见,有以下不足:

  • 基于多次系统IPC的VSync信号感知,性能必然大打折扣。

  • 撰写更多的 code 去控制UI,处理边界情况。

  • 没有经过亿级设备检验过的 Animation Code 99%的可能有相对更多的bug。

总结可得到以下两个问题的答案:

如何进入 waiting?

调用 mach_msg,设置好 timeout 参数和 wait_port 就可以进入内核态等待 mach_msg 的来临

如何 wakeup?

  • 注册 source1 事件,被外部进程的通过 IPC 激活

  • 通过 timer 激活

  • 通过其他线程手动 wakeup 该线程的某个 source0。

  • 通过 gcd 的 Dispatch port(本质上也是 source1 事件,但是跟其他线程 IPC 稍有区别,属于自己跟自己 IPC)

说了这么一圈,我们了解了Runloop本质是在处理mach msg。那么我们什么时候做渲染呢?viewdidloadlayoutSubviews 等等中代码,到底是在 Runloop 哪里被触发的呢?

Runloop 如何触发渲染?

由于 Runloop 比较多的 mode ,每个 mode 都是在组合常见的几个 item ,关于 mode 和 item 不清楚的同学可以自行查阅相关资料,这里打印出一个 Runloop 的 debugDescription 来说明:

{wakeup port = 0x2303, stopped = false, ignoreWakeUps = false, current mode = kCFRunLoopDefaultMode,//首先,目前Runloop中存在两个mode,一个是UITracking,也就是滑动用的,一个是defaultcommon modes = {type = mutable set, count = 2,entries => 0 : {contents = "UITrackingRunLoopMode"} 2 : {contents = "kCFRunLoopDefaultMode"}}

第一个mode——UITrackingRunLoopMode:

0 : {    name = UITrackingRunLoopMode,   port set = 0x4803,   queue = 0x6000013d4e00,   source = 0x6000013d4f00 (not fired),   timer port = 0x4703,  sources0 : { 0 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x18aebe530)}} 1 : {signalled = No, valid = Yes, order = 0, context = {version = 0, info = 0x6000019c4a20, callout = FBSSerialQueueRunLoopSourceHandler (0x185729edc)}} 2 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x6000006c0000, callout = __eventQueueSourceCallback (0x1844477dc)}} 5 : {signalled = No, valid = Yes, order = -2, context = {version = 0, info = 0x600003381140, callout = __eventFetcherSourceCallback (0x184447850)}}  }, sources1 : { 0 : {signalled = No, valid = Yes, order = -1, context = {version = 1, info = 0x3603, callout = PurpleEventCallback (0x18aebe538)}} }, observers = (    ")}",    "}",    "}",    "}",    "}",    "\"\n)}"    ), timers = (null),},

比较有价值的,就是这个 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 函数了他是由 CoreAnimation 注册到 Runloop 的一个 observer 的回调。在这个 mode 下只有一个 source1,说明现在(仅代表测试程序环境)只能被来一个端口 msg 中断,也就是 PurpleEventCallback对应的端口。

第二个 mode -GSEventReceiveRunLoopMode:没有暴露的 mode,也是跟事件下发相关,不做探究

1 : {    name = GSEventReceiveRunLoopMode, port set = 0x4603, queue = 0x6000013d4f80, source = 0x6000013d5080 (not fired), timer port = 0x4403,  sources0 = {type = mutable set, count = 1,    entries => 0 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x18aebe530)}}    }, sources1 = {type = mutable set, count = 1,    entries => 0 : {signalled = No, valid = Yes, order = -1, context = {version = 1, info = 0x3603, callout = PurpleEventCallback (0x18aebe538)}}    }, observers = (null), timers = (null), currently 628607093 (779024795576) / soft deadline in: 7.68614304e+11 sec (@ -1) / hard deadline in: 7.68614304e+11 sec (@ -1)},

第三个 mode - kCFRunLoopDefaultMode

2 : {    name = kCFRunLoopDefaultMode, port set = 0x5403, queue = 0x6000013dc700, source = 0x6000013dc800 (not fired), timer port = 0x2c03,  sources0 = {type = mutable set, count = 4,    entries => 0 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x18aebe530)}} 1 : {signalled = No, valid = Yes, order = 0, context = {version = 0, info = 0x6000019c4a20, callout = FBSSerialQueueRunLoopSourceHandler (0x185729edc)}} 2 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x6000006c0000, callout = __eventQueueSourceCallback (0x1844477dc)}} 5 : {signalled = No, valid = Yes, order = -2, context = {version = 0, info = 0x600003381140, callout = __eventFetcherSourceCallback (0x184447850)}}    }, sources1 = {type = mutable set, count = 1,    entries => 0 : {signalled = No, valid = Yes, order = -1, context = {version = 1, info = 0x3603, callout = PurpleEventCallback (0x18aebe538)}}    }, observers = (    "{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _runLoopObserverCallout (0x183ee5508), context = (\n    \"<_UIWeakReference: 0x600003fd4250>\"\n)}",    "{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x183f32638), context = }",    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x1843dbf00), context = }",    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x187561d54), context = }",    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x1843dbf74), context = }",    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _runLoopObserverCallout (0x183ee5508), context = (\n    \"<_UIWeakReference: 0x600003fd4250>\"\n)}"    ), timers = {type = mutable-small, count = 1, values = ( 0 : {valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 628607095 (1.47744203 @ 779060301174), callout = (Delayed Perform) UIApplication _accessibilitySetUpQuickSpeak (0x1808078f4 / 0x183895594) (/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore), context = }    )}, currently 628607093 (779024795962) / soft deadline in: 1.47938383 sec (@ 779060301174) / hard deadline in: 1.47938379 sec (@ 779060301174)},

综上,一个普通运行起来的程序,Runloop 中大概有以下几个 source/observer:

  • AutoreleasePool 相关的 observer

  • 手势/Event相关的,由 UIKit 注册的 source 或者 observer

  • QuartzCore 的 CATransaction 注册了一个 observer

AutoReleasePool 是在 Runloop 启动时就生成了一个基本的 pool 来使用,而 transaction 则是有需要才建立的,这一步是怎么处理的呢?前面我们看到, CATransaction 注册了 before waiting 的事件,在 before waiting 的时候,调用CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*),这个 callback 里主要调用了CA::Transaction::commit()

接下来我们研究一下 CATransaction 机制,并讲解CA::Transaction::commit()做了什么。

CATransaction

CATransaction 是整个 CoreAnimation 的工作流程核心,它不是对 OpenGLES 的封装的,也没有做动画插值。它是一个载体,大部分业务需求的WorkFlow都是基于CATransaction完成的。基础的用法,begin,commit,timingfunction,complete handler,隐式事务,Runloop 中的大 Transaction 我们就不谈了,不清楚的同学可以自行查阅相关已有资料。CA::Transaction::Commit() 的被调用逻辑:CA::Transaction::Commit 主要由

  • Runloop结束时的callback调用

  • 调用[CATransaction Flush]时调用

  • 自己的递归调用

CA::Transaction::Commit 内部主要逻辑如下:谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)_第4张图片可以看到,我们主要做了这几件事情:

  • 对 layer tree 调用 layoutIfNeeded

    • 在这个过程中,实现从 CoreAnimation 到 UIKit 的上升,即最终调用到我们熟悉的 layoutSubviews 等方法

    • layoutIfNeeded 理论上只对 setNeedsLayout 过的 UIView(的 Layer)进行 layout 操作,frame、layout constraint等行为,均会 setNeedsLayout,手动设置也会生效。

    • 因此我们得出结论,通常情况下,大量的 layout`工作会在一次 Runloop 结束的时候全部开始,如果 layout engine 出现很多 broken,或者本身计算量过大(跨层较多,冗余约束较多等原因),则很有可能超时,导致一轮 Runloop 超过 16.7ms,

  • 通过mach与其他线程 (render server) 通信

  • commitIfNeeded

  • 处理 Animation

下面看 commit_If_Needed 中的具体逻辑:

谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)_第5张图片
  • 调用 [CALayer _copyRenderLayer:layerFlags:commitFlags]

    • 基于这个layer的各种属性,创建CA::Render::Object *

    • 有layer级别的cache

    • 根据情况使用ioSurface,data等多种数据源来生成bitmap(image)

  • 调用CA::Render::encode_set_object(),把前面创建的render obj set进去

  • 调用 [CALayer _didCommitLayer:]

综上,当 App 的 Runloop 不管是被什么事件唤醒以后,总能在进入内核态之前完成当前所有UI 的更新任务,并通过 mach_msg 完成 commit 到 render server 的工作。最常见由 iOS 开发者控制的唤醒 Runloop 的行为有:

  • 基于GCD的

    • 在主线程上 dispatch_after() 或 dispatch_async

    • 网络进程回调,dispatch_async到主线程

  • 基于GS Event的

    • 用户操作屏幕

    • 用户操作物理按键

至此我们已经解答了以下问题:

  • 一个 VC 的 layoutSubviews 是什么时候触发的

  • 为什么我在一个 UIButton 的点击事件里做了一个动画可以顺利的展示出来?动画是怎么生成的?

有任何你想了解的问题,欢迎在文章下方评论。

图形学时间

计算机动画扩展:计算机动画发展的很快,不再局限于某些物体的位置变换和伸缩变换,出现了很多算法实现各种效果,总结来看,计算机动画分为以下几种:

  • 2D动画:

    • 图像变形(Image Morphing)

    • 形状混合(Shape blending)

  • 3D动画:

    • 关键帧动画

    • 变形物体动画(自由体变形技术,FFD)

那么 CoreAnimation 在动画方面关注的显然是 2D 动画,我们也来讲解一下关于图像变形和形状混合。图像变形图像变形有两种基本的方法:

  • 基于单张图像进行形变

    • 如:宽高拉伸

  • 基于多张图像进行插值

    • 此方法可以引入用户交互,指定某些特征进行动画,更加可控

      注意噢,这里的特征可不是 iOS 动画中的 property ,是指对某个图像人为框定几个特征点,进行拉伸形变。

谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)_第6张图片

形状混合

二维图形动画,都可以简化为为多边形处理。二维的形状混合,即在两个关键帧的多边形之间插入新的多边形。插入新的多边形,需要解决的问题就是前后两个关键帧之间,顶点的对应关系和顶点之间插值路径的问题。这两个问题在 siggraph 1992 和 1993 的论文上得到了解。

谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色(下)_第7张图片

推荐阅读:

  • 深入理解 RunLoop[1]

  • Apple CFRunloop 源码[2]

  • 界面渲染的整体流程[3]

  • iOS 事件处理机制与图像渲染过程

参考资料

[1]

深入理解 RunLoop: https://blog.ibireme.com/2015/05/18/Runloop/

[2]

Apple CFRunloop 源码: https://opensource.apple.com/source/CF/CF-635/CFRunLoop.c.auto.html

[3]

界面渲染的整体流程: http://blog.handy.wang/blog/2015/10/03/uiviewyu-calayerxie-zuo-xuan-ran-jie-mian-de-guo-cheng/

你可能感兴趣的:(ebook,xhtml,animation,epoll,sms)