一般来说,影响CPU、GPU 、内存的性能两项主要的工作是布局和渲染。
内存
App的中内存由如下构成
总的内存占用 = All Heap Allocations + All Anonymous VM
- All Heap Allocations,几乎所有类实例,包括 UIViewController、UIView、UIImage、Foundation 和我们代码里的各种类/结构实例。一般和我们的代码直接相关。
- All Anonymous VM,主要包含一些系统模块的内存占用。有些部分虽然看起来离我们的业务逻辑比较远,但其实是保证我们代码正常运行不可或缺的部分,也是我们常常忽视的部分。
内存的优化一般有以下几个点
- 注意一些循环引用导致的内存泄露
- 使用autorelease pool加速内存的回收
一些无法避免的操作,如图片的渲染,占用内存较大,可以在平常编码的过程中注意。
图片渲染
内存开销多少与图片文件的大小(解压前的大小)没有直接关系,而是跟图片分辨率有关。所以对于一张大分辨率的图,需要根据view的frame进行缩放再显示。一般使用ImageIO(CGImage)的API进行缩放操作。整个过程最多只会占用缩放后的图片所需的内存(通常只有原图的几分之一),大大减少了内存压力。
CoreGraphics相关的API是线程安全的,可以在非主线程中操作。
CPU 耗时优化
CPU的主要任务是下面四个,这些任务都发生在主线程,如果比较耗时,就会造成主线程堵塞,在界面滑动的过程中,容易造成界面帧率下降。
对象创建与销毁
对象的创建会涉及内存分配、文件IO等消耗 CPU 资源的操作。
优化点
- 提前创建,如在首页广告闪屏过程中,提前创建首页的相关类对象;
- 异步创建,如可以把相关的对象放在子线程创建,防止阻碍主线程,但是UIView和CALayer只能在主线程创建;
- 延后创建;
- 使用轻量的对象替换,如使用CALayer替换UIView(控件没有触摸交互的时候)
文本size计算与绘制
iOS中的文本内容文本控件 (UILabel、UITextView、UIWebView等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。
优化点
- 自定义文本控件,异步处理排版和绘制,因为iOS中的文本控件的底层都是通过 CoreText排版,然后绘制为 Bitmap 显示的。
- 使用CoreText自定义文本控件后,能直接获取文本的宽高等信息,避免了多次计算。
- 使用
CoreGraphics
来绘制文本,CG类的方法一般是线程安全的,就是说可以在子线程操作。
图像的绘制一般在View的
drawRect
方法是实现,绘制的过程在子线程中完成,绘制完成后将context转成位图,然后再把位图在主线程里设置到view的layer里。
图片解码
一般我们使用的图像是JPEG/PNG,这些图像数据不是位图,而是是经过编码压缩后的数据,需要线将它解码转成位图数据,然后才能把位图渲染到屏幕上。当使用UIImage来创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程。
使用ImageIO 解码 + 后台线程执行是 WWDC(18 session 219) 推荐的做法。
优化点
- 后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。
如SDWebImage的解码核心代码。
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
if (!context) {
return NULL;
}
// Apply transform
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
布局计算
视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方。
优化点
- 子线程提前计算好视图布局、文本长度、高度等
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 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
平衡CPU和GPU任务
drawRect会利用CPU生成offscreen bitmap(寄宿图),从而减轻GPU的绘制压力,用这种方式绘制UI可以将动画流畅性优化到极致,但缺点是绘制api复杂,offscreen cache增加内存开销。UI动画流畅性的优化主要平衡CPU和GPU的工作压力。一般生成寄宿图,然后给CALayer设置contents属性。contents是CGImage类型,也是寄宿图。
AsyncDisplayKit
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];
参考文章
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/