IOS 性能提升總結

IOS 性能提升總結

該文章只做個人學習記錄:原文:http://www.jianshu.com/p/866ba7a38a23?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

使用複用機制

在我們使用 UITableView 和 UICollectionView 時我們通常會遇到「複用 Cell」這個提法,所謂「複用 Cell」就是指當需要展示的數據條目較多時,只創建較少數量的 Cell 對象(一般是屏幕可顯示的 Cell 數再加一)並通過複用它們的方式來展示數據的機制。這種機制不會爲每一條數據都創建一個 Cell,所以可以節省內存,提升程序的效率和交互流暢性。
從 iOS 6 以後,我們在 UITableView 和 UICollectionView 中不光可以複用 Cell,還可以複用各個 Section 的 Header 和 Footer。
在 UITableView 做複用的時候,會用到的 API:

// 複用 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 來舉例:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier =  @"UITableViewCell"; 
    UITableViewCell *cell = nil;
    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 = @"UITableViewCell"; 
   UITableViewCell *cell = nil; 
   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;
}

總之在複用的時候需要記住:
設置 Cell 的存在差異性的那些屬性(包括樣式和內容)時,有了 if 最好就要有 else,要顯式的覆蓋所有可能性。
設置 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 = @"MyTableViewCell"; 
     UITableViewCell *cell = nil; 
     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 的子類時,你可以這樣來用。

優化 UITableView

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 的性能優化方案做一個總結:

  • 通過正確的設置 reuseIdentifier 來重用 Cell。
  • 儘量減少不必要的透明 View。
  • 儘量避免漸變效果、圖片拉伸和離屏渲染。
  • 當不同的行的高度不一樣時,儘量緩存它們的高度值。
  • 如果 Cell 展示的內容來自網絡,確保用異步加載的方式來獲取數據,並且緩存服務器的 response。
  • 使用 shadowPath 來設置陰影效果。
  • 儘量減少 subview 的數量,對於 subview 較多並且樣式多變的 Cell,可以考慮用異步繪製或重寫 drawRect。
  • 儘量優化 - [UITableView tableView:cellForRowAtIndexPath:]
  • 方法中的處理邏輯,如果確實要做一些處理,可以考慮做一次,緩存結果。
  • 選擇合適的數據結構來承載數據,不同的數據結構對不同操作的開銷是存在差異的。
  • 對於 rowHeight、sectionFooterHeight、sectionHeaderHeight 儘量使用常量。

儘可能設置 View 爲不透明

UIView 有一個 opaque屬性,在你不需要透明效果時,你應該儘量設置它爲 YES 可以提高繪圖過程的效率。
在一個靜態的視圖裏,這點可能影響不大,但是當在一個可以滾動的 Scroll View 中或是一個複雜的動畫中,透明的效果可能會對程序的性能有較大的影響。

避免臃腫的 XIB 文件

如果你壓根不用 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

圖片尺寸匹配 UIImageView

當你從 App bundle 中加載圖片到 UIImageView 中顯示時,最好確保圖片的尺寸能夠和 UIImageView 的尺寸相匹配(當然,需要考慮 @2x @3x 的情況),否則會使得 UIImageView 在顯示圖片時需要做拉伸,這樣會影響性能,尤其是在一個 UIScrollView 的容器裏。

有時候,你的圖片是從網絡加載的,這時候你並不能控制圖片的尺寸,不過你可以在圖片下載下來後去手動 scale 一下它,當然,最好是在一個後臺線程做這件事,然後在 UIImageView 中使用 resize 後的圖片。
關於這一點SDWebImage進行了優化,如果你使用SDWebImage,它會幫你進行Scale優化。

選擇合適的容器

我們經常需要用到容器來轉載多個對象,我們通常用到的包括:NSArray、NSDictionary、NSSet,它們的特性如下:

  • Array:數組。有序的,通過 index 查找很快,通過 value 查找很慢,插入和刪除較慢。
  • Dictionary:字典。存儲鍵值對,通過鍵查找很快。
  • Set:集合。無序的,通過 value 查找很快,插入和刪除較快。

根據以上特性,在編程中需要選擇適合的容器。更多內容請看:Collections Programming Topics

啓用 GZIP 數據壓縮

現在越來越多的應用需要跟服務器進行數據交互,當交互的數據量較大時,網絡傳輸的時延就會較長,通過啓動數據壓縮功能,尤其是對於文本信息,可以降低網絡傳輸的數據量,從而減短網絡交互的時間。

一個好消息是當你使用 NSURLConnection 或者基於此的一些網絡交互類庫(比如 AFNetworking)時 iOS 已經默認支持 GZIP 壓縮。並且,很多服務器已經支持發送壓縮數據。

通過在服務器和客戶端程序中啓用對網絡交互數據的壓縮,是一條提高應用程序性能的途徑。

View 的複用和懶加載機制

