iOS视图成像理论及性能优化

iOS不是一蹴而就的,其中参杂了无数先驱的心血与智慧。在我们享受着iPhone的便捷时,其实我们忽略了很多细节,视图成像就是这样。每天我们都会不自然的打开手机,点击应用,处理信息,获取快乐,但是我们所看到的一切都是如何来的呢?

CRT屏幕成像

首先从过去的 CRT 显示器原理说起。CRT(阴极射线管)显示器的核心部件是CRT显像管,工作原理是由灯丝、阴极、控制栅组成的电子枪,发射出电子流,电子流被带有高电压的加速器加速,进而调节电子束的功率,并经过透镜聚焦形成极细的电子束,经过垂直和水平的偏转线圈控制高速电子的偏转角度,最后高速电子击打在荧光屏上,使荧光粉发光,就会在屏幕上形成明暗不同的光点形成各种图案和文字,其工作原理和我们家中电视机的显像管基本一样。偏转线圈产生的磁场作用,可以控制电子束射向荧光屏的指定位置,通过控制电子束的强弱和通断,最终形成各种绚丽多彩的画面。因此,对于CRT来讲,屏幕上的图形图像是由一个个因电子束击打而发光的荧光点组成,由于显像管内荧光粉受到电子束击打后发光的时间很短,所以电子束必须不断击打荧光粉使其持续发光。电子枪从屏幕的左上角的第一行(行的多少根据显示器当时的分辨率所决定,比如800X600分辨率下,电子枪就要扫描600行)开始,从左至右逐行扫描,第一行扫描完后再从第二行的最左端开始至第二行的最右端,一直到扫描完整个屏幕后再从屏幕的左上角开始,这时就完成了一次对屏幕的刷新。

QQ截图20170327100045.png

每秒钟屏幕刷新的次数就叫场频,又称屏幕的垂直扫描频率,以Hz(赫兹)为单位。注意,这里的所谓“刷新次数”和我们通常在描述游戏速度时常说的“画面帧数”是两个截然不同的概念。后者指经电脑处理的动态图像每秒钟显示显像管电子枪的扫描频率。荧光屏上涂的是中短余辉荧光材料,否则会导致图像变化时前面图像的残影滞留在屏幕上,但如此一来,就要求电子枪不断的反复“点亮”、“熄灭”荧光点,场频与图像内容的变化没有任何关系,即便屏幕上显示的是静止图像,电子枪也照常更新。扫描频率过低会导致屏幕有明显的闪烁感,即稳定性差,容易造成眼睛疲劳。一般来讲,屏幕的刷新率要达到75HZ以上,人眼才不易感觉出,但长时间注视必然会让眼睛感到很累。所以,屏幕的刷新率是越高越好,当前市场中,低、中端指标产品垂直扫描频率为50~150Hz,而高端指标产品的垂直扫描频率在50-160Hz。

有了场频就不得不提下带宽,带宽是指每秒钟所扫描的图像频点的总和,也就是每秒钟电子枪扫描过的总像素数,它等于“水平分辨率×垂直分辨率×场频(画面刷新次数)”,带宽采用的单位为MHz(兆赫)。带宽代表的是显示器的一个综合指标,也是衡量一台显示器好坏的重要指标,因此它是显示器最基本的频率特性,它决定着一台显示器可以处理的信息范围,就是指电路工作的频率范围。显示器工作频率范围在电路设计时就已定死了,主要由高频放大部分元件的特性决定。高频处理能力越好,带宽能处理的频率越高,图像也更好。每种分辨率都对应着一个最小可接受的带宽,但如果带宽小于该分辨率的可接受数值,显示出来的图像会因损失和失真而模糊不清。

总结来说,CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。

尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。

液晶显示器工作原理

目前液晶显示技术大多以TN、STN、TFT三种技术为主。TN型液晶显示技术是液晶显示中最基本的,其它种类的液晶显示器械皆以TN型为基础来加以改进,所以它的运作原理也较其它技术来的简单。它主要包括垂直方向与水平方向的偏光板、配向膜、液晶材料以及导电的玻璃基板。

TN型液晶显示技术的显象原理是将液晶材料置于两片透明导电玻璃间,液晶分子会依配向膜的细沟槽方向依序旋转排列,如果电场未形成,光线会顺利的从偏光板射入,依液晶分子旋转其行进方向,然后从另一边射出。如果在两片导电玻璃通电之后,两片玻璃间会形成电场,进而影响其间液晶分子的排列,使其分子棒进行扭转,光线便无法穿透,进而遮住光源。这样所得到光暗对比的现象,叫做扭转式向列场效应,简称TNFE(twisted nematic field effect)。在电子产品中所用的液晶显示器,几乎都是用扭转式向列场效应原理所制成。但因为单纯的TN液晶显示器本身只有明暗两种情况,所以只能形成黑白两种颜色,并没有办法做到色彩的变化。

QQ截图20170327100114.png

STN型的显示原理与TN相类似,不同的是TN扭转式向列场效应的液晶分子是将入射光旋转90度,而STN超扭转式向列场效应是将入射光旋转180~270度。这一区别导致了光线的干涉现象,实现了一定程度色彩的变化,使STN型液晶显示器具备了一些淡绿色与橘色的色调。如果再加上一个色彩滤光片(color filter),并将单色显示矩阵之任何一个像素(pixel)分成三个子像素(sub-pixel),分别通过彩色滤光片显示红、绿、蓝三原色,再经由三原色比例之调和,也可以显示出全彩模式的色彩。

一般TFT液晶显示屏的主要构成包括荧光管、导光板、偏光板、滤光板、玻璃基板、配向膜、液晶材料、薄模式液晶体管等。这种液晶显示器必须先利用荧光灯管投射出光源,这些光源会先经过一个偏光板然后再经过液晶,这时液晶分子的排列方式会改变穿透液晶的光线角度,然后这些光线接下来还必须经过前方的彩色的滤光膜与另一块偏光板。而我们只要改变刺激液晶的电压值就可以控制最后出现的光线强度与色彩,并进而能在液晶面板上变化出有不同深浅的颜色组合了。它与前二者的区别是把TN上部夹层的电极改为FET液体管,而下层改为共同电极。

从结构上看,液晶屏由两片线性偏光器和一层液晶所构成。其中,两片线性偏光器分别位于液晶显示器的内外层,每片只允许透过一个方向的光线,它们放置的方向成90度交叉(水平、垂直),也就是说,如果光线保持一个方向射入,必定只能通过某一片线性偏光器,而无法透过另一片,默认状态下,两片线性偏光器间会维持一定的电压差,滤光片上的薄膜晶体管就会变成一个个的小开关,液晶分子排列方向发生变化,不对射入的光线产生任何影响,液晶显示屏会保持黑色。一旦取消线性偏光器间的电压差,液晶分子会保持其初始状态,将射入光线扭转90度,顺利透过第二片线性偏光器,液晶屏幕就亮起来了。

红绿蓝是显示的三原色,当这三种颜色同时混合时就会产生白色,这是在三原色强度一样的情况下才能够显示纯正的白色,这样,我们可以看见液晶面板的每一个像素中都有三种原色,这三种原色如果强度不同变化就可以产生不同的混色效果,以1024×768举例的话,全屏就有1024×768这样的像素,所以真实分辨率就是1024×768。低端的液晶显示板,各个基色只能表现6位色,即2的6次方=64种颜色。可以很简单的得出,每个独立像素可以表现的最大颜色数是64×64×64=262144种颜色,利用FRC技术使得每个基色则可以表现8位色,即2的8次方=256种颜色,则像素能表现的最大颜色数为256×256×256=16777216种颜色。当然还有32位真彩色等等,这种显示板显示的画面色彩更丰富,层次感也好。

计算机处理成像

计算机系统中 CPU、GPU、显示器是协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照垂直同步信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

QQ截图20170327100130.png

所谓帧缓存,是指渲染的帧保存在的位置。使用内置帧缓存,调用文件会更快,减少内存使用。在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都会有比较大的效率问题。因此引入双缓冲机制,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。双缓冲虽然能解决效率问题,但是当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。针对这个问题,GPU通常会做垂直同步,GPU 会等待显示器的垂直同步信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。现在iOS 设备会始终使用双缓存,并开启垂直同步。

屏幕成像问题

从上面的分析我们可以看到,屏幕的显示成像是结合了CPU数据处理和GPU渲染而来的,即每次垂直同步信号过来屏幕刷新时都会受到CPU和GPU的影响。CPU 计算显示内容(视图的创建、布局计算、图片解码、文本绘制等),完成后将计算好的内容提交到GPU,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次垂直同步信号到来时显示到屏幕上。iOS中系统图形服务通过 CADisplayLink 等机制通知 App,App 在主线程开始上述操作。如果在一个刷新时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变,从而产生掉帧情况,从而导致界面卡顿。

由此,我们就了解了,iOS成像卡顿是由CPU和GPU的不合理处理导致的,对于图像问题,有针对的分析就可以解决了。下面,我们先来看一看比较容易模糊的离屏渲染问题。

离屏渲染

相比于当前屏幕渲染,离屏渲染的代价是很高的,这也是iOS移动端优化的必要部分。GPU屏幕渲染有以下两种方式:

1.On-Screen Rendering

意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

2.Off-Screen Rendering

意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

一般情况下,会将应用提交到Render Server的效果直接渲染显示,但对于一些复杂的图像动画的渲染并不能直接渲染叠加显示,而是需要根据Command Buffer分通道进行渲染之后再组合,这一组合过程中,就有些渲染通道是不会直接显示的。Masking渲染需要更多渲染通道和合并的步骤,而这些没有直接显示在屏幕的上的通道就是Offscreen Rendering Pass。

离屏渲染时屏幕外的渲染会合并渲染图层树的一部分到一个新的缓冲区,然后该缓冲区被渲染到屏幕上。离屏渲染时一种好的方法就是缓存合成的纹理/图层,如果你的渲染树非常复杂(纹理及组合逻辑),你可以强制离屏渲染缓存那些图层,然后可以用缓存作为合成的结果放到屏幕上,通过设置 shouldRasterize 为 YES 来触发这个行为。shouldRasterize被设置成YES,在触发离屏绘制的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用,光栅化相当于是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用,这将在很大程度上提升渲染性能。但是rasterized layer(栅格化图层)的空间是有限的,iOS大概有屏幕大小两倍的空间来存储 rasterized layer或是屏幕外缓冲区。

如果设置了 shouldRasterize 为 YES,那也要记住设置 rasterizationScale 为 contentsScale。即:

1
2
3
self.layer.shouldRasterize = YES;  
 
self.layer.rasterizationScale = [UIScreen mainScreen].scale;

混合图层做动画时,GPU 会为每一帧(1/60s)重复合成所有的图层。当使用离屏渲染时,GPU 第一次会混合所有图层到一个基于新的纹理的位图缓存上,然后使用这个纹理来绘制到屏幕上。当这些图层一起移动的时候,GPU 便可以复用这个位图缓存,并且只需要做很少的工作。如果那些图层改变了,GPU 需要重新创建位图缓存。所以使用时,还是需要慎重。

Offscreen Render为什么卡顿?因为Offscreen Render需要更多的渲染通道,离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕,不同的渲染通道间切换需要耗费一定的时间,这个时间内GPU会闲置,当通道达到一定数量,对性能也会有较大的影响。例如下图:

QQ截图20170327100233.png

如果你正在直接或者间接的将mask应用到一个图层上,Core Animation 为了应用这个 mask,会强制进行屏幕外渲染,这会对 GPU 产生重负。Instrument 的 Core Animation 工具有一个叫做Color Offscreen-Rendered Yellow的选项,它会将已经被渲染到屏幕外缓冲区的区域标注为黄色(这个选项在模拟器中也可以用)。Color Hits Green and Misses Red选项,绿色代表无论何时一个屏幕外缓冲区都会被复用,而红色则代表缓冲区被重新创建。一次mask发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若再加上上下文环境切换,mask这个耗时将是无mask操作耗时的数十倍。

离屏渲染的触发

离屏渲染可以被 Core Animation 自动触发,或者被应用程序强制触发。iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染,iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

离屏渲染会触发两个步骤:创建新缓冲区和上下文切换,这在使用时是需要权衡。第一,这可能会使事情变得更慢。创建额外的屏幕外缓冲区是 GPU 需要多做的一步操作,特殊情况下这个位图可能再也不需要被复用,这便是一个无用功了。然而,可以被复用的位图,GPU 也有可能将它卸载了。所以在使用时需要计算 GPU 的利用率和帧的速率来判断这个位图是否有用。

一般情况下,你需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。所以当你打开Instrument 的 Core Animation 工具下的 Color Offscreen-Rendered Yellow 后看到黄色,这便是一个警告,但这不一定是不好的。如果 Core Animation 能够复用屏幕外渲染的结果,这便能够提升性能。

最好不要使用 layer 的方式产生屏幕外渲染。为 layer 使用蒙板或者设置圆角半径会造成屏幕外渲染,产生阴影也会如此。如果不需要对外部来源的图片做圆角处理,可以由设计师直接画成圆角图片,或是在要添加圆角的视图上再叠加一个部分透明的视图,只对圆角部分进行遮挡。多一个图层会增加合成的工作量,但这点工作量与离屏渲染相比微不足道。如果叠加的视图都一样,可以只加载一次遮罩图片以减少内存占用。或者使用 Core Graphics 提前重绘以备使用。

如果您在开发中不是专门做图像处理的,请避免使用离屏渲染。shouldRasterize(光栅化)、masks(遮罩)、shadows(阴影)、edge antialiasing(抗锯齿)、group opacity(不透明)都会触发离屏绘制。具体如下:

1) drawRect

2) layer.shouldRasterize = true;

3) 有mask或者是阴影(layer.masksToBounds, layer.shadow*);

 3.1) shouldRasterize(光栅化)

 3.2) masks(遮罩)

 3.3) shadows(阴影)

 3.4) edge antialiasing(抗锯齿)

 3.5) group opacity(不透明)

4) Text(UILabel, CATextLayer, Core Text)...

但是layer.cornerRadius,layer.borderWidth,layer.borderColor并不会Offscreen Render,因为这些不需要加入Mask。

特殊的“离屏渲染”方式:CPU渲染

Core Graphics 的绘制 API 会触发离屏渲染,但不是那种 GPU 的离屏渲染。使用 Core Graphics 绘制 API 是在 CPU 上执行,触发的是 CPU 版本的离屏渲染。如果我们重写了drawRect方法(使用CoreGraphics来实现的绘制,或使用CoreText[其实就是使用CoreGraphics]绘制)进行操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示,基本上使用时只是调用了一些向位图缓存内写入一些二进制信息的方法而已。

当前屏幕渲染、离屏渲染、CPU渲染的选择

1.尽量使用当前屏幕渲染,鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。

2.离屏渲染相对于CPU渲染,由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染;但如果仅仅是实现一个简单的效果,直接使用CPU渲染的效率又可能比离屏渲染好,毕竟离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。

