iOS核心动画高级技术(十二) 性能调优

Code should run as fast as necessary, but no faster. 代码应该运行的尽量快,而不是更快 - 理查德

在第一和第二部分,我们了解了Core Animation提供的关于绘制和动画的一些特 性。Core Animation功能和性能都非常强大,但如果你对背后的原理不清楚的话也 会降低效率。让它达到最优的状态是一门艺术。在这章中,我们将探究一些动画运 行慢的原因,以及如何去修复这些问题。

#CPU VS GPU 关于绘图和动画有两种处理的方式:CPU(中央处理器)GPU(图形处理器)。在现代iOS设备中,都有可以运行不同软件的可编程芯片,但是由于历史原因,我们可以说CPU所做的工作都在软件层面,而GPU在硬件层面。

总的来说,我们可以用软件(使用CPU)做任何事情,但是对于图像处理,通常 用硬件会更快,因为GPU使用图像对高度并行浮点运算做了优化。由于某些原因, 我们想尽可能把屏幕渲染的工作交给硬件去处理。问题在于GPU并没有无限制处理 性能,而且一旦资源用完的话,性能就会开始下降了(即使CPU并没有完全占用)

大多数动画性能优化都是关于智能利用GPU和CPU,使得它们都不会超出负荷。 于是我们首先需要知道Core Animation是如何在这两个处理器之间分配工作的。 #动画的舞台 Core Animation处在iOS的核心地位:应用内和应用间都会用到它。一个简单的动画可能同步显示多个app的内容,例如当在iPad上多个程序之间使用手势切换, 会使得多个程序同时显示在屏幕上。在一个特定的应用中用代码实现它是没有意义 的,因为在iOS中不可能实现这种效果(App都是被沙箱管理,不能访问别的视图)。

动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。 这个进程就是所谓的渲染服务。在iOS5和之前的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS6之后的版本中叫做 BackBoard 。

当运行一段动画时候,这个过程会被四个分离的阶段被打破:

  • 布局 - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景 色,边框等等)的阶段。
  • 显示 - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的 - drawRect: 和 -drawLayer:inContext: 方法的调用路径。
  • 准备 - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是 Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的 时间点。
  • 提交 - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过 IPC(内部处理通信)发送到渲染服务进行显示。

但是这些仅仅阶段仅仅发生在你的应用程序之内,在动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形 成另一个叫做渲染树的图层树(在第一章“图层树”中提到过)。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:

  • 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来 执行渲染
  • 在屏幕上渲染可见的三角形

所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。

这并不是个问题,因为在布局和显示阶段,你可以决定哪些由CPU执行,哪些交 给GPU去做。那么改如何判断呢? #GPU相关的操作 GPU为一个具体的任务做了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合然后把它们输送到屏幕上。现代iOS设备上可编程的GPU在这 些操作的执行上又很大的灵活性,但是Core Animation并没有暴露出直接的接口。 除非你想绕开Core Animation并编写你自己的OpenGL着色器,从根本上解决硬件加速的问题,那么剩下的所有都还是需要在CPU的软件层面上完成。

宽泛的说,大多数 CALayer 的属性都是用GPU来绘制。比如如果你设置图层背景或者边框的颜色,那么这些可以通过着色的三角板实时绘制出来。如果对一个 contents 属性设置一张图片,然后裁剪它 - 它就会被纹理的三角形绘制出来, 而不需要软件层面做任何绘制。

但是有一些事情会降低(基于GPU)图层绘制,比如:

  • 太多的几何结构 - 这发生在需要太多的三角板来做变换,以应对处理器的栅格 化的时候。现代iOS设备的图形芯片可以处理几百万个三角板,所以在Core Animation中几何结构并不是GPU的瓶颈所在。但由于图层在显示之前通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对 象),太多的图层就会引起CPU的瓶颈。这就限制了一次展示的图层个数(见 本章后续“CPU相关操作”)。
  • 重绘 - 主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比 率)是有限的,所以需要避免重绘(每一帧用相同的像素填充多次)的发生。 在现代iOS设备上,GPU都会应对重绘;即使是iPhone 3GS都可以处理高达 2.5的重绘比率,并任然保持60帧率的渲染(这意味着你可以绘制一个半的整屏的冗余信息,而不影响性能),并且新设备可以处理更多。
  • 离屏绘制 - 这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图 片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是要明白这会带来性能的负面影响。
  • 过大的图片 - 如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。

#CPU相关的操作

