iOS Core Animation Advanced Techniques-图层性能优化
上十一章节:
这篇随笔主要介绍有关图层性能优化。
CPU VS GPU:
- 绘图与动画有两种方式:
1.CPU(中央处理器)
2.GPU(图形处理器)
-
- 在图像处理上,正常GPU比CPU更高效,但GPU旦资源用完的话,性能就会开始下降了(即使CPU并没有完全占用)
- 大多数动画性能优化都是关于智能利用GPU和CPU,使得它们都不会超出负荷。
渲染服务:
- 动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。
运行一段动画,分四大阶段:
- 1.布局
- 准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段
- 2.显示
- 图层的寄宿图被绘制阶段,绘制有可能涉及-drawRect:和-drawLayer:inContext:方法的调用路径。
- 3.准备
- Core Animation准备发送动画数据到渲染服务的阶段
- 4.提交
- 最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
- 一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层树。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:
- 5.对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
- 6.在屏幕上渲染可见的三角形
- 5·6阶段 在动画过程中不停重复
- 1-5阶段 在软件层面处理(通过CPU)
- 6阶段 在GPU执行
- 而且只能控制1·2阶段:
- 布局与显示
- 因此可以:
- 在布局和显示阶段,决定哪些由CPU执行,哪些交给GPU去做
GPU相关操作:
- 大多数CALayer的属性都是用GPU来绘制,比如:
- 图层背景或者边框的颜色
- 会降低基于GPU图层绘制的事情:
- 1.太多的几何结构
- 图层在显示之前通过IPC发送到渲染服务器的时候,太多的图层就会引起CPU的瓶颈,限制了一次展示的图层个数
- 2.重绘
- 主要由重叠的半透明图层引起
- 3.离屏绘制
- 发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候
- 为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能
- 特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。
- 4.过大的图片
- 视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能
- 1.太多的几何结构
CPU相关的操作:
- CPU都发生在动画开始之前,不会影响到帧率,但是他会延迟动画开始的时间,让你的界面看起来会比较迟钝。
- 会延迟CPU让动画开始的时间的事情:
- 1.布局计算
- 视图层级过于复杂,特别是使用iOS6的自动布局机制尤为明显
- 2.视图惰性加载
- 3.Core Graphics绘制
- 对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,在绘制任何东西之前都会产生一个巨大的性能开销。
- 为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。
- 一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。Core Graphics绘制就会变得十分缓慢
- 4.解压图片
- 1.布局计算
IO相关操作:
- IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。
- 示范例子:
- //创建一个简单的显示模拟联系人姓名和头像列表的应用
- //实时加载图片,而不是用–imageNamed:预加载
- //添加一些图层阴影来使得列表显示得更真实
- #import "ViewController.h"
- #import <QuartzCore/QuartzCore.h>
- @interface ViewController () <UITableViewDataSource>
- @property (nonatomic, strong) NSArray *items;
- @property (nonatomic, weak) IBOutlet UITableView *tableView;
- @end
- @implementation ViewController
- - (NSString *)randomName
- {
- NSArray *first = @[@"Alice", @"Bob", @"Bill", @"Charles", @"Dan", @"Dave", @"Ethan", @"Frank"];
- NSArray *last = @[@"Appleseed", @"Bandicoot", @"Caravan", @"Dabble", @"Ernest", @"Fortune"];
- NSUInteger index1 = (rand()/(double)INT_MAX) * [first count];
- NSUInteger index2 = (rand()/(double)INT_MAX) * [last count];
- return [NSString stringWithFormat:@"%@ %@", first[index1], last[index2]];
- }
- - (NSString *)randomAvatar
- {
- NSArray *images = @[@"Snowman", @"Igloo", @"Cone", @"Spaceship", @"Anchor", @"Key"];
- NSUInteger index = (rand()/(double)INT_MAX) * [images count];
- return images[index];
- }
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- //set up data
- NSMutableArray *array = [NSMutableArray array];
- for (int i = 0; i < 1000; i++) {
- //add name
- [array addObject:@{@"name": [self randomName], @"image": [self randomAvatar]}];
- }
- self.items = array;
- //register cell class
- [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
- }
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- {
- return [self.items count];
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- //dequeue cell
- UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
- //load image
- NSDictionary *item = self.items[indexPath.row];
- NSString *filePath = [[NSBundle mainBundle] pathForResource:item[@"image"] ofType:@"png"];
- //set image and text
- cell.imageView.image = [UIImage imageWithContentsOfFile:filePath];//实时加载图片
- cell.textLabel.text = item[@"name"];
- //set image shadow
- cell.imageView.layer.shadowOffset = CGSizeMake(0, 5);
- cell.imageView.layer.shadowOpacity = 0.75;
- cell.clipsToBounds = YES;
- //set text shadow
- cell.textLabel.backgroundColor = [UIColor clearColor];
- cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
- cell.textLabel.layer.shadowOpacity = 0.5;
- return cell;
- }
- /*
- 以上代码运行时滑动经测试工具发现帧率只有14FPS
- 仅凭直觉,我们猜测性能瓶颈应该在图片加载。于是想使用GCD异步加载图片,然后缓存。。。
- 实则通过Instruments工具分析:
- Time Profiler工具(查看各项任务使用CPU率的工具):
- -tableView:cellForRowAtIndexPath:中的CPU时间总利用率只有~28%(也就是加载头像图片的地方),非常低。
- 于是建议是CPU/IO并不是真正的限制因素。
- OpenGL ES Driver工具(检测GPU利用率的工具):
- 渲染服务利用率的值达到51%和63%,很高。
- 看起来GPU需要做很多工作来渲染联系人列表。
- Core Animation调试工具选项(检查屏幕):
- Color Blended Layers选项(混合绘制检测):
- 屏幕中所有红色的部分都意味着字符标签视图的高级别混合
- 这很正常,因为我们把背景设置成了透明色来显示阴影效果。(设置透明属性会降低GPU性能)
- 这就解释了为什么渲染利用率这么高了。
- Color Offscreen-Rendered Yellow选项(离屏绘制检测):
- 黄色区域表示离屏绘制,发现所有的表格单元内容都在离屏绘制。
- 是因为我们给图片和标签视图添加的阴影效果
- 去掉代码中的阴影后发现帧率升到56 FPS,滑动流畅了
- Time Profiler工具(查看各项任务使用CPU率的工具):
- */
- 如何保持阴影效果而不影响性能呢?
- 由于每一行的字符和头像在每一帧刷新的时候并不需要改变,因此UITableViewCell的图层非常适合做缓存。
- 我们可以使用shouldRasterize来缓存图层内容。这将会让图层离屏之后渲染一次然后把结果保存起来,直到下次利用的时候去更新
- 示范例子://使用shouldRasterize提高性能
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- //dequeue cell
- UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"
- forIndexPath:indexPath];
- ...
- //set text shadow
- cell.textLabel.backgroundColor = [UIColor clearColor];
- cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
- cell.textLabel.layer.shadowOpacity = 0.5;
- //rasterize
- cell.layer.shouldRasterize = YES;//重点
- cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
- return cell;
- }
- /*
- 我们仍然离屏绘制图层内容,但是由于显式地禁用了栅格化,Core Animation就对绘图缓存了结果,于是对提高了性能。
- 通过Core Animation工具
- Color Hits Green and Misses Red选项:
- 绿色区域代表离屏绘制,大部分为绿色,
- 红色代表非离屏绘制,当滑动的时候屏幕闪烁成红色
- 所以我们最初的设想是错的。图片的加载并不是真正的瓶颈所在,而且试图把它置于一个复杂的多线程加载和缓存的实现都将是徒劳。
- 所以在动手修复之前验证问题所在是个很好的习惯!
- */