@zhangyuhangk
2015-08-11T12:09:51.000000Z
字数 6058
阅读 2248
CoreAnimation
教程:https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques
这章讲的是Instruments工具的使用,估计教程是基于Xcode4讲的吧,界面有点不一样,只是大致知道个意思。
性能基本原则:不瞎猜,测试才是王道。(严重同意。没找到性能瓶颈之前,不做任何优化。)
另外,要在真机上测,而且要考虑到可支持的最低配的机子。
Instruments提供一套灰常强大的工具,可以测几乎任何想得到指标。其中每个工具的功能、界面和操作不尽相同,所以还得在实践中逐个掌握,没有捷径。
这里有篇很好的文章,用实例讲解了Time Profiler和Allocations两个工具。
以下为笔记:
功能:定时对每个函数的执行时间采样,以定位性能瓶颈(最消耗CPU的函数)。
适用于:CPU性能优化。
默认的采样间隔是1ms(可以调整)。常用选项:
Separate by Thread
: 区分线程。Invert Call Tree
: 反转调用树的显示(感觉用处不大的样子)。Hide Missing Symbols
: 隐藏dSYM文件里没有相关符号记录(比如第三方库)。Hide System Libraries
: 隐藏系统库的调用。同时勾上Hide Missing Symbols和Hide System Libraries的话,就只剩下用户代码了。所以一般都会勾上。Flatten Recursion
: 把递归调用统计为一次调用。Top Functions
: 统计函数的总时间。勾上时,函数时间包含其内的子函数调用的时间。比如A调用了B,那A的时间就是A自身代码的时间加B的时间。一般操作流程:
1. 在app上做一些使程序变卡的操作。
2. 在下面的detail panel里找到耗时最长的用户函数(有个人头图标的)。
3. 双击那一行,可以跳进代码,并且耗时最长的那段代码还会高亮。此时找到瓶颈了,接下来就开始优化。
4. 点右上角的Xcode图标,跳转到Xcode工程中。
5. 优化完代码再来一次上面的流程。
功能:用于检查内存的使用情况。
Swift的自动引用计数(ARC)保证了不会产生真正的内存泄漏,但是依然有两种情况会导致内存没有被释放。
有部分内存是系统和框架自己申请的,这个我们无法管理。但它会在出现memory warning的时候适当释放一些。所以当发现app内存居高不下的时候,可以点一下simulate memory warning。如果依然内存只下来一点点,就说明大部分内存还是用户代码申请的。
一般操作流程:
1. 在app上做正常操作,看timeline里面的内存占用是否只涨不降。
2. 在一个操作的前后都打上标记(点击Mark Generations)
3. detail面板会显示每两个标记之间的内存使用统计。其中的Persistent列显示了这段时间间隔内被申请但没有释放的内存。
4. 试试simulate memory warning
5. detail面板显示得比较乱,可以通过以下方式进行一定程度的筛选:
(1) 在detail面板的右上角的搜索栏输入工程名,可以筛选出本工程的类。但是如果泄漏的内存是String之类的内置类型,则看不到了。
(2) 选中All Heap Allocations(应该是只保留堆上分配的内存吧)
(3) 通过Growth排序。只关注泄漏最多的。
6. 至此,可以判断出泄漏的内存的类型。但是无法像Time Profiler一样,直接定位到具体的代码行。只能靠自己分析了。
(其它工具用到再慢慢加进来吧)
GPU绘图比CPU要快得多,所以要尽可能避免直接使用Core Graphics绘图,除非迫不得已。
一般而言,绘图的方式按照以下优先级做选择(越后面越慢)
1. 使用CALayer的各种子类(如CAShapeLayer等)。因为它们内部做了特殊的优化,可以利用上GPU的高效绘图。
2. 对CALayer的contents赋值。即使将同一个image赋给多个不同layer的contents,也不会导致多份位图的拷贝,它们内部会共享同一个位图。
3. 在CALayerDelegate的drawLayer:inContext:
方法或UIView的drawRect:
方法内用Core Graphics绘图。这就完全是CPU绘图了。
所以:
而必须使用Core Graphic绘图时:
setNeedsDisplayInRect:
。UIKit会把rect参数传到drawRect:
,并且超出这个范围的绘制也会被裁掉。此时可以在drawRect:
函数内进行一些判断,把跟这个rect毫无交集的绘制忽略(画了也不会显示)。drawLayer:inContext:
。将它的数量(不记得哪个属性了)设置为1,则可以简单地实现异步绘图。drawsAsynchronously
属性实现异步绘图。它的实现原理跟CATiledLayer不同。drawLayer:inContext:
会在主线程中被调用,但是绘图指令会被放入队列中,在后台进行真正的绘制。I/O相关的操作通常比较耗时,尤其是涉及网络的时候。在主线程上执行I/O操作往往会导致UI卡顿。解决方案就是异步加载。这个很好实现,利用GCD或NSOperationQueue就可以了。
但是图片还需要个解压的过程(PNG解压很快,JPEG就比较慢)。iOS在加载完图片之后不会马上解压(以减少内存占用),而会等到需要渲染图片的时候才解压。所以这个解压的动作还是会在主线程中进行。如果图片较大,这依然会导致UI卡顿。解决方案就是在后台线程加载完图片后,显式绘制到一个CGContext。
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect:imageView.bounds];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
这个方案的优点是:
只不过,Apple这样设计有它的用意,官方也不推荐手动地解压图片。所以只有解压成为性能瓶颈的时候才考虑做优化。
UIImage的init(named:)构造函数比init(contentsOfFile:)快。
init(contentsOfFile:)用于动态的图片,比如从网络加载的,更灵活。
而init(named:)仅限于bundle里的图片,工程在编译打包时做了适当的优化。它在载入时会一并解压,避免了上面的解压时卡顿的问题。一般情况下,我们只会在bundle里放一些图标之类的常用的小型资源,iOS会对init(named:)载入的图片做一个内存的cache。像UITableView上下滚动时,上面用到的icon反复加载也不会慢。但是这个cache的大小、释放策略都是无法干预的。如果在bundle里放一些大的图片,cache里的其它资源就更容易被释放,而导致加载icon变慢。所以一般大的图片,我们用init(contentsOfFile:),并且自己实现一个cache来管理。
图片动画小技巧:
对大尺寸的高清图片执行动画会比较耗时,而肉眼其实无法分辨一张在移动中的图片是否清晰。所以可以用一张低分辨率的图片代替,完成动画时再替换回原来的高清图片。
iOS提供的缓冲类。
1. 使用setObject:forKey:
和objectForKey:
来增删内容。
2. 使用setObject:forKey:cost
在添加数据时指定其大小。这个值影响资源释放的优先顺序。cost传0就相当于直接调用setObject:forKey:
3. 使用setCountLimit:
和setTotalCostLimit:
来指定cache的最大限制。
最常用的图片格式是PNG和JPEG。Apple官方推荐使用PNG,而实际测试的效果是JPEG很多情况下加载和解压的时间都比PNG短很多。相同分辨率下,JPEG也比PNG的文件尺寸小。但是当涉及半透明的时候,JPEG就无能为力了,因为它没有alpha通道。另外图片有太多细节的时候,JPEG算法也难以发挥太大作用。
有一种折衷的技术综合两者的优点,可以使图像质量接近PNG,且文件尺寸接近JPEG。这就叫Hybird Images。
基本原理是这样的:使用JPEG保存图片内容,再提供一张相同尺寸的PNG(其实不同尺寸也可以,系统会自动缩放)。用PNG作mask来与JPEG作混合运算达到最终效果。(所以其实是用时间换空间。)
以下是书中提供的代码(对API没完全理解,回头再自己做做实验吧)
//load color image
UIImage *image = [UIImage imageNamed:@"Snowman.jpg"];
//load mask image
UIImage *mask = [UIImage imageNamed:@"SnowmanMask.png"];
//convert mask to correct format
CGColorSpaceRef graySpace = CGColorSpaceCreateDeviceGray();
CGImageRef maskRef = CGImageCreateCopyWithColorSpace(mask.CGImage, graySpace);
CGColorSpaceRelease(graySpace);