有些离屏绘制是发生在绘制服务(是独立的处理过程)并且同时通过GPU执行。当绘制程序在绘制每个layer的时候,有可能因为包含多子层级关系而必须停下来把他们合成到一个单独的缓存里。对GPU来说,从当前屏幕(on-screen)到离屏(off-screen)上下文环境的来回切换(这个过程必须flush管线和光栅),代价是非常大的。因此对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做得更好。所以在尝试处理一些复杂的层级,并且在犹豫到底用[CALayer setShouldRasterize:]还是通过CoreGraphics来绘制层级上的所有内容时,唯一的方法就是测试并且进行权衡。

补充:图层与UIView

UIView能显示在屏幕上,是因为里面有个图层,UIView的DrawRect方法,会将绘制的东西绘在图层上,当要显示的时候,将图层拷贝到屏幕上进行显示。UIView本身不具备显示的功能,是它内部的层才有显示功能。UIView与CALayer的细节如下:

1.CALayer属于QuartzCore框架 与 CGColor所属于CoreGraphics框架。QuartzCore框架与CoreGraphics框架都是可跨平台使用,能在iOS和Mac OS X上都能使用。

2.UIView比CALayer多了一个事件处理的功能。

CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的。CALayer 比 UIView 要轻量许多,不需要响应触摸事件的控件,用 CALayer 显示会更加合适。

CALayer的基本属性

1.尺寸(bounds)

2.背影颜色(backgroundColor)

3.位置(position)

4.边框颜色(borderColor)、边框长度(borderWidth) 配合使用才有效果

5.圆角(cornerRadius)

6.内容对象(contents) 通常设置图片,如果要显示圆角效果,必须设置图层的masksToBounds为YES,意思是把多余的剪切掉。

7.阴影属性 如果图片添加了圆角并设置了剪切掉多余的部分,阴影效果不会出现;如果又想图片有圆角效果,又想有阴影,那图片只能是本来就有圆角效果的,不用代码设置。

  • 阴影颜色(shadowColor)、阴影透明度(shadowOpacity)

要配合使用才有效果

  • 阴影起始位置(shadowOffset)

transform(CATransform3D)属性

  • 缩放CATransform3DMakeScale

  • 旋转CGAffineTransformMakeRotation,理解xyz方向的意思

  • 平移CATransform3DMakeTranslation

  • transform里的key Paths 如transform.scale 、transform.rotation、 transform.translation

CALayer and Mask

使用Mask我们可以很容易的设置一个遮罩层,一般来说,给UIView或者CALayer做Mask只需要用一个CAShapeLayer来充当CALayer或者UIView.layer的mask属性就好了(当然你也可以用别的炫酷的CALayer子类来当mask,比如说CATextLayer)。核心动画提供了很多种层,来帮助我们完成许多的任务。下面讨论几个比较有用的层,包括:

1.CAShapeLayer,这个层提供了一个简单的可以使用核心图像路径在层树中组成一个阴影的方法。使用CAShapeLayer,你可以通过创建一个核心图像路径,并且分配给CAShaperLayer的path属性,从而为需要的形状指定路径。并且你可以使用-setFillColor方法给该形状指定一个填充颜色。

2.CAGradientLayer,这个层你可以通过指定颜色,一个开始的点,一个结束的点和梯度类型使你能够简单的在层上绘制一个梯度。

3.CAReplicatorLayer,可以复制任何增加到层中的子层。这个复制的子层还可以被变换来产生一个耀眼的效果。

当一个特效或者一些不常见的阴影需要时,这些层能用来完成目标。

CAShapeLayer示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)viewDidLoad {
[ super  viewDidLoad];
 
UIImageView *image = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@ "launch_5" ]];
[self.view addSubview:image];
image.frame = CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height);
 
CAShapeLayer *shape = [CAShapeLayer layer];
CGMutablePathRef ms = CGPathCreateMutable();
//CGPathAddEllipseInRect(ms, nil, CGRectInset(CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.width), 50,50));画圆
CGPathAddRect(ms, nil, CGRectInset(CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.width), 50,50)); //画方
shape.path = ms;
//shape.fillColor = [UIColor greenColor].CGColor;
shape.shadowOpacity = 1;
shape.shadowRadius = 45;
image.layer.mask = shape;
}

通过设置maskLayer的shadow,实际上是让圆圈边缘地区生成从额外的alpha通道值,mask构建的时候也能够获知阴影产生的alpha通道,从而让照片有(伪)边缘羽化的效果,如果被mask的layer是纯色或者渐变色的话,这样可以做出来辉光的感觉。

下面是CAGradientLayer,你在iphone上或者MacOS X上经常看到一张图片的倒影效果。这种效果是困难去做的,但是用CAGradientLayer就很简单了。创建一个倒影的基本方案是(就像在iChat或者在iWeb展示的图片)使用主图片的一个拷贝翻转放置到主图片的下方。然后你应用一个梯度到翻转的图片上,像是背景的阴影一样。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
- (void)viewDidLoad {
[ super  viewDidLoad];
[[[self view] layer]setBackgroundColor:
  [[UIColor blackColor]CGColor]];
UIImage *balloon = [UIImage imageNamed:@ "launch_5" ]; //换图片即可
// Create the top layer; thisis the main image
CALayer *topLayer = [[CALayer alloc] init];
[topLayer setBounds:CGRectMake(0.0f, 0.0f, 320.0, 240.0)];
[topLayer setPosition:CGPointMake(160.0f, 120.0f)];
[topLayer setContents:(id)[balloon CGImage]];
// Add the layer to the view
[[[self view] layer]addSublayer:topLayer];
// Create the reflectionlayer; this image is displayed beneath // the top layer
CALayer *reflectionLayer =[[CALayer alloc] init];
[reflectionLayer setBounds:CGRectMake(0.0f, 0.0f,320.0, 240.0)];
[reflectionLayer setPosition:CGPointMake(158.0f, 362.0f)];
// Use a copy of the imagecontents from the top layer // for the reflection layer
[reflectionLayer setContents:[topLayer contents]];
// Rotate the image 180degrees over the x axis to flip the image
[reflectionLayer setValue:DegreesToNumber(180.0f) forKeyPath:@ "transform.rotation.x" ];
// Create a gradient layer touse as a mask for the
// reflection layer
CAGradientLayer *gradientLayer = [[CAGradientLayer alloc] init];
[gradientLayer setBounds:[reflectionLayer bounds]];
[gradientLayer setPosition:
  CGPointMake([reflectionLayer bounds].size.width/2, [reflectionLayer bounds].size.height/2)];
[gradientLayer setColors:[NSArray arrayWithObjects: (id)[[UIColor clearColor] CGColor],
                          (id)[[UIColor blackColor]CGColor], nil]];
// Override the default startand end points to give the gradient // the right look
[gradientLayer setStartPoint:CGPointMake(0.5,0.35)];
[gradientLayer setEndPoint:CGPointMake(0.5,1.0)];
// Set the reflection layer’smask to the gradient layer
[reflectionLayer setMask:gradientLayer];
// Add the reflection layerto the view  
[[[self view] layer]addSublayer:reflectionLayer];
 
 
}
 
NSNumber *DegreesToNumber(CGFloat degrees) {
return  [NSNumber numberWithFloat: DegreesToRadians(degrees)];
}
CGFloat DegreesToRadians(CGFloat degrees) {
return  degrees * M_PI / 180;}
 
- (void)didReceiveMemoryWarning {
[ super  didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

CAReplicatorLayer是一个不常用的但是很强大的CALayer的子类。它的主要工作是反射任何增加到它上面的子层。这些子类可以被反射很多次是基于-instanceCount这个属性的。另外反射它的子层,CAReplicatorLayer将会基于下面的属性,改变他们的颜色和变换层:

1
2
3
4
5
6
7
8
9
10
11
instanceTransform
 
instanceColor
 
instanceRedOffSet
 
instanceGreenOffSet
 
instanceBlueOffset
 
instanceAlphaOffset

一个用途就是用CAReplicatorLayer来模拟图像的倒影类似与CoverFlow。你可以创建一个UIView,那会自动创建一个子层的镜像。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)viewDidLoad {
[ super  viewDidLoad];    
CGMutablePathRef ms = CGPathCreateMutable();
CGPathAddEllipseInRect(ms, nil, CGRectInset(CGRectMake(0, 50, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.width), 50,50));
 
// 具体的layer
UIView *tView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
tView.center = CGPointMake([[UIScreen mainScreen] bounds].size.width - 50, 250);
tView.layer.cornerRadius = 5;
tView.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
 
// 动作效果
CAKeyframeAnimation *loveAnimation = [CAKeyframeAnimation animationWithKeyPath:@ "position" ];
loveAnimation.path = ms;
loveAnimation.duration = 8;
loveAnimation.repeatCount = MAXFLOAT;
[tView.layer addAnimation:loveAnimation forKey:@ "loveAnimation" ];
 
  CAReplicatorLayer *loveLayer = [CAReplicatorLayer layer];
loveLayer.instanceCount = 40;                 // 40个layer
loveLayer.instanceDelay = 0.2;                // 每隔0.2出现一个layer
loveLayer.instanceColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0].CGColor;
loveLayer.instanceGreenOffset = -0.03;        // 颜色值递减。
loveLayer.instanceRedOffset = -0.02;          // 颜色值递减。
loveLayer.instanceBlueOffset = -0.01;         // 颜色值递减。
[loveLayer addSublayer:tView.layer];
[self.view.layer addSublayer:loveLayer];   
}

绘制图形

1.画线

1
2
3
4
5
6
- (void)drawRect:(CGRect)rect{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(context, 10, 10);
CGContextAddLineToPoint(context, 30, 100);
CGContextStrokePath(context);
}

2.画三角形

1
2
3
4
5
6
7
8
9
10
- (void)drawRect:(CGRect)rect{
//画三角形
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1);
CGContextMoveToPoint(context, 10, 10);
CGContextAddLineToPoint(context, 110, 10);
CGContextAddLineToPoint(context, 110, 110);
CGContextClosePath(context);
CGContextStrokePath(context);
}

3.画矩形

1
2
3
4
5
6
7
- (void)drawRect:(CGRect)rect{
//画矩形
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddRect(context, CGRectMake(10, 20, 100, 100));
//CGContextFillPath(context);
CGContextStrokePath(context);
}

4.画扇形

1
2
3
4
5
6
7
8
- (void)drawRect:(CGRect)rect{
//画扇形
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(context, 100, 100);
CGContextAddArc(context, 100, 100,60, - 3 * M_PI_4, -M_PI_4, 1);
CGContextClosePath(context);
CGContextStrokePath(context);
}

5.画弧

1
2
3
4
5
6
7
8
9
10
11
12
13
-(void)drawArc{
CGContextRef context = UIGraphicsGetCurrentContext();
  //x,y 圆心
  //radius 半径
  //startAngle 画弧的起始位置
  //endAngel 画弧的结束位置
   //clockwise 0 顺针 1 逆时针
CGContextAddArc(context, 100, 100, 60, 0, M_PI, 1);
CGContextClosePath(context);
//渲染
CGContextStrokePath(context);
//CGContextFillPath(context);
}

6.画圆

1
2
3
4
5
6
- (void)drawRect:(CGRect)rect{
//画圆
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(context, CGRectMake(10, 10, 100, 100));
CGContextStrokePath(context);
}

7.画图片

1
2
3
4
5
- (void)drawRect:(CGRect)rect{
//画图片
UIImage *image = [UIImage imageNamed:@ "papa" ];
[image drawAsPatternInRect:CGRectMake(10, 10, 50, 50)];
}

8.画文字

1
2
3
4
5
6
- (void)drawRect:(CGRect)rect{
//画文字
NSString *str = @ "恍恍惚惚恍恍惚惚恍恍惚惚恍恍惚惚" ;
NSDictionary *attr = @{NSFontAttributeName:[UIFont systemFontOfSize:13],NSForegroundColorAttributeName:[UIColor yellowColor]};
[str drawInRect:CGRectMake(10, 10, 100, 100) withAttributes:attr];
}

CPU处理优化

由上面的分析我们可以看到,影响屏幕显示的CPU方面的原因主要由以下几个方面:对象创建、对象调整、对象销毁、布局计算、文本计算、图片解码、自动布局、图像绘制、文本渲染等等。在正常的开发中,这些方面我们是不需要做太多的处理的,因为iOS的处理极其的强大,相对于安卓手机来说,iOS内部就对这些方面做了很好的管控,在业务开发中,正常使用即可。但是,今天我们主要讨论的是优化方面,所以会在这些方面做一些优中选优的工作。除非是对性能要求极高的开发工作,其它时候常规使用即可。

我们都知道,CPU 用于计算显示内容,内容显示时首先便是创建内容。在对象的创建中会进行分配内存、调整属性等操作。因为每个对象都会占用一定的内存,在OC中实现一个功能可以有多个选择,这就给了我们很大的操作空间。尽量用轻量的对象代替重量的对象,如上面的CALayer 与 UIView 的比较,在后台线程创建对象也是一种选择。同时使用懒加载技术,推迟对象的创建时间,并实现复用,都能很好的把控对象创建时的CPU消耗。假如创建了很多临时对象,你会发现内存一直不会减少直到这些对象被release的时候,这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。但是,可以在自己的@autoreleasepool里创建临时的对象来避免这个行为。如下面的示例:

1
2
3
4
5
6
7
8
9
NSArray *urls = @[@ "http://baidu.com" ,@ "http://jd.com" ];
for  (NSURL *url  in  urls) {
     @autoreleasepool {
         NSError *error;
         NSString *fileContents = [NSString stringWithContentsOfURL:url
                                                           encoding:NSUTF8StringEncoding error:&error];
         // 处理
     }
}

这段代码在每次遍历后释放所有autorelease对象。

CPU的优化处理其实考虑更多的是对技术理解的深度,只要不怕麻烦,一切都可操控。在视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,优化时应该尽量避免调整视图层次、添加和移除视图。对象销毁对资源的消耗不多,一个方法就是在后台线程销毁处理。视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方,不论通过何种技术对视图进行布局,其最终都会落到对 UIView.frame/bounds/center 等属性的调整上。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。Autolayout 是苹果本身提倡的技术,在大部分情况下能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。优化时追求的是细节,Autolayout恰恰是隐藏了细节。

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

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

图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程,如 [UIView drawRect:] 方法。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
- (void)method {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
     CGContextRef ctx = CGBitmapContextCreate(...);
     // draw in context...
     CGImageRef img = CGBitmapContextCreateImage(ctx);
     CFRelease(ctx);
     dispatch_async(dispatch_get_main_queue(), ^{
         layer.contents = img;
     });
});
}

总结来说,就是后台线程处理,然后在主线程显示。

iOS5中加入的Storyboards正在快速取代XIB,然而XIB在一些场景中仍然很有用。比如你的app需要适应iOS5之前的设备,或者你有一个自定义的可重用的view,你就不可避免地要用到他们。如果你不得不使用XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。

需要注意的是,当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller。在XIB中,所有图片都被chache,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage 的imageNamed:方法来获取图片资源。所以,使用XIB来说,对于性能的优化并不是一个很好的选择。