大多数工作在Core Animation的CPU都发生在动画开始之前。这意味着它不会影响到帧率,所以很好,但是他会延迟动画开始的时间,让你的界面看起来会比较迟钝。

以下CPU的操作都会延迟动画的开始时间:

  • 布局计算 - 如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤为明显,它应 该是比老版的自动调整逻辑加强了CPU的工作。
  • 视图懒加载 - iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操作”),都会比CPU正常操作慢得多。
  • Core Graphics绘制 - 如果对视图实现了-drawRect方法,或者 CALayerDelegate-drawLayer:inContext:方法,那么在绘制任何东 西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
  • 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同 于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片(14章“图片IO”会更详细讨论)。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用 UIImageView )或者把它绘制到 Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。

当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层级关系中有太多的图层,就会导致CPU没一帧的渲染,即使这些事情不是你的应用程序可控的。 #IO相关操作 还有一项没涉及的就是IO相关工作。上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。一些动画可能需要从闪存(甚至是远程URL)来加载。一个典型的例子就是两个视图控制器之间的过渡效果,这就需要从一个nib文件或者是它的内容中懒加载,或者一个旋转的图片,可能在内存中尺寸太大,需要动态滚动来加载。 IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。总的来说,这就需要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。这些技术将会在第14章中讨论。 #测量,而不是猜测 于是现在你知道有哪些点可能会影响动画性能,那该如何修复呢?好吧,其实不需要。有很多种诡计来优化动画,但如果盲目使用的话,可能会造成更多性能上的问题,而不是修复。

如何正确的测量而不是猜测这点很重要。根据性能相关的知识写出代码不同于仓促的优化。前者很好,后者实际上就是在浪费时间。

那该如何测量呢?第一步就是确保在真实环境下测试你的程序。 #真机测试,而不是模拟器 当你开始做一些性能方面的工作时,一定要在真机上测试,而不是模拟器。模拟器虽然是加快开发效率的一把利器,但它不能提供准确的真机性能参数。

模拟器运行在你的Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac 上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备 的GPU,这意味着GPU相关的操作在模拟器上运行的慢,尤其是使用 CAEAGLLayer 来写一些OpenGL的代码时候。

这就是说在模拟器上的测试出的性能会高度失真。如果动画在模拟器上运行流畅,可能在真机上十分糟糕。如果在模拟器上运行的很卡,也可能在真机上很平滑。你无法确定。

另一件重要的事情就是性能测试一定要用发布配置,而不是调试模式。因为当用发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。你也可以自己做到这些,例如在发布环境禁用NSLog语句。你只关心发布性能,那才是你需要测试的点。

最后,最好在你支持的设备中性能最差的设备上测试:如果基于iOS6开发,这意 味着最好在iPhone 3GS或者iPad2上测试。如果可能的话,测试不同的设备和iOS 版本,因为苹果在不同的iOS版本和设备中做了一些改变,这也可能影响到一些性 能。例如iPad3明显要在动画渲染上比iPad2慢很多,因为渲染4倍多的像素点(为 了支持视网膜显示)。

#保持一致的帧率 为了做到动画的平滑,你需要以60FPS(帧每秒)的速度运行,以同步屏幕刷新 速率。通过基于 NSTimer 或者 CADisplayLink 的动画你可以降低到30FPS,而且效果还不错,但是没办法通过Core Animation做到这点。如果不保持60FPS的速率,就可能随机丢帧,影响到体验。

你可以在使用的过程中明显感到有没有丢帧,但没办法通过肉眼来得到具体的数据,也没法知道你的做法有没有真的提高性能。你需要的是一系列精确的数据。

你可以在程序中用 CADisplayLink 来测量帧率(就像11章“基于定时器的动 画”中那样),然后在屏幕上显示出来,但应用内的FPS显示并不能够完全真实测量 出Core Animation性能,因为它仅仅测出应用内的帧率。我们知道很多动画都在应 用之外发生(在渲染服务器进程中处理),但同时应用内FPS计数的确可以对某些 性能问题提供参考,一旦找出一个问题的地方,你就需要得到更多精确详细的数据 来定位到问题所在。苹果提供了一个强大的Instruments工具集来帮我们做到这些。 #Instruments Instruments是Xcode套件中没有被充分利用的一个工具。很多iOS开发者从没用 过Instruments,或者只是用Leaks工具检测循环引用。实际上有很多Instruments工 具,包括为动画性能调优的东西。

你可以通过在菜单中选择Profile选项来打开Instruments(在这之前,记住要把目标设置成iOS设备,而不是模拟器)。然后将会显示出图12.1(如果没有看到所有选项,你可能设置成了模拟器选项)。