當你的程序中需要展示很多的 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,這時候你有兩種實現方案:

  1. 在當前界面第一次加載的時候就創建出這個 View,只是把它隱藏起來,當你需要它的時候,只用顯示它就行了。
  2. 使用「懶加載機制」,在你需要這個 View 的時候才創建它,並展示它。
    這兩種方案都各有利弊。採用方案一,你在不需要這個 View 的時候顯然白白地佔用了更多的內存,但是當你點擊按鈕展示它的時候,你的程序能響應地相對較快,因爲你只需要改變它的 hidden 屬性。採用方案二,那麼你得到的效果相反,你更準確的使用了內存,但是如果對這個 View 的初始化和創建比較耗時,那麼響應性相對就沒那麼好了。

所以當你考慮使用何種方案時,你需要根據現實的情況來參考,去權衡到底哪個因素纔是影響性能的瓶頸,然後再做出選擇。

緩存

在開發我們的程序時,一個很重要的經驗法則就是:對那些更新頻度低,訪問頻度高的內容做緩存。
有哪些東西使我們可以緩存的呢?比如下面這些:

  • 服務器的響應信息(response)。
  • 圖片。
  • 計算值。比如:UITableView 的 row heights。
    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,然後在設置這兩個按鈕的背景圖的時候分別做拉伸就可以了。
但是一味的使用使用預置的圖片也會有一些缺點,比如你做一些簡單的動畫的時候各個幀都用圖片疊加,這樣就可能要使用大量圖片。
總之,你需要去在圖形繪製的性能和應用程序包的大小上做權衡,找到最合適的性能優化方案。

處理 Memory Warnings

關於內存警告,蘋果的官方文檔是這樣說的:

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 中實現 - [AppDelegate applicationDidReceiveMemoryWarning:]代理方法。
在 UIViewController 中重載 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 對象來提供程序的性能。

減少應用啓動時間

快速啓動應用對於用戶來說可以留下很好的印象。尤其是第一次使用時。儘量將啓動過程中的處理分拆成各個異步處理流,比如:網絡請求、數據庫訪問、數據解析等等。
避免臃腫的 XIB 文件,因爲它們會在你的主線程中進行加載。重申:Storyboard 沒這個問題,放心使用。
注意:在測試程序啓動性能的時候,最好用與 Xcode 斷開連接的設備進行測試。因爲 watchdog 在使用 Xcode 進行調試的時候是不會啓動的。

選擇正確的數據格式

我們的 iOS 應用程序與服務器進行交互時,通常採用的數據格式就是 JSON 和 XML 兩種。那麼在選擇哪一種時,需要考慮到它們的優缺點。
JSON 文件的優點是:

  • 能夠更快的被解析。
  • 在承載相同的數據時,通常體積比 XML 更小,這意味着傳輸的數據量更小。

缺點是:

  • 需要整個 JSON 數據全部加載完成後才能開始解析。

而 XML 文件的優缺點則剛好反過來。 XML 的一個優點就是它可以使用 SAX 來解析數據,從而可以邊加載邊解析,不用等所有數據都讀取完成了才解析。這樣在處理很大的數據集的時提高性能和降低內存消耗。
所以,你需要根據具體的應用場景來權衡使用何種數據格式。

合理的設置背景圖片

我們通常有兩種方式來設置一個 View 的背景圖片:

  • 通過 - [UIColor colorWithPatternImage:]方法來設置 View 的 background color。
  • 通過給 View 添加一個 UIImageView 來設置其背景圖片。
    當你有一個全尺寸圖片作爲背景圖時,你最好用 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"]];

優化 WebView

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);等到離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到屏幕上又需要將上下文環境從離屏切換到當前屏幕,而上下文環境的切換是一項高開銷的動作。
通常圖層的以下屬性將會觸發離屏渲染:

  • 陰影(UIView.layer.shadowOffset/shadowRadius/...)
  • 圓角(當 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用時)
  • 圖層蒙板
    在 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 是樣式不一樣,比如高度不定,排版多變,那就要慎重了。

選擇合適的數據存儲方式

在 iOS 中可以用來進行數據持有化的方案包括:

  • 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. */ 
      }
}

上面的代碼在每一輪迭代中都會釋放掉臨時對象,從而緩解內存壓力,提高性能。

imageNamed 和 imageWithContentsOfFile

在 iOS 應用中加載圖片通常有 - [UIImage imageNamed:]
和 -[UIImage imageWithContentsOfFile:]
兩種方式。它們的不同在於前者會對圖片進行緩存,而後者只是簡單的從文件加載文件。

UIImage *img = [UIImage imageNamed:@"myImage"]; // caching
// or
UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"]; // no caching

在整個程序運行的過程中,當你需要加載一張較大的圖片,並且只會使用它一次,那麼你就沒必要緩存這個圖片,這時你可以使用 -[UIImage imageWithContentsOfFile:]
,這樣系統也不會浪費內存來做緩存了。當然,如果你會多次使用到一張圖時,用 - [UIImage imageNamed:]
就會高效很多,因爲這樣就不用每次都從硬盤上加載圖片了。

避免使用 NSDateFormatter

在前文中,我們已經講到了通過複用或者單例來提高 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。

發佈了24 篇原創文章 · 獲贊 19 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章