AsyncDisplayKit(Texture)的优化原理
简介
AsyncDisplayKit是一个UI优化框架,现已改名为Texture
AsyncDisplayKit的基本单元是node
,简单来说ASDisplayNode
是一个对UIView的抽象,也可以说是对CALayer
的一个抽象。因为UIView
其实只是CALayer
的delegate。UIView
只能在主线程上使用,但是ASDisplayNode
则不同,它是线程安全的,你可以在子线程中完成实例化与配置等。
为了保持用户界面的流畅和响应性,iOS App的显示帧率应当保持再60 FPS左右。也就是说主线程有1/60秒的时间来绘制与显示每一帧,AsyncDisplayKit的主要原理就是把图像解码、文本大小计算、绘制、界面布局、对象创建与销毁以及其他耗时的UI操作从主线程中移除,以保持主界面的流畅性。
简单来说,AsyncDisplayKit的工作原理就是使用ASDisplayNode
来替代UIView
,AsyncDisplayKit 为此创建了 ASDisplayNode 类,包含了UIView的常用属性(比如 frame、bounds、alpha、transform、backgroundColor等)。
为了开发者使用方便, AsyncDisplayKit 把大量常用控件都封装成了ASDisplayNode
的子类,比如 Button、Control、Cell、Image、ImageView、Text 等。利用这些控件,开发者替代原生的UIKit控件。另外,如果ASDisplayNode
无需接受用户事件,可以关闭这个属性(isLayerBacked)。
示例
- 如
ASImageNode
ASImageNode类似于UIKit的UIImageView。最基本的区别是图像在默认情况下是异步解码的。当然,还有一些更高级的改进,比如支持GIF和imagemodificationblock。
使用UIKit创建一个UIImageView时
_imageView = [UIImageView alloc];
_imageView.image = [UIImage imageNamed:@"img"];
_imageView.frame = CGRectMake(0.0f, 0.0f, 40.0f, 40.0f);
[self.view addSubview:_imageView];
使用AsyncDisplayKit来创建UIImageView时
_imageNode = [ASImageNode new] ;
_imageNode.image = [UIImage imageNamed:@"img"];//异步解码图片
_imageNode.frame = CGRectMake(0.0f, 0.0f, 40.0f, 40.0f);
[self.view addSubview:_imageNode.view];
CPU任务
由前一篇博文可知,
移动设备中显示系统一般是由CPU绘制好显示内容,GPU渲染结束后将渲染结果放入帧缓冲区,随后视频控制器会按照VSync信号(垂直同步)读取帧缓冲区的数据,然后通过数模转换,发送给显示器显示。
如果当一个VSync信号来临的时候,帧缓冲区还没有新的渲染结果,就会使用旧的,而当前帧就会被丢弃。这就是为什么会卡帧的原因,CPU或者GPU绘制渲染跟不上VSync信号频率。
而CPU的主要任务是下面四个,这些任务都发生在主线程,如果比较耗时,就会造成主线程堵塞。
-
文本size计算与绘制
屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText配合CoreGraphics 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。
一般在UIView的drawRect
方法是实现,绘制的过程在子线程中完成,绘制完成后将context转成位图,然后再把位图在主线程里设置到view的layer里。
如果要实现图文混排,还是通过CoreText配合CoreGraphics实现,CoreText绘制文字,空出位置,CoreGraphics完成图片的绘制就行。 -
图片解码
当使用UIImage来创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库(如SDWebImage)都自带这个功能。 -
自动布局
Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。 -
布局计算
视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。不论通过何种技术对视图进行布局,其最终都会落到对 UIView.frame/bounds/center 等属性的调整上。对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。 -
图像绘制
图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphics 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。
注意:UIView 和 CALayer 不是线程安全的,并且只能在主线程创建、访问和销毁。
一个简单异步绘制的过程大致如下
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
- 对象创建与销毁
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。
GPU 任务
相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。
-
纹理的渲染
所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。 -
视图的混合 (Composing)
当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。 -
图形的生成
CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
离屏渲染参考上一篇博文
参考文章
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/