引言
说起性能调优,我本来想说很多,但是觉得枯燥的语言并不能表达我对于性能调优的重视,所以我打算举个不太恰当的例子。
曾经有这么一家工厂,他们的产品从原材料到出厂一共需要经过30条产品线。
这家公司的老板对底下的员工曾多次强调过质量的重要性,员工们却不以为然,他们觉得自己做的都很不错。
有一次,老板去视察,走到了一条产品线上,问起工人,这条产品线的合格率能达到多少,那个工人自豪的说,"98%!"。老板却并没有十分满意,摇着头走开了。
那位工人非常的疑惑。这么高的合格率,老板为什么还是不满意呢?
于是这位工人就去向领导打听,领导也不知道,于是后来这位工人直接找到了老板家里,向老板请教。
老板很是诧异,于是就给这位工人简单科普了一下,一条产品线的合格率是98%,并不代表最后总的合格率是98%。应该是这样计算最后的合格率的:
x=0.98?0.98?0.98...=0.9830=0.545
所以,这家公司的生产线的合格率实际上只有54.5%。
这个故事说明的问题其实很简单,我们开发的app的最终性能,都是由每一个细小的部分组合而成的,其中很多部分的内容是进行的乘法,而不是加减,所以想要自己设计的app让用户用的爽,任何一个小的性能问题我们都不能忽略。
性能调优的方式:
1、通过专门的性能调优工具
2、通过优化代码
1. 性能调优工具:
下面针对iOS的性能调优工具进行一个介绍:
1.1 静态分析工具–Analyze
相信IOS开发者在App进行Build或Archive时,会产生很多编译警告,这些警告是编译时产生的,静态分析的过程也类似,在XCode Product菜单下,点击Analyze对App进行静态分析。
Analyze主要分析以下四种问题:
1. 逻辑错误:访问空指针或未初始化的变量等;
2. 内存管理错误:如内存泄漏等;
3. 声明错误:从未使用过的变量;
4. Api调用错误:未包含使用的库和框架。
1.2 内存泄漏分析工具–Leaks
点击XCode的Product菜单Profile启动Instruments,使用Leaks开始动态分析。
选择Leaks,会自动启动Leaks工具和IOS模拟器,Leaks启动后会开始录制,随着对模拟器运行的App的操作,可以在Leaks中查看内存占用的情况。
注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的。
Leaks顶部分为两栏:Allocations和Leaks,右侧的曲线代表内存分配和内存泄漏曲线。
点击第二栏Leaks,进行内存泄漏分析,左下角会出现Leaks调试的选项:
建议把Snapshot Interval间隔时间设置为10秒,勾选Automatic Snapshotting,Leaks会自动进行内存捕捉分析。
在你怀疑有内存泄漏的操作前和操作后,可以点击Snapshot Now进行手动捕捉。
以下是切换到我的App中调用 +(UIImage*)getSubImage:(unsigned long)ulUserHeader
函数的视图,可以发现内存泄漏:
Leaked Object的表格中显示了内存泄漏的类型、数量及内存空间。
点击具体的某个内存泄漏对象,在右侧Detail窗口中会出现导致泄漏可能的位置,其中黑色头像代表了最可能的位置。
Leaks已成功找出了[CMTool getSubImage:]这个函数:
内存泄漏动态分析技巧
熟练使用Leaks后会对内存泄漏判断更准确,在可能导致泄漏的操作里,多使用Snapshot Now手动捕捉。
开始时如果设备性能较好,可以把自动捕捉间隔设置为5秒钟。
使用ARC的项目,一般内存泄漏都是malloc、自定义结构、资源引起的,多注意这些地方进行分析。
开启ARC后,内存泄漏的原因
开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。
在IOS 中使用malloc分配的内存,ARC是不会处理的,需要自己进行处理。
例子中的 CGImageRef 也是一个Image的指针,ARC也不会进行处理。
1.3 不合理内存分析工具–Allocation
关于内存的问题,除了内存泄漏以外,还可能存在内存不合理使用的情况,也会导致IOS内存警告。
内存的不合理使用往往比内存泄漏更难发现,内存泄漏可以更多借助于工具的判断,而内存的不合理运用更多需要开发者结合代码、架构来进行分析。
明确说明一下两者的区别:
内存泄漏:是指内存被分配了,但程序中已经没有指向该内存的指针,导致该内存无法被释放,产生内存泄漏。
内存不合理运用:苹果官方称这种情况为Abandoned Memory,也就是存在已分配内存的引用,但实际上程序中不会使用,比如图片等对象加入了缓存,但缓存中的对象一直没有被使用。
XCode提供的Instruments中的Allocation工具可以用来帮你了解内存的分配情况,当你的App收到内存警告时,首先应该用Allocation进行内存分析,了解哪些对象占用了太多内存。
1.4 干掉僵尸对象–Zombies
僵尸对象,也就是我们会遇到的EXC_BAD_ACCESS错误,由于内存已经被释放,而这个对象仍旧保留这那个坏地址而导致的。
在MRC的开发中,这个错误比较常见,ARC下面在使用到C++的代码也会遇到。
不过这个工具比较简单,遇到这类错误,打开这个位于Instruments下的工具,直接就能帮你定位到,这里就不赘述了。
1.5 性能提升工具–Time Profile
这篇文章的重中之重!
既然是性能调优,那么怎么提升代码的运行效率其实才是我们程序员最直接的诉求,而这个工具可以辅助我们办到这件事。
Time Profiler分析原理: 它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。它将各个方法消耗的时间统计起来,形成了我们直接定位需要进行优化的代码的好帮手。
选择Time Profiler工具开始测试,这时会自动启动模拟器和Time Profiler录制。
先进行一些App的操作,让Time Profiler收集足够的数据,尤其是你觉得那些有性能瓶颈的地方。
4、5、6所标记的面板是需要关注的:
4是扩展面板,用来跟踪显示堆栈;5是详细地面板,可以从这里看到cpu运行的时间都消耗在哪里;6是选项面板,可以用来设置Time Profiler的运行参数。
通过对应用的操作,可以在详细面板中看到那些最耗时的操作是哪些,并可以逐行展开查看:
图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方,可以逐渐向下展开,找到产生的根本原因。
Time Profiler参数设置
这里边几个选项的含义如下:
- Separate by Thread: 每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的”重”线程
- Invert Call Tree: 从上倒下跟踪堆栈,这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中话费时间最深的方法.也就是说FuncA{FunB{FunC}} 勾选此项后堆栈以C->B-A 把调用层级最深的C显示在最外面
- Hide Missing Symbols: 如果dSYM无法找到你的app或者系统框架的话,那么表中看不到方法名只能看到十六进制的数值,如果勾线此项可以隐藏这些符号,便于简化数据
- Hide System Libraries: 勾选此项你会显示你app的代码,这是非常有用的. 因为通常你只关心cpu花在自己代码上的时间不是系统上的
- Show Obj-C Only: 只显示oc代码 ,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的
- Flatten Recursion: 递归函数, 每个堆栈跟踪一个条目
- Top Functions: 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。
上面的参数在实践中合理设置,也没有什么太多技巧,就是通过数据的隐藏、显示让我们更关注于想找到的数据。
2. 性能调优的代码优化:
下面介绍一下在开发中可以直接进行的代码优化的方面。
2.1 views设置为不透明(opaque=yes)
(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES, 渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。
如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。
给你们看个图,你们就知道了,如果这个属性为NO,那么:
知道了吧,GPU会利用图层颜色合成公式去合成真正的色值,这在ScrollView或者动画中是很消耗性能的。
2.2 不要阻塞主线程
永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。
一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。
大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。
通常建议这些操作都使用GCD的方式直接异步执行,并将UI相关操作在主线程进行回调,像这样:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 切换到全局队列,异步执行耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
// 切换到主线程,更新你的UI。
});
});
2.3 提前调整ImageView中的图片大小(同图片和动画的渲染)
如果要在UIImageView
中显示一个图片,你应保证图片的大小和UIImageView的大小相同。
因为在运行中缩放图片是很耗费资源的,特别是UIImageView
嵌套在UIScrollView
中的情况下。
如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。
这个类比到图片和动画的渲染中,是通用的。
具体方法参考上面的GCD操作。
2.4 正确使用容器的特性
- Arrays: 有序的一组值。使用index来查找很快,使用value 查找很慢, 插入/删除很慢。
- Dictionaries: 存储键值对。 用键来查找比较快。
- Sets: 无序的一组值。用值来查找很快,插入/删除很快。
2.5 大文件传输使用gzip
大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。
减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
当然,现在苹果已经自动支持了,你只需要告诉你们服务端的同学,传输大文件的时候记得用gzip就完了。
2.6 View的重用和懒加载
更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
重用就是模仿UITableView
和UICollectionView
的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
当需要使用View的时候,去可重用队列里面找一找有没有可以被复用的View。
这里我的一份框架中曾经使用过类似的方法去创建一个图片浏览器,大家可以稍做参考。View的重用
懒加载就是在程序启动时并不进行加载,只有当用到这个对象的时候,才进行加载。
这个不仅在属性中可以进行这样的使用,在View上面也是一样,不过实现稍有不同。
懒加载会消耗更少内存,但是在View的显示上会稍有滞后。
2.7 Cache
一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。
我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。
NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。
下面是一个可用的代码段,你可以可以用它去为一个基本不会改变的图片创建一个NSURLRequest并缓存它:
+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; // this will make sure the request always returns the cached image
request.HTTPShouldHandleCookies = NO;
request.HTTPShouldUsePipelining = YES;
[request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
return request;
}
注意你可以通过 NSURLConnection 获取一个URL request, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。
如果想了解更多关于HTTP caching, NSURLCache, NSURLConnection的相关知识,可以读下这篇文章。
如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。
NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。 Mattt Thompson有一篇很棒的关于它的文章::http://nshipster.com/nscache/
2.8 记得处理内存警告
一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:
如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.
幸运的是,UIKit提供了几种收集低内存警告的方法:
- 在app delegate中使用
applicationDidReceiveMemoryWarning:
的方法 - 在你的自定义UIViewController的子类(subclass)中覆盖
didReceiveMemoryWarning
- 注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知
一旦收到这类通知,你就需要释放任何不必要的内存使用。
例如,UIViewController的默认行为是移除一些不可见的view, 它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。
这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。
然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。
2.9 重用大的开销对象
这里的大开销是指一些初始化很慢的objects,如:NSDateFormatter和NSCalendar。但是,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。
想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。
下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:
// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;
// inside the implementation (.m)
// When you need, just use self.formatter
- (NSDateFormatter *)formatter {
if (! _formatter) {
_formatter = [[NSDateFormatter alloc] init];
_formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy"; // twitter date format
}
return _formatter;
}
还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。
2.10 避免反复的处理数据
许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。
比如你需要数据来展示一个table view,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。
类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。
2.11 正确设定背景图片
在View里放背景图片就像很多其它iOS编程一样有很多方法:
- 使用UIColor的 colorWithPatternImage来设置背景色;
- 在view中添加一个UIImageView作为一个子View。
如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存:
// You could also achieve the same result in Interface Builder
UIImageView *backgroundView = [ [UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.view addSubview:backgroundView];
如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
2.12 试试苹果最新的WKWebView来处理web
UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。
但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation 为特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要调整下你的HTML了。第一件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。
另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。
最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。
当然,上面是针对你在使用UIWebView的情况下,需要尽量减少使用web的特性,而苹果最近刚推出的Safari的底层框架WKWebView也许能帮我们规避掉很多这样的性能问题。
2.13 优化你的TableView
Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。
为了保证table view平滑滚动,确保你采取了以下的措施:
- 正确使用
reuseIdentifier
来重用cells - 尽量使所有的view opaque,包括cell自身
- 避免渐变,图片缩放,后台选人
- 缓存行高
- 如果cell内现实的内容来自web,使用异步加载,缓存请求结果
- 使用
shadowPath
来画阴影 - 减少subviews的数量
- 尽量不适用
cellForRowAtIndexPath:
,如果你需要用到它,只用一次然后缓存结果 - 使用正确的数据结构来存储数据
- 使用
rowHeight
,sectionFooterHeight
和sectionHeaderHeight
来设定固定的高,不要请求delegate
2.14 选择正确的数据存储方式
当存储大块数据时你会怎么做?
你有很多选择,比如:
- 使用
NSUerDefaults
- 使用XML, JSON, 或者 plist
- 使用NSCoding存档
- 使用类似SQLite的本地SQL数据库
- 使用 Core Data
NSUserDefaults的问题是什么?虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了
XML这种结构化档案呢?总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。
NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。
在这种应用场景下,使用SQLite 或者 Core Data比较好。使用这些技术你用特定的查询语句就能只加载你需要的对象。
在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graph model,但SQLite就是一个DBMS。Apple在一般情况下建议使用Core Data,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。
如果你使用SQLite,你可以用FMDB(https://github.com/ccgus/fmdb)这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。
2.15 把Xib换成Storyboard吧
当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。
Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.
当加载XIB时,所有图片都被缓存,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:
当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage 的`imageNamed:`方法来获取图片资源。
很明显,同样的事情也发生在storyboards中,但我并没有找到任何支持这个结论的文档。
另外,快速打开app是很重要的,特别是用户第一次打开它时,对app来讲,第一印象太太太重要了。
你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据,解析数据。
还是那句话,避免过于庞大的XIB,因为他们是在主线程上加载的。所以尽量使用没有这个问题的Storyboards吧!
注意,用Xcode debug时watchdog并不运行,一定要把设备从Xcode断开来测试启动速度
2.16 学会手动创建Autorelease Pool
NSAutoreleasePool
负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。
假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。
好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:
NSArray *urls = [@"url1",@"url2"];
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL: url encoding: NSUTF8StringEncoding error: &error];
/* Process the string, creating and autoreleasing more objects. */
}
}
这段代码在每次遍历后释放所有autorelease对象