CPU消耗的处理手段有限,大概就是后台处理,注意细节,能用更底层的就用其替代,因为封装越好的可操控和优化性越低。永远不要使主线程承担过多,因为UIKit在主线程上做所有工作,渲染、管理触摸反应、回应输入等都需要在它上面完成。如果阻塞了主线程,app就会失去反应。大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络等。如果需要做其它类型的需要耗费巨大资源的操作(比如时间敏感的计算或者存储读写)那就用 GCD 或者 NSOperation 和 NSOperationQueues,从而在后台进行处理。其实,无非这样。

GPU处理优化

GPU 主要处理的内容是接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常应用上所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类,这是基于OpenGL而来的。

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

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

把不透明的Views设置它们的opaque属性为YES的原因是,这会使系统用一个最优的方式渲染这些views。这个简单的属性在IB或者代码里都可以设定。Apple的文档对于为图片设置不透明属性的描述是:(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES,渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。在相对比较静止的画面中,设置这个属性不会有太大影响。然而当这个view嵌在scroll view里边,或者是一个复杂动画的一部分,不设置这个属性的话会在很大程度上影响app的性能。

CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。图像显示时可以用整幅的图片、可调大小的图片、也可以用CALayer、CoreGraphics甚至OpenGL来画它们。每个不同的解决方法都有不同的复杂程度和相应的性能。简单来说,就是用事先渲染好的图片更快一些,因为如此一来iOS就免去了创建一个图片再画东西上去然后显示在屏幕上的程序。方法很多,其实最重要的无非是权衡利弊。

GPU 编程的高度优化框架 Metal

Metal 是针对 iPhone 和 iPad 中 GPU 编程的高度优化的框架,是专门为了苹果的平台而创建的,是iOS原生的框架,用 Objective-C 编写,基于 Foundation,使用 GCD 在 CPU 和 GPU 之间保持同步。它是更先进的 GPU 管道的抽象,如果 OpenGL 想达到这些的话只能完全重写。

Metal 框架支持 GPU 加速高级 3D 图像渲染,以及数据并行计算工作。它不仅为图形的组织、处理和呈现,也为计算命令以及为这些命令相关的数据和资源的管理,提供了细粒度和底层的控制。Metal 的主要目的是最小化 GPU 工作时 CPU 所要的消耗。对于寻找游戏引擎的开发者来说,Metal 不是最佳选择。如果想要一个 iOS 上高性能的并行计算库,Metal 是唯一的选择。

Metal 的最大好处就是与 OpenGL ES 相比显著降低了消耗。在 OpenGL 中无论创建缓冲区还是纹理,OpenGL 都会复制一份以防止 GPU 在使用它们的时候被意外访问。出于安全的原因复制类似纹理和缓冲区这样的大的资源是非常耗时的操作,而 Metal 并不复制资源。Metal 的另外一个好处是其预估 GPU 状态来避免多余的验证和编译。通常在 OpenGL 中,你需要依次设置 GPU 的状态,在每个绘制指令 (draw call) 之前需要验证新的状态。最坏的情况是 OpenGL 需要再次重新编译着色器 (shader) 以反映新的状态。当然,这种评估是必要的,但 Metal 选择了另一种方法,在渲染引擎初始化过程中,一组状态被烘焙 (bake) 至预估渲染的路径 (pass) 中。多个不同资源可以共同使用该渲染路径对象,但其它的状态是恒定的。Metal 中一个渲染路径无需更进一步的验证,使 API 的消耗降到最低,从而大大增加每帧的绘制指令的数量。

Metal 提供的大多是协议,因为 Metal 对象的具体类型取决于 Metal 运行在哪个设备上。这鼓励了面向接口而不是面向实现编程。如果不使用 Objective-C 运行时的广泛而危险的操作,就不能子类化 Metal 的类或者为其增加扩展。Metal 为了速度而在安全性上做了必要的妥协。在某些时候,你会收到指向内部缓冲区的裸指针,你必须小心的同步访问它。OpenGL 中发生错误时,结果通常是黑屏;然而在 Metal 中,结果可能是完全随机的效果,例如闪屏和偶尔的崩溃。之所以有这些陷阱,是因为 Metal 框架是对 GPU 的非常轻量级抽象。苹果并没有为 Metal 实现可以在 iOS 模拟器上使用的软件渲染。使用 Metal 框架的时候应用必须运行在真实设备上。所以,在模拟器上 Metal 会报错。

Metal处理因为更偏向底层,所以写法上比我们常见的OC写法要复杂一些,但在GPU的高度优化上确实有显著的提升,总之就是好用不好写,根据需要使用。

具体示例可以在https://github.com/warrenm/metal-demo-objcio上获得。

总结,优化显示的多种手段

通过上面的分析,我们已经清楚了,优化界面的显示需要结合CPU和GPU的特性,并关注于iOS成像系统的逻辑。由于iOS的显示都要放在主线程进行处理,所以防止主线程的阻塞是成像处理中的一大关键。阻塞主线程的方面在于文本和布局的计算、渲染、解码、绘制、对象创建调整及销毁,具体的分析在上面我们也做了一些研究,在开发一些应用时,针对这些我们可以使用预排版、预渲染、异步绘制来逐一解决。

为了保证table view平滑滚动,需要保证以下条件:正确使用reuseIdentifier来重用cells;尽量使所有的view opaque,包括cell自身;避免渐变,图片缩放;缓存行高;如果cell内现实的内容来自web,使用异步加载,缓存请求结果;使用shadowPath来画阴影;减少subviews的数量;尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果;使用正确的数据结构来存储数据;使用rowHeight, sectionFooterHeight和 sectionHeaderHeight来设定固定的高,不要请求delegate。

对于通常的 TableView 来说,提前在后台计算好布局结果是非常重要的一个性能优化点。优化点有重用 cell,减少 cell 初始化的工作量,延迟装载,定制复杂 cell 时使用 drawRect 自绘,Cache 尽可能多的东西包括 cell 高度,尽可能让 cell 不透明,避免使用图像特性,比如 gradients(渐变)。因为自动布局和UILabel 等文本控件的计算会消耗系统资源,为了达到最高性能,可能需要牺牲这些方面,避免使用UILabel、UITextView等在主线程中进行排版和绘制的控件,自定义文本控件,用TextKit或者CoreText进行文本异步绘制。如果对性能的要求不高,可以尝试用 TableView 的预估高度的功能,并把每个 Cell 高度缓存下来。处理 Cell 需要的数据时在后台线程计算并封装为一个布局对象 CellLayout。CellLayout 包含所有文本的 CoreText 排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了。

对于 TableView 来说,Cell 内容的离屏渲染会带来较大的 GPU 消耗。为了避免离屏渲染,应当尽量避免使用 layer 的 border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应内容并单独保存到一个 ImageCache 中去。列表中有不少视觉元素并不需要触摸事件,这些元素可以用图层合成技术预先绘制为一张图,进一步减少每个 Cell 内图层的数量,同时用 CALayer 替换掉 UIView。处理显示文本的控件时采用异步绘制,当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。针对于此,尽量快速、提前判断当前绘制任务是否已经被取消,在绘制每一行文本前,保证被取消的任务能及时退出,不至于影响后续操作。

CPU 是用时间片轮转来实现线程并发的,serial queue 不能充分利用多核 CPU 的资源,尽管 concurrent queue 能控制线程的优先级,但当大量线程同时创建运行销毁时,这些操作仍然会挤占掉主线程的 CPU 资源。大量的任务提交到后台队列时,某些任务会因为某些原因被锁住导致线程休眠,或者被阻塞,concurrent queue 随后会创建新的线程来执行其他任务。当这种情况变多时,或者 App 中使用了大量 concurrent queue 来执行较多任务时,App 在同一时刻就会存在几十个线程同时运行、创建、销毁。一种办法是把 App 内所有异步操作,包括图像解码、对象释放、异步绘制等,都按优先级不同放入全局的 serial queue 中执行,这样尽量避免了过多线程导致的性能问题。

另外,使图片符合UIImageView的尺寸,不要在运行的时候再让UIImageView自行压缩,因为这样会降低运行时的性能。图片和视图的大小避免超过4096*4096,因为这是目前iphone5到iphone6p以及ipad仅仅通过GPU就直接处理的纹理尺寸上限,否则GPU就会提交CPU先处理,这样开销很大。设置UIView的背景图片时,如果是整幅图,就采用addSubView一个UIImageView;如果是要重复平铺一个小图,就使用colorWithPatternImage,因为这个函数的设计上就是针对小图的,如果用于整幅大图来做背景,反而会消耗更多内存。

1
2
3
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@ "background" ]];
 
