精辟总结
ASDK 认为,阻塞主线程的任务,主要分为(布局计算\渲染\对象操作)三大类。前两个可以异步操作,但 UIKit 和 CA 相关操作必需在主线程进行。传统的CALayer(属性改变/动画产生)是通过delegate来通知UIView的, ASDK创建了ASDisplayNode. ASDisplayNode持有UIView和CALayer对象, 当ASDisplayNode的属性(比如frame/transform)改变后,它并不会立刻同步到其持有的 view 或 layer 去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过(某个机制)一次性设置到内部的 view 或 layer。
传统的CA
当一个触摸事件到来时,RunLoop 被唤醒,切换到UITrackingMode, App 中的代码会执行一些调整视图的操作;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去。因为Observer监听了 BeforeWaiting 和 Exit 事件,RunLoop 即将进入休眠(或者退出)时, Observer 会被回调通知, 把所有的中间状态合并提交到 GPU 去显示.
ASDK模拟
当有些操作要在主线程时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器CALayer。当 RunLoop 进入休眠前、CA 处理完事件后,Observer会被回调通知, ASDK 就会执行该 loop 内提交的所有任务。
ASDK 对于绘制过程的优化有三部分:分别是栅格化子视图、绘制图像以及绘制文字。
它拦截了视图加入层级时发出的通知 - willMoveToWindow: 方法,然后手动调用 - setNeedsDisplay,强制所有的 CALayer 执行 - display 更新内容;
然后将上面的操作全部抛入了后台的并发线程中,并在 Runloop 中注册回调,在每次 Runloop 结束时,对已经完成的事务进行 - commit,以图片的形式直接传回对应的 layer.content 中,完成对内容的更新。
在 ASDK 中的渲染围绕 ASDisplayNode 进行,其过程总共有四条主线:
- 初始化 ASDisplayNode 对应的 UIView 或者 CALayer;
- 在当前视图进入视图层级时执行 setNeedsDisplay;
- display 方法执行时,向后台线程派发绘制事务;
- 注册成为 RunLoop 观察者,在每个 RunLoop 结束时回调。
https://www.jianshu.com/p/276df9732a70
系统调用CALayer的display, 进行更新内容
display方法会调用layer的delegate的displayLayer方法, 进行更新内容
通常UIView就是CALayer的delegate
核心在于ASDisplayNode
ASDisplaynode可以实现异步执行:
- 层次计算
- 渲染布局
绘制的入口是ASDisplayLayer的displayAsyncLayer: asynchronously:
在这个方法里做了三件事情
- 递归获取displayBlock(绘图操作, 可以异步执行)
- 渲染完成后的completion block(必须在主线程中进行)
- 调度displayBlock
displayBlock 的构建
displayBlock 的创建一般分为三种不同的方式(不重要):
- 将当前视图的子视图压缩成一层绘制在当前页面上
- 使用 - displayWithParameters:isCancelled: 返回一个 UIImage,对图像节点 ASImageNode 进行绘制
- 使用 - drawRect:withParameters:isCancelled:isRasterizing: 在 CG 上下文中绘制文字节点 ASTextNode
这三种方式都通过 ASDK 来优化视图的渲染速度,这些操作最后都会扔到后台的并发线程中进行处理。
在何处注册了runloop
向mainRunloop注册Observer的地方有两个。
对应的处理是完成mainTransactionGroup的提交,上面我们在分析CALayer中获取_ASAsyncTransaction时,提到了CALayer会被添加至mainTransactionGroup中。这时候mainTransactionGroup在提交时会将自己管理的CALayer当前的_ASAsyncTransaction对象进行提交,从而完成layer的绘制工作。
向mainRunloop注册Observer的另一个处是在ASRunLoopQueue类初始化中。这里observer注册被调用的时机是在runloop开始sleep的时候。并创建了一个runLoopSource用来避免runLoop没有任务的时候会自动退出。同时在新任务到来的时候唤醒runLoop。
回调processQueue函数主要从内置的_internalQueue中取出_batchSize个任务进行处理。处理任务调用的block是_queueConsumer,也就是上面初始化函数传过来的handlerBlock。那我们到ASRunLoopQueue被初始化的地方看到handlerBlock定义如下:
大家可以看到ASRunLoopQueue就是典型的生产者-消费者模式。ASRunLoopQueue的实例renderQueue是一个单例对象,当ASDisplayNode节点需要被重新绘制的时候,则将该node添加至renderQueue中,然后在mainRunloop对象中被调度处理,处理结束后将该节点从Queue中移除。
这里的任务主要来自于我们在修改Node的内容,需要重新绘制时,会调用ASDisplayNode的setNeedsDisplay方法(类似于UIView的setNeedsDisplay方法),这时候node会将自己添加到renderQueue中,等待被重绘。
这里主要对node内置的layer进行递归重新绘制,其中在[layer displayIfNeeded]可能会重新调起CALayer的display方法,则会触发上面创建_ASAsyncTransaction,然后生成绘制displayBlock的流程。
iOS 保持界面流畅的技巧
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/