iOS之性能优化

来源:互联网 发布:淘宝达人申请大v认证 编辑:程序博客网 时间:2024/05/22 03:08
要提一下,“过早的优化是万恶之源”,在需求未定,性能问题不明显时,没必要尝试做优化,而要尽量正确的实现功能。做性能优化时,也最好是走修改代码 -> Profile -> 修改代码这样一个流程,优先解决最值得优化的地方。

入门级(这是些你一定会经常用在你app开发中的建议)

- 在正确的地方使用reuseIdentifier

一个开发中常见的错误就是没有给UITableViewCells, UICollectionViewCells,甚至UITableViewHeaderFooterViews设正确的reuseIdentifier。为了性能最优化,table view用 tableView:cellForRowAtIndexPath: 为rows分配cells的时候,它的数据应该重用自UITableViewCell。一个table view维持一个队列的数据可重用的UITableViewCell对象。 不使用reuseIdentifier的话,每显示一行table view就不得不设置全新的cell。这对性能的影响可是相当大的,尤其会使app的滚动体验大打折扣。
自iOS6起,除了UICollectionView的cells和补充views,你也应该在header和footer views中使用reuseIdentifiers。 想要使用reuseIdentifiers的话,在一个table view中添加一个新的cell时在data source object中添 加这个方法:

static NSString *CellIdentifier = @"Cell"; UITableViewCell  * cell  =  [tableView  dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];  

这个方法把那些已经存在的cell从队列中排除,或者在必要时使用先前注册的nib或者class创造新的cell。如果没有可重用的cell,你也没有注册一个class或者nib的话,这个方法返回nil。

- 尽可能使Views透明

UIView的alpha、hidden和opaque属性之间的关系和区别;
(1)、alpha
液晶显示器是由一个个的像素点组成的,每个像素点都可以显示一个由RGBA颜色空间组成的一种色值。其中的A就表示透明度alpha,UIView中alpha是一个浮点值,取值范围0~1.0,表示从完全透明到完全不透明。
当把alpha的值设置成0以后:
1、当前的UIView和subview都会被隐藏,而不管subview的alpha值为多少。
2、当前UIView会从响应者链中移除,而响应者链中的下一个会成为第一响应者
alpha的默认值是1.0。另外,更改alpha值时,默认是有动画效果的,这是因为图层在Cocoa中是由Core Animation中CALayer表示的,该动画效果是CALayer的隐含动画。当然也有办法禁用此动画效果,在这就不多述了,感兴趣的同学可以继续关注后续的博客。
(2)、hidden
该属性为BOOL值,用来表示UIView是否隐藏,默认值是NO。
当值设为YES时:
1、当前的UIView和subview都会被隐藏,而不管subview的hidden值为多少。
2、当前UIView会从响应者链中移除,而响应者链中的下一个会成为第一响应者
总之,同alpha为0时的显示效果相同。具体两者之间有什么区别就不清楚了,如果有知道的还望不吝赐教!
(3)、opaque
该属性为BOOL值,UIView的默认值是YES,但UIButton等子类的默认值都是NO。opaque表示当前UIView是否不透明,不过搞笑的是事实上它却决定不了当前UIView是不是不透明,比如你将opaque设为NO,该UIView照样是可见的(上文说过,是否可见是由alpha或hidden属性决定的),照理说为NO就表示透明,那就应该是不可见的呀?前面讲过,显示器中的每个像素点都可以显示一个由RGBA颜色空间组成的色值,比如有红色(上层的红色图层)和绿色(下层的绿色图层)两个交叉的图层色块,对于没有交叉的部分,即纯红色和绿色部分来说,对应位置的像素点只需要简单的显示红或绿,对应的RGBA为(1,0,0,1)和(0,1,0,1)就行了,负责图形显示的GPU需要很小的计算量就可以确定像素点对应的显示内容。
问题是红色和绿色还有相交的一块,其相交的颜色为黄色。这里的黄色是怎么来的呢?原来,GPU会通过图层一和图层二的颜色进行图层混合,计算出混合部分的颜色,最理想情况的计算公式如下:
R = S + D * ( 1 – Sa )
其中,R表示混合结果的颜色,S是源颜色(位于上层的红色图层一),D是目标颜色(位于下层的绿色图层二),Sa是源颜色的alpha值,即透明度。公式中所有的S和D颜色都假定已经预先乘以了他们的透明度。
知道图层混合的基本原理以后,再回到正题说说opaque属性的作用。当UIView的opaque属性被设为YES以后,按照上面的公式,也就是Sa的值为1,这个时候公式就变成了:
R = S
即不管D为什么,结果都一样。因此GPU将不会做任何的计算合成,不需要考虑它下方的任何东西(因为都被它遮挡住了),而是简单从这个层拷贝。这节省了GPU相当大的工作量。由此看来,opaque属性的真实用处是给绘图系统提供一个性能优化开关!
按照前面的逻辑,当opaque属性被设为YES时,GPU就不会再利用图层颜色合成公式去合成真正的色值。因此,如果opaque被设置成YES,而对应UIView的alpha属性不为1.0的时候,就会有不可预料的情况发生,这一点苹果在官方文档中有明确的说明:
An opaque view is expected to fill its bounds with entirely opaque content—that is, the content should have an alpha value of 1.0. If the view is opaque and either does not fill its bounds or contains wholly or partially transparent content,the results are unpredictable. You should always set the value of this property to NO if the view is fully or partially transparent.
大家切记!!!!
(4)、最后
当把UIView的alpha属性设成0,或者把hidden设成YES的时候,当前UIView和它所包含的子UIView都会变成不可见,同时也不会再响应event事件。注意这里是或的关系,即只要设置了其中的一个都会有此效果,而不管另外一个属性的值是什么。