就像之前提到的那样,你应该始终将程序设置成发布选项。幸运的是,配置文件默认就是发布选项,所以你不需要在分析的时候调整编译策略。 我们将讨论如下几个工具:

  • 时间分析器 - 用来测量被方法/函数打断的CPU使用情况。
  • Core Animation - 用来调试各种Core Animation性能问题。
  • OpenGL ES驱动 - 用来调试GPU性能问题。这个工具在编写Open GL代码的时候很有用,但有时也用来处理Core Animation的工作。

Instruments的一个很棒的功能在于它可以创建我们自定义的工具集。除了你初始选择的工具之外,如果在Instruments中打开Library窗口,你可以拖拽别的工具到左侧边栏。我们将创建以上我们提到的三个工具,然后就可以并行使用了(见图 12.2)。

#时间分析器 时间分析器工具用来检测CPU的使用情况。它可以告诉我们程序中的哪个方法正 在消耗大量的CPU时间。使用大量的CPU并不一定是个问题 - 你可能期望动画路径对CPU非常依赖,因为动画往往是iOS设备中最苛刻的任务。

但是如果你有性能问题,查看CPU时间对于判断性能是不是和CPU相关,以及定 位到函数都很有帮助(见图12.3)。

时间分析器有一些选项来帮助我们定位到我们关心的的方法。可以使用左侧的复选框来打开。其中最有用的是如下几点:

  • 通过线程分离 - 这可以通过执行的线程进行分组。如果代码被多线程分离的 话,那么就可以判断到底是哪个线程造成了问题。
  • 隐藏系统库 - 可以隐藏所有苹果的框架代码,来帮助我们寻找哪一段代码造成 了性能瓶颈。由于我们不能优化框架方法,所以这对定位到我们能实际修复的 代码很有用。
  • 只显示Obj-C代码 - 隐藏除了Objective-C之外的所有代码。大多数内部的Core Animation代码都是用C或者C++函数,所以这对我们集中精力到我们代码中显 式调用的方法就很有用。

#Core Animation Core Animation工具用来监测Core Animation性能。它给我们提供了周期性的 FPS,并且考虑到了发生在程序之外的动画(见图12.4)。

Core Animation工具也提供了一系列复选框选项来帮助调试渲染瓶颈:

  • Color Blended Layers - 这个选项基于渲染程度对屏幕中的混合区域进行绿到 红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性 能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。
  • ColorHitsGreenandMissesRed - 当使用 shouldRasterizep 属性的时候, 耗时的图层绘制会被缓存,然后当做一个简单的扁平图片呈现。当缓存再生的 时候这个选项就用红色对栅格化图层进行了高亮。如果缓存频繁再生的话,就 意味着栅格化可能会有负面的性能影响了(更多关于使 用 shouldRasterize 的细节见第15章“图层性能”)。
  • Color Copied Images - 有时候寄宿图片的生成意味着Core Animation被强制 生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针。这个选 项把这些图片渲染成蓝色。复制图片对内存和CPU使用来说都是一项非常昂贵 的操作,所以应该尽可能的避免。
  • Color Immediately - 通常Core Animation Instruments以每毫秒10次的频率更 新图层调试颜色。对某些效果来说,这显然太慢了。这个选项就可以用来设置 每帧都更新(可能会影响到渲染性能,而且会导致帧率测量不准,所以不要一 直都设置它)。
  • Color Misaligned Images - 这里会高亮那些被缩放或者拉伸以及没有正确对 齐到像素边界的图片(也就是非整型坐标)。这些中的大多数通常都会导致图 片的不正常缩放,如果把一张大图当缩略图显示,或者不正确地模糊图像,那 么这个选项将会帮你识别出问题所在。
  • Color Offscreen-Rendered Yellow - 这里会把那些需要离屏渲染的图层高亮 成黄色。这些图层很可能需要用 shadowPath 或者 shouldRasterize 来优 化。
  • Color OpenGL Fast Path Blue - 这个选项会对任何直接使用OpenGL绘制的 图层进行高亮。如果仅仅使用UIKit或者Core Animation的API,那么不会有任 何效果。如果使用 GLKView 或者 CAEAGLLayer ,那如果不显示蓝色块的话 就意味着你正在强制CPU渲染额外的纹理,而不是绘制到屏幕。
  • Flash Updated Regions - 这个选项会对重绘的内容高亮成黄色(也就是任何 在软件层面使用Core Graphics绘制的图层)。这种绘图的速度很慢。如果频 繁发生这种情况的话,这意味着有一个隐藏的bug或者说通过增加缓存或者使 用替代方案会有提升性能的空间。

