ASDK (Texture) 简介

一 、 ASDK简介

ASDK是AsyncDisplayKit的简称, 是最初由Facebook的Paper应用程序开发的UI框架,主要用来解决尽可能缓解主线程的压力,提升应用程序的性能及使用流畅性。

ASDK 的作者是 Scott Goodson (Linkedin),

他曾经在苹果工作,负责 iOS 的一些内置应用的开发,比如股票、计算器、地图、钟表、设置、Safari 等,当然他也参与了 UIKit framework 的开发。后来他加入 Facebook 后,负责 Paper 的开发,创建并开源了 AsyncDisplayKit。

二、影响应用程序卡顿的原因

通常来说,在显示器发出的VSync 信号到来后(垂直同步信号),App 主线程开始在CPU 计算好显示内容(比如视图的创建、布局计算、图片解码、文本绘制等)提交到 GPU中,由 GPU 进行变换、合成、渲染,GPU 渲染完成后将渲染结果放入帧缓冲区,等待下一次 VSync 信号到来,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。

由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

ASDK (Texture) 简介_第1张图片

对于CPU:

1、对象创建

对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择。

尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。

2、对象调整

对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

3、对象销毁

对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。

4、布局计算

视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。

不论通过何种技术对视图进行布局,其最终都会落到对 UIView.frame/bounds/center 等属性的调整上。上面也说过,对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。

5、Autolayout

Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。

6、文本计算

如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 text.boundingRect(with: , options: , attributes: , context: ) 来计算文本宽高,用  text.draw(at: , withAttributes: ) 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。

7、文本渲染

屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。

8、图片解码

当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。

9、图像绘制

图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 drawRect(rect: CGRect)  里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。

对于GPU:

相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

1、纹理的渲染

所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。

当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。

2、视图的混合

当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。

3、图形的生成

CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

三、ASDK 的基本原理

ASDK 认为,阻塞主线程的任务,主要分为三大类。文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但 UIKit 和 Core Animation 相关操作必需在主线程进行。ASDK 的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。

ASDK (Texture) 简介_第2张图片

传统的CALayer(属性改变/动画产生)是通过delegate来通知UIView的,View 持有 Layer 用于显示,View 中大部分显示属性实际是从 Layer 映射而来;Layer 的 delegate 在这里是 View,当其属性改变、动画产生时,View 能够得到通知。UIView 和 CALayer 不是线程安全的,并且只能在主线程创建、访问和销毁。


ASDK (Texture) 简介_第3张图片

ASDK 为此创建了 ASDisplayNode 类,尝试对 UIKit 组件进行封,包装了常见的视图属性(比如frame、bounds、alpha、transform、backgroundColor、superNode、subNodes等)


ASDK (Texture) 简介_第4张图片

然后它用 UIView->CALayer 相同的方式,实现了 ASNode->UIView 这样一个关系。


ASDK (Texture) 简介_第5张图片

当不需要响应触摸事件时,ASDisplayNode 可以被设置为 layer backed,即 ASDisplayNode 充当了原来 UIView 的功能,节省了更多资源。

与 UIView 和 CALayer 不同,ASDisplayNode 是线程安全的,它可以在后台线程创建和修改。Node 刚创建时,并不会在内部新建 UIView 和 CALayer,直到第一次在主线程访问 view 或 layer 属性时,它才会在内部生成对应的对象。当它的属性(比如frame/transform)改变后,它并不会立刻同步到其持有的 view 或 layer 去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过某个机制一次性设置到内部的 view 或 layer。

通过模拟和封装 UIView/CALayer,可以把代码中的 UIView 替换为 ASNode,很大的降低了开发和学习成本,同时能获得 ASDK 底层大量的性能优化。为了方便使用, ASDK 把大量常用控件都封装成了 ASNode 的子类,比如 Button、Control、Cell、Image、ImageView、Text、TableView、CollectionView 等。利用这些控件,可以尽量避免直接使用 UIKit 相关控件,以获得更完整的性能提升。

四、ASDK的使用

1、ASDK的安装

