iOS程序性能优化
一、初级
-
使用ARC进行内存管理
在iOS5发布的ARC,它解决了最常见的内存泄露问题。但是值得注意的是,ARC并不能避免所有的内存泄露。使用ARC之后,工程中可能还会有内存泄露,不过引起这 些内存泄露的主要原因是:block,retain循环,对CoreFoundation对象(通常是C结构)管理不善等。
-
在适当的情况下使用
reuseIdentifier
在使用
UITableView
和UICollectionView
时,其Cell和Header/Footer都会涉及到复用问题,系统提供了复用的方法,但需要设置其reuseIdentifier
。// 复用 Cell:
- [UITableView dequeueReusableCellWithIdentifier:];
- [UITableView registerNib:forCellReuseIdentifier:];
- [UITableView registerClass:forCellReuseIdentifier:];
- [UITableView dequeueReusableCellWithIdentifier:forIndexPath:];
// 复用 Section 的 Header/Footer:
- [UITableView registerNib:forHeaderFooterViewReuseIdentifier:];
- [UITableView registerClass:forHeaderFooterViewReuseIdentifier:];
- [UITableView dequeueReusableHeaderFooterViewWithIdentifier:];其原理是:如果tableView维护的UITableViewCell队列或列表中有可用的cell,则从队列从移除一个已经存在的cell,如果没有可以重用的cell的话,就从之前注册的nib文件或类中创建一个新的cell。
值得注意的是:因为涉及到复用问题,当一个 Cell 被拿来复用时,它所有被设置的属性(包括样式和内容)都会被拿来复用,如果刚好某一个的 Cell你没有显式地设置它的属性,那么它这些属性就直接复用别的 Cell 的了。处理方法一:设置 Cell的存在差异性的那些属性(包括样式和内容)时,有了 if 最好就要有 else,要显式的覆盖所有可能性。处理方法二:设置 Cell 的存在差异性的那些属性时,代码要放在初始化代码块的外部。
-
尽可能将View设置为不透明(Opaque)
如果view是不透明的,那么应该将其
opaque
属性设置为YES,这样设置可以让系统以最优的方式来绘制view。这个性能问题尤其体现在:如果view是嵌入到scroll view中的,或者是复杂动画的一部分。 -
避免臃肿的XIB
苹果官方文档称:当加载XIB时,所有涉及到的图片都将被缓存,并且如果是开发的 程序是针对OS X的话,声音文件也会被加载。所以如果有一个view还不立即使用的话,就会造成内存的浪费。而这在
storyboard
中是不会发生的,因为storyboard还在需要的时候才实例化一个view controller。 -
不要阻塞主线程
永远都不要在主线程做繁重的任务。因为UIKit的任务都在主线程中进行,例如绘制、触摸管理和输入响应。如果你需要做一些其它类型开销很大的操作,那么就使用
GCD
(Grand Central Dispatch,推荐使用),或NSOperations
和NSOperationQueues
。 -
让图片的大小跟UIImageView一样
图片的缩放非常耗费资源,特别是将UIImageView嵌入到UIScro llView中。注意:
对于网络图片,当下载好图片后,手动进行图片的缩放(最好是在后台线程中),然后再在UIImageView中使用缩放过的图片。 -
选择合适的集合
Array:数组。有序的,通过 index 查找很快,通过 value 查找很慢,插入和删除较慢。
Dictionary:字典。存储键值对,通过键查找很快。
Set:集合。无序的,通过 value 查找很快,插入和删除较快。 -
使用GZIP压缩
使用GZIP对网络传输中的数据进行压 缩,这样可以减小文件的大小,并加快下载的速度。压缩对于文 本数据特别有用,因为文本具有很高的压缩比。
NSURLCo nnection
默认情况下已经支持GZIP压缩。
二、中级
-
View的复用和懒加载机制
当你的程序中需要展示很多的View的时候,这就意味着需要更多的CPU处理时间和内存空间,尤其体现在使用
IScrollView
来装载和呈现界面。处理方法一:运用懒加载机制,不要一次性把所有的 subviews 都创建出来,而是在你需要他们的时候创建,并且用复用机制去复用他们。减少内存分配的开销,节省内存空间。
处理方法二:在当前界面第一次加载的时候就创建出这个 View,只是把它隐藏起来,当你需要它的时候,只用显示它就行了。比较占用内存,但只改变了其hidden属性,所以程序响应相对较快。
-
缓存
缓存重要的内容,比如远程服务器的响应内容,图片,甚至是计算结果,比如UITableView的行高。
NSURLConnection
根据HTTP头的处理过程,已经把一些资源缓存到磁盘和内存中了。你甚至可以手动创建一个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;
}关于HTTP缓存的更多内容可以关注
NSURLCache
。关于缓存其他非HTTP请求的内容,可以关注NSCache
,NSCache
的外观和行为与NSDictionary
类似,但是,当系统需要回收内存时,NSCache会自动的里面存储的内容。对于图片缓存可以关注一个第三方库SDWebImage
。 -
考虑绘制
对于一个背景图片的处理,可以使用全尺寸图片直接设置,还可以用
resizable images
,或者使用CALayer
、CoreGraphics
甚至OpenGL
来绘制。 -
处理内存警告
当系统内存偏低时,iOS会通知所有在运行的程序。我们可以通过这些方式来获得内存警告:
1.实现app delegate中的applicationDidReceiveMemoryWarning: 方法。
2.在UIViewController子类中重写(Override)didReceiveMemoryWarning方法。
3.在通知中心里面注册UIApplicationDidReceiveMemoryWarningNotificatio通知。常用释放内存操作:
1.UIViewController的默认情况是清除掉当前不可见的view;
2.在UIViewController的子类中,可以清除一些额外的数据。3.程序中没有显示在当前屏幕中的图片也可以清除掉。 -
重用花销很大的对象
有些对象的初始化非常慢,比如
NSDateFormatter
和NSCalendar
。为了使用这些对象我们一般在类中添加一个属性或者创建一个静态变量。出于对多线程环境的考虑,所以需用
dispatch_once
来加强设置。- (NSDateFormatter *)dateFormatter {
static NSDateFormatter *dateFormatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd a HH:mm:ss EEEE"];
});
return dateFormatter;
} -
避免重新处理数据
将数据从程序传到网络服务器中有多种方法,其中使用的数据格 式基本都是JSON和XML格式。获得的数据就是所需要格式的,可以避免将数据转换为适合程序的数据格式带来的额外代价。
-
选择正确的数据格式
将数据从程序传到网络服务器中有多种方法,其中使用的数据格式基本都是JSON和XML格式。
JSON:
优点是能够更快的被解析;在承载相同的数据时,通常体积比XML更小,这意味着传输的数据量更小;
缺点是需要整个JSON数据全部加载完成后才能开始解析。
XML:
优点是XML的一个优点就是它可以使用SAX来解析数据,从而可以边加载边解析。
缺点是在处理很大的数据集的时提高性能和降低内存消耗。 -
设置适当的背景图片
我们通常有两种方式来设置一个 View 的背景图片:
1.通过 - [UIColor colorWithPatternImage:] 方法来设置 View 的 background color。
该方法采用一个小块的模板样式图片,就像贴瓷砖那样来重复填充整个背景,绘制的更快,并且不会用到太多的内存。
2.通过给 View 添加一个 UIImageView 来设置其背景图片。
该方法适合用于全尺寸的图片,用 UIImageView 会节省大量的内存。 -
降低Web内容的影响
第一步:避免过量使用 Javascript,例如避免使用较大的 Javascript 框架,比如 jQuery。一般使用原生的 Javascript 而不是依赖于 Javascript 框架可以获得更好的性能。
第二步:可以异步加载那些不影响页面行为的 Javascript 脚本,比如一些数据统计脚本。
第三步:页面中所使用的图片,根据具体的场景来显示正确尺寸的图片,同时注意适时复用。
-
减少离屏渲染
离屏渲染,即
Off-Screen Rendering
。与之相对的是On-ScreenRendering
,即在当前屏幕渲染,意思是渲染操作是用于在当前屏幕显示的缓冲区进行。那么离屏渲染则是指图层在被显示之前是在当前屏幕缓冲区以外开辟的一个缓冲区进行渲染操作。离屏渲染需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕,而上下文环境的切换是一项高开销的动作。
通常图层的以下属性将会触发
Core Animation
做离屏渲染造成开销:- 1.阴影(UIView.layer.shadowOffset/shadowRadius/…)
- 2.圆角(当 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用时)
- 3.图层蒙板
- 4.重写drawRect方法
- 5.光栅化(UIView.layer.shouldRasterize/…)
// 会造成离屏渲染扩大开销
imageView.layer.shadowOffset = CGSizeMake(5.0f, 5.0f);
imageView.layer.shadowRadius = 5.0f;
imageView.layer.shadowOpacity = 0.6;解决办法:直接通过绘图设置相关路径,比如下面用
ShadowPath
属性来创建出一个对应形状的阴影路径// 注意:如果图层是一个比较复杂的图形,生成正确的阴影路径可能就比较难
imageView.layer.shadowPath = [[UIBezierPath bezierPathWithRect:CGRectMake(imageView.bounds.origin.x+5, imageView.bounds.origin.y+5, imageView.bounds.size.width, imageView.bounds.size.height)] CGPath];
imageView.layer.shadowOpacity = 0.6; -
光栅化
CALayer 有一个属性是 shouldRasterize 通过设置这个属性为 YES 可以将图层绘制到一个屏幕外的图像,然后这个图像将会被缓存起来并绘制到实际图层的 contents 和子图层,如果很很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧来更加高效。但是光栅化原始图像需要时间,而且会消耗额外的内存。
当我们使用得当时,光栅化可以提供很大的性能优势,但是一定要避免在内容不断变动的图层上使用,否则它缓存方面的好处就会消失,而且会让性能变的更糟。
为了检测你是否正确地使用了光栅化方式,可以用 Instrument 的 Core Animation Template 查看一下 Color Hits Green and Misses Red 项目,看看是否已光栅化图像被频繁地刷新(这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。
如果你最后设置了 shouldRasterize 为 YES,那也要记住设置 rasterizationScale 为合适的值。在我们使用 UITableView 和 UICollectionView 时经常会遇到各个 Cell 的样式是一样的,这时候我们可以使用这个属性提高性能:
// 注意:如果你的Cell是样式不一样,比如高度不定,排版多变,那就最好别用
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [[UIScreen mainScreen] scale]; -
优化TableView
- 通过正确的设置
reuseIdentifier
来重用 Cell。 - 尽量减少不必要的透明 View。
- 尽量避免渐变效果、图片拉伸和离屏渲染。
- 当不同的行的高度不一样时,尽量缓存它们的高度值。
- 如果cell显示的内容来此网络,那么确保这些内容是通过异步来获取的。
- 使用shadowPath来设置阴影。
- 减少subview的数量。
- 在
cellForRowAtIndexPath:
中尽量做更少的操作。如果需要做一些处理,那么最好做过一次之后,就将结果缓存起来。 - 使用适当的数据结构来保存需要的信息。不同的结构会带来不同的操作代价。
- 使用
rowHeight
,sectionFooterHeight
和sectionHeaderHeight
来设置一个恒定高度,而不要从 delegate中获取。
- 通过正确的设置
-
选择正确的数据存储方式
- NSUserDefaults:只适合用来存小数据。
- XML、JSON、Plist 等文件:JSON 和 XML 文件的差异在「选择正确的数据格式」已经说过了。
- 使用 NSCoding 来存档:NSCoding 同样是对文件进行读写,所以它也会面临必须加载整个文件才能继续的问题。
- 使用 SQLite 数据库:可以配合 FMDB 使用。数据的相对文件来说还是好处很多的,比如可以按需取数据、不用暴力查找等等。
- 使用 CoreData:也是数据库技术,跟 SQLite 的性能差异比较小。但是 CoreData 是一个对象图谱模型,显得更面向对象;SQLite 就是常规的 DBMS。
三、高级
加速启动时间
-
使用
Autorelease Pool
NSAutoreleasePool 是用来管理一个自动释放内存池的机制。在我们的应用程序中通常都是 UIKit 隐式的自动使用 Autorelease Pool,但是有时候我们也可以显式的来用它。
比如当你需要在代码中创建许多临时对象时,你会发现内存消耗激增直到这些对象被释放,一个问题是这些内存只会到 UIKit 销毁了它对应的 Autorelease Pool 后才会被释放,这就意味着这些内存不必要地会空占一些时间。这时候就是我们显式的使用 Autorelease Pool 的时候了,一个示例如下:
// 在每一轮迭代中都会释放掉临时对象,从而缓解内存压力,提高性能。
NSArray *urls = <# An array of file URLs #>;
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. */
}
} -
缓存图片 — 或者不缓存
在 iOS 应用中加载图片通常有
- [UIImage imageNamed:]
和-[UIImage imageWithContentsOfFile:]
两种方式。它们的不同在于前者会对图片进行缓存,而后者只是简单的从文件加载文件。当需要加载一张较大的图片,并且只会使用它一次,那么就没必要缓存这个图片,这时可以使用
-[UIImage imageWithContentsOfFile:]
,这样系统也不会浪费内存来做缓存了。 -
避免使用
NSDateFormatter
在前文中,我们已经讲到了通过复用或者单例来提高
NSDateFormatter
这个高开销对象的使用效率。但是如果你要追求更快的速度,你可以直接使用 C 语言替代NSDateFormatter
来解析 date。如果是解析ISO-8601 date string
的代码,你可以根据你的需求改写。完成的代码见:SSToolkit/NSDate+SSToolkitAdditions.m如果你自己能控制处理日期的格式,那么可以选择
Unix timestamps
。Unix timestamps
是一个简单的整数,代表 了从新纪元时间(epoch)开始到现在已经过了多少秒,通常这个新纪元参考时间是00:00:00 UTC on 1 January 1970
。你可以很容易的见这个时间戳转换为NSDat e,如下所示:// 将时间戳转化为 NSDate 对象效率很高,比上面的c语言写的还高
- (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
return [NSDate dateWithTimeIntervalSince1970:timestamp];
} -
IMP Caching
在 Objective-C 的消息分发过程中,所有
[receiver message:…]
形式的方法调用最终都会被编译器转化为obj_msgSend(recevier, @selector(message), …)
的形式调用。在运行时,Runtime 会去根据selector
到对应方法列表中查找相应的 IMP 来调用,这是一个动态绑定的过程。为了加速消息的处理,Runtime 系统会缓存使用过的 selector 对应的 IMP 以便后面直接调用,这就是 IMP Caching。通过IMP Caching
的方式,Rumtime 能够跳过obj_msgSend
的过程直接调用方法的实现,从而提高方法调用效率。下面看段示例代码:
#define LOOP 1000000
#define START { clock_t start, end; start = clock();
#define END end = clock(); printf("Cost: %f ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000); }
- (NSDateFormatter *)dateFormatter:(NSString *)format {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:format]; return dateFormatter;
}
- (void)testIMPCaching {
[self normalCall]; [self impCachingCall];
}
- (void)normalCall {
START for (int32_t i = 0; i < LOOP; i++) {
NSDateFormatter *d =[self dateFormatter:@"yyyy-MM-dd a HH:mm:ss EEEE"];
d = nil;
}
END
// Print: Cost: 1456.835000 ms
}
- (void)impCachingCall {
START SEL sel = @selector(dateFormatter:);
NSDateFormatter *(*imp)(id, SEL, id) = (NSDateFormatter *(*)(id, SEL, id)) [self methodForSelector:sel]; for (int32_t i = 0; i < LOOP; i++) {
NSDateFormatter *d = imp(self, sel, @"yyyy-MM-dd a HH:mm:ss EEEE");
d = nil;
} END
// Print: Cost: 1332.810000 ms
}