[self.view addSubview:backgroundView];

或者:

1
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@ "background" ]];

针对单个 view 尽量不要在viewWillAppear做费时的操作,viewWillAppear在 view 显示之前被调用,出于效率考虑,在这个方法中不要处理复杂费时的事情,只应该在这个方法设置 view 的显示属性之类的简单事情,比如背景色,字体等。避免使用静态初始化,包括静态c++对象和加载时会运行的代码,如+(void) load{}(在程序启动之前会调用所有的类的[手动实现的]+load方法[没有实现不调用],按父类->子类->类别的顺序调用) ,这样会造成在Main函数之前运行额外的代码。

关于shadow,通常的做法是:

1
2
3
4
5
6
7
UIView *view = [[UIView alloc] init];
 
view.layer.shadowOffset = CGSizeMake(-1.0f, 1.0f);
 
view.layer.shadowRadius = 5.0f;
 
view.layer.shadowOpacity = 0.6;

如此,Core Animation不得不先在后台得出你的图形并加好阴影然后才渲染,这开销是很大的。使用shadowPath的话就避免了这个问题:

1
view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];

使用shadow path的话iOS就不必每次都计算如何渲染,它使用一个预先计算好的路径。不过自己计算path的话可能在某些View中比较困难,且每当view的frame变化的时候你都需要去update shadow path。

猜你喜欢

转载自blog.csdn.net/liushihua147/article/details/78647531
今日推荐