轉載自: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
參考: