iOS Core Animation Advanced Techniques-图层性能优化

上十一章节:

  1. 图层树
  2. 图层的寄宿图
  3. 图层几何学
  4. 图层视觉效果
  5. 图层变换
  6. 专用图层
  7. 隐式动画
  8. 显式动画
  9. 图层时间
  10. 图层缓冲
  11. 基于定时器的动画

这篇随笔主要介绍有关图层性能优化。

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在图层每次显示之前对图片预处理,同样也会降低性能

CPU相关的操作:

  • CPU都发生在动画开始之前,不会影响到帧率,但是他会延迟动画开始的时间,让你的界面看起来会比较迟钝。
  • 会延迟CPU让动画开始的时间的事情:
    • 1.布局计算
      • 视图层级过于复杂,特别是使用iOS6的自动布局机制尤为明显
    • 2.视图惰性加载
    • 3.Core Graphics绘制
      • 对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,在绘制任何东西之前都会产生一个巨大的性能开销。
      • 为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。
      • 一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。Core Graphics绘制就会变得十分缓慢
    • 4.解压图片

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,滑动流畅了
    • */
  • 如何保持阴影效果而不影响性能呢?
    • 由于每一行的字符和头像在每一帧刷新的时候并不需要改变,因此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选项:
    • 绿色区域代表离屏绘制,大部分为绿色,
    • 红色代表非离屏绘制,当滑动的时候屏幕闪烁成红色
    • 所以我们最初的设想是错的。图片的加载并不是真正的瓶颈所在,而且试图把它置于一个复杂的多线程加载和缓存的实现都将是徒劳。
    • 所以在动手修复之前验证问题所在是个很好的习惯!
    • */

 

posted @ 2016-03-12 23:17  Jk_Chan  阅读(211)  评论(0编辑  收藏  举报