//combine images
CGImageRef resultRef = CGImageCreateWithMask(image.CGImage, maskRef);
UIImage *result = [UIImage imageWithCGImage:resultRef];
CGImageRelease(resultRef);
CGImageRelease(maskRef);
github上有封装好的库:https://github.com/nicklockwood/JPNG
全称PowerVR Texture Compression,是一种图片格式(或者叫压缩算法)。可以绕过Core Animation直接使用OpenGL绘图,优点是可以利用GPU,速度很快,缺点是有大量的限制而且质量比较差。
使用PVRTC需要很复杂的代码才可以贴一张图,个人感觉很极端的情况下才会考虑这个方案,比如非常密集地绘图而又不太关心图片质量。这可能在游戏的复杂场景中才会出现,一般的app相信使用Core Animation就完全可以满足需求了。所以这里不深入学习。
修改layer的特定属性时,有可能会触发绘图动作。
shouldRasterize = true
)。对性能会有显著的提升,代价就是初次绘制会比较慢,而且会多占用一些内存。就是绘制到一个缓冲区,再绘制到layer上。使用以下技术时会导致离屏渲染:
cornerRadius
+ maskToBounds
与光栅化不同的是,这个缓冲区只是暂时的。不会长期占内存。
contentsCenter
和可拉伸的圆角矩形图片,可做出任意尺寸的圆角矩形layer,同时避免离屏渲染。甚至可以实现圆角矩形的阴影效果。shadowPath
可大幅提高阴影渲染性能。如非必要,尽量减少使用半透明效果。混合计算以及混合部分的重绘都会拖慢性能。设置opaque
和backgroundColor
可以避免混合。
比如一个UILabel,如果没有透明背景的需要,可以为其背景设置一个颜色,这样可以有效提高UILabel的渲染性能。
使用shouldRasterization
可以有效避免sublayers之间的混合运算。
Core Animation会自动过滤完全没显示的图层,但是是过滤之前必须计算frame。这依然有可能导致性能问题。可能的话,可以在创建图层的时候就判断此图层是否能显示,不能显示的直接不创建。
可以像UITableView那样建立回收机制,可以活动省去每次创建对象(比如layer)的成本。
如果某个图层包含大量的静态内容的子图层,可以考虑用Core Graphics在drawRect:里自己绘制。虽然Core Graphics比较慢,但如果当前的性能瓶颈在于图层数量的话,它反而能提高性能。
这种方法的前提是子图层足够简单(比如文本,几何图形),不然就得考虑用光栅化。
创建一个layer(及其sublayer),但不把它添加到图层树中(这样系统不会绘制它)。然后用Core Graphics取它的截图(调用renderInContext:和UIGraphicsGetImageFromCurrentImageContext),再赋给一个UIImageView或某个layer的contents。这样的效果很类似于光栅化,但不同的是,这个layer不需要放进图层树,也就不会增加layer的数量。
但缺点就是需要自己控制它的重绘(比如界面旋转了)