ASDK我们使用CocoaPods来管理, pod'Texture','> = 2.0'  

2、ASDisplayNode介绍 

Texture的基本单位是node。 ASDisplayNode是一种抽象UIView,而这又是一种抽象CALayer。与只能在主线程上使用的视图不同,节点是线程安全的,可以在后台线程上并行实例化和配置它们的整个层次结构。

在 ASDK 中的渲染围绕 ASDisplayNode 进行,其过程总共有四条主线:

初始化 ASDisplayNode 对应的 UIView 或者 CALayer

在当前视图进入视图层级时执行 setNeedsDisplay

display 方法执行时,向后台线程派发绘制事务

注册成为 RunLoop 观察者,在每个 RunLoop 结束时回调

3、使用ASDisplayNode替换UIKit组件

ASDK (Texture) 简介_第6张图片


ASDK (Texture) 简介_第7张图片


ASDK (Texture) 简介_第8张图片


ASDK (Texture) 简介_第9张图片


ASDK (Texture) 简介_第10张图片


ASDK (Texture) 简介_第11张图片

以上我们可以看作是ASNode分别对基于UIKit的UILabel、UIView、UIButton、UIImageView的封装,需要注意的是,如果加载的是网络图片要使用 ASNetworkImageNode,如果是本地图片使用ASImageNode。

4、列表卡顿的优化

在基于UIkit的TableView的cell中,常常会有大量的UI绘制、文本渲染、布局计算等。对于简单的cell或者单一的cell,加上本身的cell的复用机制其实并不会出现很明显的卡顿。但当List中有N种不同样式的cell,而屏幕只能显示四五条甚至更少的cll时候,卡顿就会比较明显了。使用Node 创建时, 不会立即在其内部新建 UIView 和 CALayer, 直到主线程第一次访问时才会生成对应的对象, 除此之外, 还通过图层预合成和基于 Runloop 的异步并发。

ASTableNode异于TableView之处的是,TableNode没有复用机制,只有缓存, 每个 CellNode 都是全新的。

CellNode 与数据源没有绑定关系: 创建后就算把数据源删除, TableNode 依然可以正常展示

TableViewCell 的展示大致为: 添加空假数据子视图 -> 数据填充 -> 刷新, 涉及布局或图文时会更复杂

CellNode 只有一步: 添加真数据的子视图; 因此可以直接根据业务逻辑对控件和布局做出处理, 而不用一次或多次刷新

5、ASTableNode的核心机制  智能预加载

预加载与智能预加载(iOS)

ASDK 可以通过滚动的方向预加载不同数量的内容


ASDK (Texture) 简介_第12张图片

如上图所示 ASDK 把正在滚动的 ASTableView/ASCollectionView 划分为三种状态:

Fetch Data

Display

Visible

上面的这三种状态都是由 ASDK 来管理的,而每一个 ASCellNode 的状态都是由 ASRangeController 控制,所有的状态都对应一个 ASInterfaceState:

ASInterfaceStatePreload 当前元素貌似要显示到屏幕上,需要从磁盘或者网络请求数据;

ASInterfaceStateDisplay 当前元素非常可能要变成可见的,需要进行异步绘制;

ASInterfaceStateVisible 当前元素最少在屏幕上显示了 1px。

当用户滚动当前视图时,ASRangeController就会修改不同区域内元素的状态:

在滚动方向(Leading)上 Fetch Data 区域会是非滚动方向(Trailing)的两倍,ASDK 会根据滚动方向的变化实时改变缓冲区的位置;在向下滚动时,下面的 Fetch Data 区域就是上面的两倍,向上滚动时,上面的 Fetch Data 区域就是下面的两倍。

这里的两倍并不是一个确定的数值,ASDK 会根据当前设备的不同状态,改变不同区域的大小,但是滚动方向的区域总会比非滚动方向大一些

智能预加载能够根据当前的滚动方向,自动改变当前的工作区域,选择合适的区域提前触发请求资源、渲染视图以及异步布局等操作,让视图的滚动达到真正的流畅。

你可能感兴趣的:(ASDK (Texture) 简介)