在 IOS 设备中,一个像素被描绘到屏幕上并显示出来是经过大量的计算跟操作的,而CPU
却是负责了计算这一部分,例如我们加载一张PNG图片,CPU
的工作就是加载并且解压。当然解压是要用到一系列的解压算法的,这里我们就暂且不说。CPU
将数据解压完后下一步操作就是要将数据传输到GPU
上,这时候如果是一些大型的纹理数据传输就会非常耗时了。当GPU
接收到数据之后,就会将每一个frame的 bitmap(位图)合成在一起(FPS 为60,就是屏幕一秒钟刷新60次,所以我们用到的 CADisplayLink
这个类为什么是事件 1/60 秒的原因了)。
那么,明白了CPU
和GPU
的关系后,我们就要就开始说一下如何优化我们的图形性能了。
1.设置图形的透明和不透明(CALayer下的opaque属性)
如果我们设置了两个视图重合在一起,那么GPU
就会计算他们重合的部分的像素,并将这些像数合并在一起 如果我们设置了视图为不透明的,目标像素等于原纹理了,这样GPU
就会忽略不透明视图下方的任何纹理了,不会为它所在部分进行合成了,因为它下方都被它挡住了,这为 GPU
解除了部分工作
view.layer.opaque = true;
当然我们在使用CoreGraphics
的 UIGraphicsBeginImageContextWithOptions(<#CGSize size#>, <#BOOL opaque#>, <#CGFloat scale#>)
方法的时候传入opaque
都为 ture,为GPU 来节省消耗。
2.离屏渲染
要讲离屏渲染的优化之前当然还得给大家讲讲什么是离屏渲染了
离屏渲染:离屏渲染为`GPU`渲染提供了另外一种方式,首先将屏幕外的渲染会合并并且计算等等一些列操作,
保存到一个新的缓冲区里面,然后由该缓冲区被渲染到屏幕上面。
所以当下次加载该视图的时候,`GPU` 只需要复用一下这个缓存, 那么`GPU`就不用重新把视图绘制,
这为 `GPU` 省下了不少压力了。
那么很明显,当我们的视图出现了屏幕外渲染并且需要复用的时候我们就需要用到离屏渲染了。
那么什么时候会发生屏幕外渲染?最常见的就是为视图的 layer 层设置蒙版(Mask) 或 圆角(cornerRadius)就会触发屏幕外渲染了。
对于圆角的设置,我们可以如下:
view.layer.cornerRadius = 3.0f;
view.layer.masksToBounds = true;
view.layer.shouldRasterize = true;
view.layer.rasterizationScale = [UIScreen mainScreen].scale;;
其中 shouldRasterize
就是为我们开启离屏渲染模式,但是rasterizationScale
又是什么呢?之前说过,离屏渲染是让缓存马上渲染,如果有缓存,当然还得有一个仓库了,那么这个仓库的空间肯定是有限的,所以我们要用rasterizationScale
尽量为把缓存降到最低。
所以说使用离屏渲染是有代价的,他只是为了不得不使用屏幕外渲染的情况下使用的一个优化手段,当然,这些视图是要重用的,不然的话离屏渲染就没有任何价值了,例如在 UITableView
中使用,才能把他的价值发挥到至极点。
以下附上两幅使用离屏渲染跟没有离屏渲染的区别,当然,你还得打开Instrument
的Core Animation
, 然后打开Color Offscreen-Rendered Yellow
和 Color Hits Green and Misses Red
来检查。以下是分别是没有离屏渲染和有离屏渲染的两幅图(以下两幅图是我开发工程的图片。)
其中黄色代表渲染到屏幕外缓冲区的区域,红色代表缓冲区被重新创建,而绿色代表无论何时一个屏幕外缓冲区被复用。
大家可以看出到个人头像是设置了圆角的,而图片中个人头像的显示是没有进行离屏渲染的颜色为黄色,有进行离屏渲染的颜色为红色了。这样我们滚动视图的时候,个人头像这一视图就会在屏幕外的缓冲区中拿到缓存重用,节省了 GPU 的消耗。
当然,你也可以直接使用异步绘制方法,直接生成一张圆角图片,这样的性能更好,那么异步绘制怎么做呢?请看IOS 图形性能优化锦集(2)。
3.尽量别在 tableView 上加载 JPEG 格式的图片
在开发当中很多时候都必须要 tableView 上加载图片,但是图片的格式对应用的性能也是有很大影响的,我们常见的图片格式分别为JPEG(我们常说的 JPG)或 PNG 格式。那么我们分别来说说两者有什么不一样。
`JPEG`:其实 `JPEG` 本身是一种算法,它使用一个基于`离散余弦变换 ` 的算法将图片中的某些肉眼无法识别
的元素去掉,并且通过`哈夫曼编码`的变种, 从而减少图片的大小。
这就是为什么我们用 Iphone 拍照后的照片有4M 左右,通过UIImageJPEGRepresentation(<#UIImage * image#>, <#CGFloat compressionQuality#>)
方法压缩质量后能压到几十 KB 但是又看不出明显变化的原因了。
当然有压缩当然也有解压,我们在加载 JPEG 图片的时候CPU 要忙于为图片解压会造成延迟显示图片,如果过多的在 tableView 上使用 JPEG 图片,会造成 CPU
压力过大的问题了。
`PNG`:`PNG` 不能想 `JPEG` 一样压缩图片,但是它的解码比 `JPEG` 却简单的多,所以用来在程序中展示原图
是一个非常好的选择。苹果在 IOS 的技术上对正常的 `PNG` 的 解压进行了优化,这就是苹果为什么推荐
开发者尽量使用 `PNG` 图片的原因了。
4.理性使用
-drawRect:
方法
大家或许感到奇怪,有不少开发者在发有关性能优化的博客当中指出使用-drawRect:
来优化性能。但是我这里不太建议大家未经思考的使用-drawRect:
方法。原因如下:
因为你使用UIImageView
在加载一个视图的时候,这个视图虽然依然有 CALayer
,但是却没有申请到一个后备的存储,取而代之的是使用一个使用屏幕外渲染,将 CGImageRef 作为内容,并用渲染服务将图片数据绘制到帧的缓冲区,就是显示到屏幕上,当我们滚动视图的时候,这个视图将会重新加载,浪费性能。所以对于使用-drawRect:
方法,更倾向于使用CALayer
来绘制图层。因为使用CALayer
的 -drawInContext:
,CoreAnimation
将会为这个图层申请一个后备存储,用来保存那些方法绘制进来的位图。那些方法内的代码将会运行在 CPU 上,结果将会被上传到 GPU。这样做的性能更为好些。
所以我们需要画图的时候,尽量少用`-drawRect:`方法,选用`CALayer`的`-drawInContext:`更为妥当。
5.少用自动布局
想了很久,其实不知道该不该写这一点,因为自从自动布局出来以后,很多的开发者为了追随苹果的脚步纷纷使用了自动布局。其实之前我也一样,苦苦的研究自动布局,然后在开发当中去策马奔腾,为此还用 C 语言来封装一套 autoLayout 的库来开发。但是自然有些东西别人说好,它并不真的好,当你真正试过了,试多了,你就会发现到它的缺点了。
使用 AutoLayout 布局确实很方便,但是,在 tableView 上使用 autoLayout 的话,当你遇上自适应布局,你会感觉到明显的卡顿。你使用的子视图越多,这一现象就会越明显,这是因为使用 AutoLayout 执行的解约束计算是非常多的,视图越多,计算量越大,当用户滑动的时候,每个 Cell 就会为布局进行疯狂的计算,这个对CPG
和 GPU
的折磨。
那么个人比较好的建议就是:
使用旧式的计算布局,在`tableView:heightForRowAtIndexPath:`方法里对高度进行计算和布局设置。
// 这个方法在服务端返回数据后设置数据后调用, item 为 cell 的数据源,所以以下 self 为 item。
- (void)p_configureFrame{
//头像
self.headViewF = CGRectMake(kMarginLeft,kMarginTop ,36, 36);
//等级
CGFloat levelWH = 19;
self.levelF = CGRectMake(CGRectGetMaxX(self.headViewF) - levelWH / 2., CGRectGetMaxY(self.headViewF) - levelWH, levelWH, levelWH);
//时间
CGSize dateSize = [self.date sizeWithAttributes:@{NSFontAttributeName:FSNFontWithSize(12)}];
self.dateLabelF = CGRectMake(SCREEN_WIDTH - dateSize.width - kMarginLeft, 0, dateSize.width, dateSize.height);
self.height = CGRectGetMaxY(self.dateLabelF);
}
然后我们可以直接在
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
BaseItem *item = self.dataSource[indexPath.section][indexPath.row];
return item.height;
}
// 因为我们在拿到服务端数据的时候就已经进行了高度计算了,所以item 的height属性早已有值,
// 这里直接返回,而这个方法是每个 cell 都要调用一次的方法,务必做到快速返回高度。
虽然说上面的方法没有用 AutoLayout 的方便,但是我想,你会为你的耐心而获得性能上巨大的回报。
@end
好了,今天先写这么多,接下来再慢慢跟大家分享。一年过去了,目前在想着过年的时候写一写年终总结,
说一说自己的得失,好让自己每年也有一个对比和反思。希望这些内容能为大家在 IOS 的海洋上有推波助澜的帮助吧,
感谢各位的支持,我会尽力在工作之余来更新我的博客,并为大家开放我自己写源码架构的,请继续关注我喔。
网友评论