- 避免庞大的XIB

iOS5中加入的Storyboards(分镜)正在快速取代XIB。然而XIB在一些场景中仍然很有用。比如你的app需要适应iOS5之前的设备,或者你有一个自定义的可重用的view,你就不可避免地要用到他们。 如果你不得不XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。 需要注意的是,当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller. 当家在XIB是,所有图片都被chache,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:
当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。在OS X中,图片和声音资源缓存在named cache中以便将来用到时获取。在iOS中,仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage 的imageNamed:方法来获取图片资源。

- 不要阻塞主线程

永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。
一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。这。。。正是在App Store中拿到一颗星的捷径大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。你可以使用NSURLConnection异步地做网络操作: + (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler 或者使用像AFNetworking这样的框架来异步地做这些操作。如果你需要做其它类型的需要耗费巨大资源的操作(比如时间敏感的计算或者存储读写)那就用Grand Central Dispatch,或者 NSOperation 和 NSOperationQueues. 下面代码是使用GCD的模板

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // switch to a background thread and perform your expensive operation    dispatch_async(dispatch_get_main_queue(), ^{  // switch back to the main thread to update your UI    }); }); 

发现代码中有一个嵌套的dispatch_async吗?这是因为任何UIKit相关的代码需要在主线程上进行。

- 在Image Views中调整图片大小

如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。

- 选择正确的Collection

一些常见collection的总结:
Arrays: 有序的一组值。使用index来lookup很快,使用value lookup很慢,插入/删除很慢。
Dictionaries: 存储键值对,用键来查找比较快。
Sets: 无序的一组值。用值来查找很快,插入/删除很快。

- 打开gzip压缩

大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。 问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。 减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。 好消息是,iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。

中级性能提升

你确信你已经掌握了前述那些基础级的优化方案了吗?但实际情况是,有时一些解决方案并不像那些一样明显,它们往往严重依赖于你如何架构和书写你的app。下面的这些建议就是针对这些场景的。

- 重用和延迟加载(lazy load) Views

更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。这里我们用到的技巧就是模仿UITableView和UICollectionView的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。创建views的能效问题也适用于你app的其它方面。想象一下一个用户点击一个按钮的时候需要呈现一个view
的场景。有两种实现方法:
- 创建并隐藏这个view当这个screen加载的时候,当需要时显示它;
- 当需要时才创建并展示。
每个方案都有其优缺点。 用第一种方案的话因为你需要一开始就创建一个view并保持它直到不再使用,这就会更加消耗内存。然而这也会使你的app操作更敏感因为当用户点击按钮的时候它只需要改变一下这个view的可见性。第二种方案则相反消耗更少内存,但是会在点击按钮的时候比第一种稍显卡顿。

- Cache, Cache, 还是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 获取一个URLrequest, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。 NSCache和NSDictionary
类似,不同的是系统回收内存的时候它会自动删掉它的内容。

- 权衡渲染方法

在iOS中可以有很多方法做出漂亮的按钮。你可以用整幅的图片,可调大小的图片,可以用CALayer,CoreGraphics甚至OpenGL来画它们。当然每个不同的解决方法都有不同的复杂程度和相应的性能。简单来说,就是用事先渲染好的图片更快一些,因为如此一来iOS就免去了创建一个图片再画东西上去然后显示在屏幕上的程序。问题是你需要把所有你需要用到的图片放到app的bundle里面,这样就增加了体积–这就是使用可变大小的图片更好的地方了: 你可以省去一些不必要的空间,也不需要再为不同的元素(比如按钮)来做不同的图。
然而,使用图片也意味着你失去了使用代码调整图片的机动性,你需要一遍又一遍不断地重做他们,这样就很浪费时间了,而且你如果要做一个动画效果,虽然每幅图只是一些细节的变化你就需要很多的图片造成bundle大小的不断增大。
总得来说,你需要权衡一下利弊,到底是要性能能还是要bundle保持合适的大小。

- 处理内存警告

一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述: 如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references. 幸运的是,UIKit
提供了几种收集低内存警告的方法:
1.在app delegate中使用applicationDidReceiveMemoryWarning: 的方法
2.在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning
3.注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知一旦收到这类通知,你就需要释放任何不必要的内存使用。
例如,UIViewController的默认行为是移除一些不可见的view,它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。

- 重用大开销对象

一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。 注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:

@property (nonatomic, strong) NSDateFormatter *formatter;  - (NSDateFormatter *)formatter { if(! _formatter) {          _formatter = [[NSDateFormatter alloc] init];          _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";    }  return _formatter; } 

还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。

- 选择正确的数据格式

从app和网络服务间传输数据有很多方案,最常见的就是JSON和XML。你需要选择对你的app来说最合适的一个。 解析JSON会比XML更快一些,JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization就更加方便使用了。但是XML也有XML的好处,比如使用SAX 来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。

- 正确设定背景图片

在View里放背景图片就像很多其它iOS编程一样有很多方法:
1.使用UIColor的 colorWithPatternImage来设置背景色;
2.在view中添加一个UIImageView作为一个子View。
如果你使用全画幅的背景图,你可以用application bundle的顶层文件夹寻找由供应的名字的图象。

UIImageView * imageView = [[UIImageView alloc] initWithImage: [UIImage imageNamed:@"icon.png"]];//会缓存图片(理论上)放在内存里作为cache的,这样图片会占用大量的内存。UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:@"icon.png"]];// 不会缓存图片,根据path去取图片所有比较节省内存,速度相对之前慢。

如果用来创建小的重复的图片作为背景的。这种情形下使用UIImageView
可以节约不少的内存,如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存: self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];

- 减少使用Web特性

UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation为特色的Webkit的Nitro Engine的限制。所以想要更高的性能你就要调整下你的HTML了。第一件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。 最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。更多相关信息:https://developer.apple.com/videos/wwdc2012/

- 优化Table View

Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。

为了保证table view平滑滚动,确保你采取了以下的措施:

正确使用`reuseIdentifier`来重用cells尽量使所有的view opaque,包括cell自身避免渐变,图片缩放,后台选人缓存行高如果cell内现实的内容来自web,使用异步加载,缓存请求结果使用`shadowPath`来画阴影减少subviews的数量尽量不适用`cellForRowAtIndexPath:`,如果你需要用到它,只用一次然后缓存结果使用正确的数据结构来存储数据使用`rowHeight`, `sectionFooterHeight` 和 `sectionHeaderHeight`来设定固定的高,不要请求delegate

- 选择正确的数据存储选项

当存储大块数据时你会怎么做?

你有很多选择,比如:

使用`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吧。

进阶性能提示

- 加速启动时间

快速打开app是很重要的,特别是用户第一次打开它时,对app来讲,第一印象太太太重要了。你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据,解析数据。还是那句话,避免过于庞大的XIB,因为他们是在主线程上加载的。所以尽量使用没有这个问题的Storyboards吧!注意,用Xcode debug时watchdog并不运行,一定要把设备从Xcode断开来测试启动速度

- 使用Autorelease Pool

NSAutoreleasePool负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:这段代码在每次遍历后释放所有autorelease对象

- 避免日期格式转换

如果你要用NSDateFormatter来处理很多日期格式,应该小心以待。就像先前提到的,任何时候重用NSDateFormatters都是一个好的实践。然而,如果你需要更多速度,那么直接用C是一个好的方案。Sam Soffes有一个不错的帖子(http://soff.es/how-to-drastically-improve-your-app-with-an-afternoon-and-instruments)里面有一些可以用来解析ISO-8601日期字符串的代码,简单重写一下就可以拿来用了。嗯,直接用C来搞,看起来不错了,但是你相信吗,我们还有更好的方案!如果你可以控制你所处理的日期格式,尽量选择Unix时间戳。你可以方便地从时间戳转换到NSDate:

0 0