这些高亮图层的选项同样在iOS模拟器的调试菜单也可用(图12.5)。我们之前说过用模拟器测试性能并不好,但如果你能通过这些高亮选项识别出性能问题出在什么地方的话,那么使用iOS模拟器来验证问题是否解决也是比真机测试更有效的。

#OpenGL ES驱动 OpenGL ES驱动工具可以帮你测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animation那样显示FPS 的工具(图12.6)。

侧栏的邮编是一系列有用的工具。其中和Core Animation性能最相关的是如下几点:

  • Renderer Utilization - 如果这个值超过了~50%,就意味着你的动画可能对帧 率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。
  • Tiler Utilization - 如果这个值超过了~50%,就意味着你的动画可能限制于几 何结构方面,也就是在屏幕上有太多的图层占用了。

#一个可用的案例

现在我们已经对Instruments中动画性能工具非常熟悉了,那么可以用它在现实中解决一些实际问题。

我们创建一个简单的显示模拟联系人姓名和头像列表的应用。注意即使把头像图 片存在应用本地,为了使应用看起来更真实,我们分别实时加载图片,而不是用 – imageNamed: 预加载。同样添加一些图层阴影来使得列表显示得更真实。清单12.1 展示了最初版本的实现。

#import "ViewController.h" 
#import 
@interface ViewController ()  
@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"]; 
 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;
 //set image and text
 return cell; 
}
@end
复制代码

当快速滑动的时候就会非常卡(见图12.7的FPS计数器)。

仅凭直觉,我们猜测性能瓶颈应该在图片加载。我们实时从闪存加载图片,而且没有缓存,所以很可能是这个原因。我们可以用一些很赞的代码修复,然后使用 GCD异步加载图片,然后缓存。。。等一下,在开始编码之前,测试一下假设是否成立。首先用我们的三个Instruments工具分析一下程序来定位问题。我们推测问题 可能和图片加载相关,所以用Time Profiler工具来试试(图12.8)。

-tableView:cellForRowAtIndexPath:中的CPU时间总利用率只有~28% (也就是加载头像图片的地方),非常低。于是建议是CPU/IO并不是真正的限制 因素。然后看看是不是GPU的问题:在OpenGL ES Driver工具中检测GPU利用率 (图12.9)。

渲染服务利用率的值达到51%和63%。看起来GPU需要做很多工作来渲染联系人列表。

为什么GPU利用率这么高呢?我们来用Core Animation调试工具选项来检查屏 幕。首先打开Color Blended Layers(图12.10)。

屏幕中所有红色的部分都意味着字符标签视图的高级别混合,这很正常,因为我们把背景设置成了透明色来显示阴影效果。这就解释了为什么渲染利用率这么高了。

那么离屏绘制呢?打开Core Animation工具的Color Offscreen - Rendered Yellow 选项(图12.11)。

所有的表格单元内容都在离屏绘制。这一定是因为我们给图片和标签视图添加的阴影效果。在代码中禁用阴影,然后看下性能是否有提高(图12.12)。

问题解决了。干掉阴影之后,滑动很流畅。但是我们的联系人列表看起来没有之前好了。那如何保持阴影效果而且不会影响性能呢?

好吧,每一行的字符和头像在每一帧刷新的时候并不需要变,所以看起来的图层非常适合做缓存。我们可以使用来缓存图层内容。这将会让图层离屏之后渲染一次然后把结果保存起来,直到下次利用的时候去更新(见清单12.2)。

清单12.2 使用 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选项(图12.13)。

图12.13 Color Hits Green and Misses Red验证了缓存有效

结果和预期一致 - 大部分都是绿色,只有当滑动到屏幕上的时候会闪烁成红色。 因此,现在帧率更加平滑了。

所以我们最初的设想是错的。图片的加载并不是真正的瓶颈所在,而且试图把它置于一个复杂的多线程加载和缓存的实现都将是徒劳。所以在动手修复之前验证问题所在是个很好的习惯!

#总结 在这章中,我们学习了Core Animation是如何渲染,以及我们可能出现的瓶所在。你同样学习了如何使用Instruments来检测和修复性能问题。在下三章中,我们将对每个普通程序的性能陷阱进行详细讨论,然后学习如何修复。

你可能感兴趣的:(iOS核心动画高级技术(十二) 性能调优)