转载自:http://www.samirchen.com/ios-performance-optimization/html
程序性能优化不该该是一件放在功能完成以后的事,对性能的概念应该从咱们一开始写代码时就萦绕在咱们脑子里。了解 iOS 程序性能优化的相关知识点,从一开始就把它们落实到代码中是一种好的习惯。ios
在咱们使用 UITableView 和 UICollectionView 时咱们一般会遇到「复用 Cell」这个提法,所谓「复用 Cell」就是指当须要展现的数据条目较多时,只建立较少数量的 Cell 对象(通常是屏幕可显示的 Cell 数再加一)并经过复用它们的方式来展现数据的机制。这种机制不会为每一条数据都建立一个 Cell,因此能够节省内存,提高程序的效率和交互流畅性。git
从 iOS 6 之后,咱们在 UITableView 和 UICollectionView 中不光能够复用 Cell,还能够复用各个 Section 的 Header 和 Footer。github
在 UITableView 作复用的时候,会用到的 API:web
// 复用 Cell:
- [UITableView dequeueReusableCellWithIdentifier:];
- [UITableView registerNib:forCellReuseIdentifier:];
- [UITableView registerClass:forCellReuseIdentifier:];
- [UITableView dequeueReusableCellWithIdentifier:forIndexPath:];
// 复用 Section 的 Header/Footer:
- [UITableView registerNib:forHeaderFooterViewReuseIdentifier:];
- [UITableView registerClass:forHeaderFooterViewReuseIdentifier:];
- [UITableView dequeueReusableHeaderFooterViewWithIdentifier:];
复用机制是一个很好的机制,可是不正确的使用却会给咱们的程序带来不少问题。下面拿 UITableView 复用 Cell 来举例:objective-c
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = nil;
UITableViewCell *cell = nil;
CellIdentifier = @"UITableViewCell";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
// 偶数行 Cell 的 textLabel 的文字颜色为红色。
if (indexPath.row % 2 == 0) {
[cell.textLabel setTextColor:[UIColor redColor]];
}
}
cell.textLabel.text = @"Title";
// 偶数行 Cell 的 detailTextLabel 显示 Detail 文字。
if (indexPath.row % 2 == 0) {
cell.detailTextLabel.text = @"Detail";
}
return cell;
}
咱们原本是但愿只有偶数行的 textLabel 的文字颜色为红色,而且显示 Detail 文字,可是当你滑动 TableView 的时候发现不对了,有些奇数行的 textLabel 的文字颜色为红色,并且还显示了 Detail 文字,很奇怪。其实形成这个问题的缘由就是「复用」,当一个 Cell 被拿来复用时,它全部被设置的属性(包括样式和内容)都会被拿来复用,若是恰好某一个的 Cell 你没有显式地设置它的属性,那么它这些属性就直接复用别的 Cell 的了。就如上面的代码中,咱们并无显式地设置奇数行的 Cell 的 textLabel 的文字颜色以及 detailTextLabel 的文字,那么它就有可能复用别的 Cell 的这些属性了。此外,还有个问题,对偶数行 Cell 的 textLabel 的文字颜色的设置放在了初始一个 Cell 的 if 代码块里,这样在复用的时候,逻辑走不到这里去,那么也会出现复用问题。因此,上面的代码须要改为这样:数据库
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = nil;
UITableViewCell *cell = nil;
CellIdentifier = @"UITableViewCell";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
cell.textLabel.text = @"Title";
if (indexPath.row % 2 == 0) {
[cell.textLabel setTextColor:[UIColor redColor]];
cell.detailTextLabel.text = @"Detail";
}
else {
[cell.textLabel setTextColor:[UIColor blackColor]];
cell.detailTextLabel.text = nil;
}
return cell;
}
总之在复用的时候须要记住:编程
上面的代码中,咱们展现了 - [UITableView dequeueReusableCellWithIdentifier:];
的用法。下面看看另几个 API 的用法:数组
@property (weak, nonatomic) IBOutlet UITableView *myTableView;
- (void)viewDidLoad {
[super viewDidLoad];
// Setup table view.
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
[self.myTableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:@"MyTableViewCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = nil;
UITableViewCell *cell = nil;
CellIdentifier = @"MyTableViewCell";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = @"Title";
if (indexPath.row % 2 == 0) {
[cell.textLabel setTextColor:[UIColor redColor]];
}
else {
[cell.textLabel setTextColor:[UIColor blackColor]];
}
return cell;
}
能够看到,- [UITableView dequeueReusableCellWithIdentifier:forIndexPath:];
必须搭配 - [UITableView registerClass:forCellReuseIdentifier:];
或者 - [UITableView registerNib:forCellReuseIdentifier:];
使用。当有可重用的 Cell 时,前者直接拿来复用,并调用 - [UITableViewCell prepareForReuse]
方法;当没有时,前者会调用 Identifier 对应的那个注册的 UITableViewCell 类的 - [UITableViewCell initWithStyle:reuseIdentifier:]
方法来初始化一个,这里省去了你本身初始化的步骤。当你自定义了一个 UITableViewCell 的子类时,你能够这样来用。缓存
UIView 有一个 opaque
属性,在你不须要透明效果时,你应该尽可能设置它为 YES 能够提升绘图过程的效率。
在一个静态的视图里,这点可能影响不大,可是当在一个能够滚动的 Scroll View 中或是一个复杂的动画中,透明的效果可能会对程序的性能有较大的影响。
若是你压根不用 XIB,那就不须要看了。
在你须要重用某些自定义 View 或者由于历史兼容缘由用到 XIB 的时候,你须要注意:当你加载一个 XIB 时,它的全部内容都会被加载,若是这个 XIB 里面有个 View 你不会立刻就用到,你其实就是在浪费宝贵的内存。而加载 StoryBoard 时并不会把全部的 ViewController 都加载,只会按需加载。
基本上 UIKit 会把它全部的工做都放在主线程执行,好比:绘制界面、管理手势、响应输入等等。当你把全部代码逻辑都放在主线程时,有可能由于耗时太长卡住主线程形成程序没法响应、流畅性太差等问题。形成这种问题的大多数场景是由于你的程序把 I/O 操做放在了主线程,好比从硬盘或者网络读写数据等等。
你能够经过异步的方式来进行这些操做,把他们放在别的线程中处理。好比处理网络请求时,你可使用 NSURLConnection 的异步调用 API:
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler;
或者使用第三方的类库,好比 AFNetworking。
当你作一些耗时比较长的操做时,你可使用 GCD、NSOperation、NSOperationQueue。好比 GCD 的常见使用方式:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// switch to another thread and perform your expensive operation
dispatch_async(dispatch_get_main_queue(), ^{
// switch back to the main thread to update your UI
});
});
关于 GCD 更多的知识,你能够看看这篇文章:GCD。
当你从 App bundle 中加载图片到 UIImageView 中显示时,最好确保图片的尺寸可以和 UIImageView 的尺寸想匹配(固然,须要考虑 @2x @3x 的状况),不然会使得 UIImageView 在显示图片时须要作拉伸,这样会影响性能,尤为是在一个 UIScrollView 的容器里。
有时候,你的图片是从网络加载的,这时候你并不能控制图片的尺寸,不过你能够在图片下载下来后去手动 scale 一下它,固然,最好是在一个后台线程作这件事,而后在 UIImageView 中使用 resize 后的图片。
咱们常常须要用到容器来转载多个对象,咱们一般用到的包括:NSArray、NSDictionary、NSSet,它们的特性以下:
根据以上特性,在编程中须要选择适合的容器。更多内容请看:Collections Programming Topics
如今愈来愈多的应用须要跟服务器进行数据交互,当交互的数据量较大时,网络传输的时延就会较长,经过启动数据压缩功能,尤为是对于文本信息,能够下降网络传输的数据量,从而减短网络交互的时间。
一个好消息是当你使用 NSURLConnection 或者基于此的一些网络交互类库(好比 AFNetworking)时 iOS 已经默认支持 GZIP 压缩。而且,不少服务器已经支持发送压缩数据。
经过在服务器和客户端程序中启用对网络交互数据的压缩,是一条提升应用程序性能的途径。
在上面的内容里咱们介绍了一些显而易见的优化程序性能的途径,可是有时候,有些优化程序性能的方案并非那么明显,这些方案是否适用取决于你的代码状况。可是,若是在正确的场景下,这些方案能起到很显著的做用。
当你的程序中须要展现不少的 View 的时候,这就意味着须要更多的 CPU 处理时间和内存空间,这个状况对程序性能的影响在你使用 UIScrollView 来装载和呈现界面时会变得尤其显著。
处理这种状况的一种方案就是向 UITableView 和 UICollectionView 学习,不要一次性把全部的 subviews 都建立出来,而是在你须要他们的时候建立,而且用复用机制去复用他们。这样减小了内存分配的开销,节省了内存空间。
「懒加载机制」就是把建立对象的时机延后到不得不须要它们的时候。这个机制经常用在对一个类的属性的初始化上,好比:
- (UITableView *)myTableView {
if (!_myTableView) {
CGRect viewBounds = self.view.bounds;
_myTableView = [[UITableView alloc] initWithFrame:viewBounds style:UITableViewStylePlain];
_myTableView.showsHorizontalScrollIndicator = NO;
_myTableView.showsVerticalScrollIndicator = NO;
_myTableView.backgroundColor = [UIColor whiteColor];
[_myTableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
_myTableView.dataSource = self;
_myTableView.delegate = self;
}
return _myTableView;
}
只有当咱们第一次用到 self.myTableView 的时候采起初始化和建立它。
可是,存在这样一种场景:你点击一个按钮的时候,你须要显示一个 View,这时候你有两种实现方案:
这两种方案都各有利弊。采用方案一,你在不须要这个 View 的时候显然白白地占用了更多的内存,可是当你点击按钮展现它的时候,你的程序能响应地相对较快,由于你只须要改变它的 hidden 属性。采用方案二,那么你获得的效果相反,你更准确的使用了内存,可是若是对这个 View 的初始化和建立比较耗时,那么响应性相对就没那么好了。
因此当你考虑使用何种方案时,你须要根据现实的状况来参考,去权衡到底哪一个因素才是影响性能的瓶颈,而后再作出选择。
在开发咱们的程序时,一个很重要的经验法则就是:对那些更新频度低,访问频度高的内容作缓存。
有哪些东西使咱们能够缓存的呢?好比下面这些:
NSURLConnection 能够根据 HTTP 头部的设置来决定把资源内容缓存在磁盘或者内存,你甚至能够设置让它只加载缓存里的内容:
+ (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。对于图片缓存,能够关注一个第三方库 SDWebImage。
当咱们为一个 UIButton 设置背景图片时,对于这个背景图片的处理,咱们有不少种方案,你可使用全尺寸图片直接设置,还能够用 resizable images,或者使用 CALayer、CoreGraphics 甚至 OpenGL 来绘制。
固然,不一样的方案的编码复杂度不同,性能也不同。关于图形绘制的不一样方案的性能问题,能够看看:Designing for iOS: Graphics Performance
简而言之,使用 pre-rendered 的图片会更快,由于这样就不须要在程序中去建立一个图像,并在上面绘制各类形状了(Offscreen Rendering,离屏渲染)。可是缺点是你必须把这些图片资源打包到代码包,从而须要增长程序包的体积。这就是为何 resizable images 是一个很棒的选择:不须要全尺寸图,让 iOS 为你绘制图片中那些能够拉伸的部分,从而减少了图片体积;而且你不须要为不一样大小的控件准备不一样尺寸的图片。好比两个按钮的大小不同,可是他们的背景图样式是同样的,你只须要准备一个对应样式的 resizable image,而后在设置这两个按钮的背景图的时候分别作拉伸就能够了。
可是一味的使用使用预置的图片也会有一些缺点,好比你作一些简单的动画的时候各个帧都用图片叠加,这样就可能要使用大量图片。
总之,你须要去在图形绘制的性能和应用程序包的大小上作权衡,找到最合适的性能优化方案。
关于内存警告,苹果的官方文档是这样说的:
If your app receives this warning, it must free up as much memory as possible. The best way to do this is to remove strong references to caches, image objects, and other data objects that can be recreated later.
咱们能够经过这些方式来得到内存警告:
- [AppDelegate applicationDidReceiveMemoryWarning:]
代理方法。didReceiveMemoryWarning
方法。UIApplicationDidReceiveMemoryWarningNotification
通知。当经过这些方式监听到内存警告时,你须要立刻释放掉不须要的内存从而避免程序被系统杀掉。
好比,在一个 UIViewController 中,你能够清除那些当前不显示的 View,同时能够清除这些 View 对应的内存中的数据,而有图片缓存机制的话也能够在这时候释放掉不显示在屏幕上的图片资源。
可是须要注意的是,你这时清除的数据,必须是能够在从新获取到的,不然可能由于必要数据为空,形成程序出错。在开发的时候,可使用 iOS Simulator 的 Simulate memory warning
的功能来测试你处理内存警告的代码。
在 Objective-C 中有些对象的初始化过程很缓慢,好比:NSDateFormatter
和 NSCalendar
,可是有些时候,你也不得不使用它们。为了这样的高开销的对象成为影响程序性能的重要因素,咱们能够复用它们。
好比,咱们在一个类里添加一个 NSDateFormatter 的对象,并使用懒加载机制来使用它,整个类只用到一个这样的对象,并只初始化一次:
// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
// inside the implementation (.m)
// When you need, just use self.dateFormatter
- (NSDateFormatter *)dateFormatter {
if (! _dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
[_dateFormatter setDateFormat:@"yyyy-MM-dd a HH:mm:ss EEEE"];
}
return _dateFormatter;
}
可是上面的代码在多线程环境下会有问题,因此咱们能够改进以下:
// no property is required anymore. The following code goes inside the implementation (.m)
- (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;
}
这样就线程安全了。(关于多线程 GCD 的知识,能够看看这篇文章:GCD)
须要注意的是:设置 NSDateFormatter 的 date format 跟建立一个新的 NSDateFormatter 对象同样慢,所以当你的程序中要用到多种格式的 date format,而每种又会用到屡次的时候,你能够尝试为每种 date format 建立一个可复用的 NSDateFormatter 对象来提供程序的性能。
若是你是游戏开发者,使用 Sprite Sheet 能够帮助你比标准的绘图方法更快地绘制场景,甚至占用更少的内存。
固然这里有些地方也能够参考上面已经提到过的性能优化方案,好比你的游戏有不少 Sprites 时,你能够参考 UITableViewCell 的复用机制,而不是每次都建立它们。
在咱们开发应用时,常常会遇到要从服务器获取 JSON 或者 XML 数据来处理的状况,这时咱们一般都须要解析这些数据,通常会解析为 NSArray、NSDictionary 的对象。可是在咱们实际的开发中,咱们一般会为在界面上展现的那些数据定义一些数据结构(ViewModel)。这时候问题就来了,咱们须要把解析出来的 NSArray、NSDictionary 对象再倒腾成咱们定义的那些数据结构,从程序性能的角度来考虑,这是一项开销较大的操做。
为了不这样的状况成为影响程序性能的瓶颈,在设计客户端应用程序对应的数据结构时就须要作更细致的考虑,尽可能用 NSArray 去承接 NSArray,用 NSDictionary 去承接 NSDictionary,避免倒腾数据形成开销。
咱们的 iOS 应用程序与服务器进行交互时,一般采用的数据格式就是 JSON 和 XML 两种。那么在选择哪种时,须要考虑到它们的优缺点。
JSON 文件的优势是:
缺点是:
而 XML 文件的优缺点则恰好反过来。 XML 的一个优势就是它可使用 SAX 来解析数据,从而能够边加载边解析,不用等全部数据都读取完成了才解析。这样在处理很大的数据集的时提升性能和下降内存消耗。
因此,你须要根据具体的应用场景来权衡使用何种数据格式。
咱们一般有两种方式来设置一个 View 的背景图片:
- [UIColor colorWithPatternImage:]
方法来设置 View 的 background color。当你有一个全尺寸图片做为背景图时,你最好用 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:@"backgroundPattern"]];
UIWebView 在咱们的应用程序中很是有用,它能够便捷的展现 Web 的内容,甚至作到你用标准的 UIKit 控件较难作到的视觉效果。可是,你应该注意到你在应用程序里使用的 UIWebView 组件不会比苹果的 Safari 更快。这是首先于 Webkit 的 Nitro Engine 引擎。因此,为了获得更好的性能,你须要优化你的网页内容。
优化第一步就是避免过量使用 Javascript,例如避免使用较大的 Javascript 框架,好比 jQuery。通常使用原生的 Javascript 而不是依赖于 Javascript 框架能够得到更好的性能。
优化第二步,若是可能的话,能够异步加载那些不影响页面行为的 Javascript 脚本,好比一些数据统计脚本。
优化第三步,老是关注你在页面中所使用的图片,根据具体的场景来显示正确尺寸的图片,同时也可使用上面提到的「使用 Sprites Sheets」的方案来在某些地方减小内存消耗和提升速度。
什么是「离屏渲染」?离屏渲染,即 Off-Screen Rendering。与之相对的是 On-Screen Rendering,即在当前屏幕渲染,意思是渲染操做是用于在当前屏幕显示的缓冲区进行。那么离屏渲染则是指图层在被显示以前是在当前屏幕缓冲区之外开辟的一个缓冲区进行渲染操做。
离屏渲染须要屡次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束之后,将离屏缓冲区的渲染结果显示到屏幕上又须要将上下文环境从离屏切换到当前屏幕,而上下文环境的切换是一项高开销的动做。
一般图层的如下属性将会触发离屏渲染:
在 iOS 开发中要给一个 View 添加阴影效果,有很简单快捷的作法:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:...];
// Setup the shadow ...
imageView.layer.shadowOffset = CGSizeMake(5.0f, 5.0f);
imageView.layer.shadowRadius = 5.0f;
imageView.layer.shadowOpacity = 0.6;
可是上面这样的作法有一个坏处是:将触发 Core Animation 作离屏渲染形成开销。
那要作到阴影图层效果,又想减小离屏渲染、提升性能的话要怎么作呢?一个好的建议是:设置 ShadowPath 属性。
UIImageView *imageView = [[UIImageView alloc] initFrame:...];
// Setup the shadow ...
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;
若是图层是一个简单几何图形如矩形或者圆角矩形(假设不包含任何透明部分或者子图层),经过设置 ShadowPath 属性来建立出一个对应形状的阴影路径就比较容易,并且 Core Animation 绘制这个阴影也至关简单,不会触发离屏渲染,这对性能来讲颇有帮助。若是你的图层是一个更复杂的图形,生成正确的阴影路径可能就比较难了,这样子的话你能够考虑用绘图软件预先生成一个阴影背景图。
CALayer 有一个属性是 shouldRasterize
经过设置这个属性为 YES 能够将图层绘制到一个屏幕外的图像,而后这个图像将会被缓存起来并绘制到实际图层的 contents 和子图层,若是很不少的子图层或者有复杂的效果应用,这样作就会比重绘全部事务的全部帧来更加高效。可是光栅化原始图像须要时间,并且会消耗额外的内存。这是须要根据实际场景权衡的地方。
当咱们使用得当时,光栅化能够提供很大的性能优点,可是必定要避免在内容不断变更的图层上使用,不然它缓存方面的好处就会消失,并且会让性能变的更糟。
为了检测你是否正确地使用了光栅化方式,能够用 Instrument 的 Core Animation Template 查看一下 Color Hits Green and Misses Red
项目,看看是否已光栅化图像被频繁地刷新(这样就说明图层并非光栅化的好选择,或则你无心间触发了没必要要的改变致使了重绘行为)。
若是你最后设置了 shouldRasterize 为 YES,那也要记住设置 rasterizationScale 为合适的值。在咱们使用 UITableView 和 UICollectionView 时常常会遇到各个 Cell 的样式是同样的,这时候咱们可使用这个属性提升性能:
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [[UIScreen mainScreen] scale];
可是,若是你的 Cell 是样式不同,好比高度不定,排版多变,那就要慎重了。
UITableView 是咱们最经常使用来展现数据的控件之一,而且一般须要 UITableView 在承载较多内容的同时保证交互的流畅性,对 UITableView 的性能优化是咱们开发应用程序必备的技巧之一。
在前文「使用复用机制」一节,已经提到了 UITableView 的复用机制。如今就来看看 UITableView 在复用时最主要的两个回调方法:- [UITableView tableView:cellForRowAtIndexPath:]
和 - [UITableView tableView:heightForRowAtIndexPath:]
。UITableView 是继承自 UIScrollView,因此在渲染的过程当中它会先肯定它的 contentSize 及每一个 Cell 的位置,而后才会把复用的 Cell 放置到对应的位置。好比如今一共有 50 个 Cell,当前屏幕上显示 5 个。那么在第一次建立或 reloadData 的时候, UITableView 会先调用 50 次 - [UITableView tableView:heightForRowAtIndexPath:]
肯定 contentSize 及每一个 Cell 的位置,而后再调用 5 次 - [UITableView tableView:cellForRowAtIndexPath:]
来渲染当前屏幕的 Cell。在滑动屏幕的时候,每当一个 Cell 进入屏幕时,都须要调用一次 - [UITableView tableView:cellForRowAtIndexPath:]
和 - [UITableView tableView:heightForRowAtIndexPath:]
方法。
了解了 UITableView 的复用机制以及相关回调方法的调用次序,这里就对 UITableView 的性能优化方案作一个总结:
- [UITableView tableView:cellForRowAtIndexPath:]
方法中的处理逻辑,若是确实要作一些处理,能够考虑作一次,缓存结果。在 iOS 中能够用来进行数据持有化的方案包括:
下面介绍几个高级技巧。
快速启动应用对于用户来讲能够留下很好的印象。尤为是第一次使用时。
保证应用快速启动的指导原则:
注意:在测试程序启动性能的时候,最好用与 Xcode 断开链接的设备进行测试。由于 watchdog 在使用 Xcode 进行调试的时候是不会启动的。
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. */
}
}
上面的代码在每一轮迭代中都会释放掉临时对象,从而缓解内存压力,提升性能。
关于 Autorelease Pool 你还能够看看:Using Autorelease Pool Blocks 和 iOS 中的 AutoreleasePool。
在 iOS 应用中加载图片一般有 - [UIImage imageNamed:]
和 -[UIImage imageWithContentsOfFile:]
两种方式。它们的不一样在于前者会对图片进行缓存,然后者只是简单的从文件加载文件。
UIImage *img = [UIImage imageNamed:@"myImage"]; // caching
// or
UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"]; // no caching
在整个程序运行的过程当中,当你须要加载一张较大的图片,而且只会使用它一次,那么你就不必缓存这个图片,这时你可使用 -[UIImage imageWithContentsOfFile:]
,这样系统也不会浪费内存来作缓存了。固然,若是你会屡次使用到一张图时,用 - [UIImage imageNamed:]
就会高效不少,由于这样就不用每次都从硬盘上加载图片了。
在前文中,咱们已经讲到了经过复用或者单例来提升 NSDateFormatter 这个高开销对象的使用效率。可是若是你要追求更快的速度,你能够直接使用 C 语言替代 NSDateFormatter 来解析 date,你能够看看这篇文章:link,其中展现了解析 ISO-8601 date string 的代码,你能够根据你的需求改写。完成的代码见:SSToolkit/NSDate+SSToolkitAdditions.m。
固然,若是你可以控制你接受到的 date 的参数的格式,你必定要尽可能选择 Unix timestamps
格式,这样你可使用:
- (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
return [NSDate dateWithTimeIntervalSince1970:timestamp];
}
这样你能够轻松的将时间戳转化为 NSDate 对象,而且效率甚至高于上面提到的 C 函数。
须要注意的是,不少 web API 返回的时间戳是以毫秒为单位的,由于这更利于 Javascript 去处理,可是上面代码用到的方法中 NSTimeInterval 的单位是秒,因此当你传参的时候,记得先除以 1000。
在 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: 1328.845000 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: 1130.200000 ms
}
代码打印结果以下:
Cost: 1328.845000 ms
Cost: 1130.200000 ms
可见相差并不太大,在 impCachingCall
中是直接手动作 IMP Caching 来跳过 obj_msgSend 调用方法的实现。normalCall
则是在 Runtime 经过系统本身的 IMP Caching 机制来运行。一般咱们不须要作 IMP Caching,可是若是有时候哪怕一点点的速度提高也是你须要的,你能够考虑考虑这点。
此外关于 Objective 运行时相关的知识,你能够看看这篇:Objective-C 的 Runtime
参考: