寫在前面的話:建議通篇先看完,不要一開始就一步一步讀下去,并且按照作者提供的鏈接下載閱讀或者嘗試,整篇文章講的很好,個人很喜歡作者幽默風趣,善舉例子說明的風格,而相關代碼,由于作者鏈接的網站改版,就算運行也獲取不到想要的效果,所以沒必要都下載下來看。為此,我用自己最簡短的總結概括下:
- tableview上加載既有圖片又有文字的數據(所有數據是基于HTML的,需要解析HTML,拉取圖片ZIP文件,并解壓ZIP文件);
- 最開始作者處理的方案是所有的解析HTML,下載ZIP文件解壓ZIP文件都放在主線程,(結果卡頓很久,影響UI和用戶交互);
- 作者開始使用ASI異步下載,并使用通知回調,但是這時上上下下來回滑動,界面就卡死,于是作者加入了dispatch_async,保證所有耗時操作(比如:解析HTML,下載ZIP,解壓ZIP)都放后臺處理,而更新UI以及顯示圖片都放主線程處理。
- 最后提及NSOperations以及operation queues,而前者是基于GCD的。
你有沒有遇到這種情況:當你開發一個app時,某個地方你想處理一 些事情,但是由于UI長時間沒有反應而停頓了好長時間?
通常,這種跡象說明,你的app需要多線程處理下!
在這個教程,你將獲得關于iOS上可用的核心多線程API:GCD的相關經驗!
我們為你提供一個根本沒使用多線程的app,然后使用多線程修改它,你會為前后的不同感到震驚的!
該教程假設你已經熟悉基本的iOS開發。如果你完全是個iOS開發新手,你可以看看其它教程。
廢話少說,痛飲一番碳酸飲料或者嚼嚼泡泡糖,開始該教程吧!你已經踏上了多線程之路啦!
為什么我應該在乎?
“呃,為什么你要告訴我這呢?為啥我應該在乎呢?我才不在乎。你中午吃的啥飯呀?(我關心這,哈哈)”
如果你像一個木偶人,你可能仍在懷疑你為什么應該關心這些多線程業務,那么讓我們通過一個根本不用多線程的app的實例來告訴你為什么。
下載最原始工程,用XCode打開,然后編譯運行。你會看到來自vickiwenderlich.com的一個游戲藝術包展示在屏幕上:
這個APP叫
ImageGrabber
,它主要是通過這個web頁面的HTML并且檢索其中所有相關的圖像,顯示在表視圖,這樣你就可以更仔細地看到他們??岬氖撬踔料螺dzip文件并查找zip中所有圖片,比如vickiwenderlich.com上的free game art zip。接下來,點擊按鈕
Grab!
,看是否有反應。…
…waiting…
…
…waiting…
…
…waiting…
…
哇!它終于有效果了,但是等了太久!這App解析HTML,下載所有圖片和zip文件,以及解壓zip文件,都在主線程。最終的結果是用戶不得不花費大量寶貴時間等待,還不一定確定這個App是否還在加載!這樣后果是非常可怕的:用戶可能會退出App,系統會在等了太久而終止App,或者生氣的Tomato先生會攻擊你的樹屋。
幸運的是有了多線程的營救!我們把這些繁重的工作通過蘋果提供的簡單的APIs放到后臺處理,而不再試都放在主線程中。
多線程和群貓們
如果你已經熟悉多線程的概念,可以隨時跳到下一節,否則,繼續讀吧,騷年!
當你想到一個程序正在運行時,你可以想象它就像(下圖)一只貓要移動那個箭頭。貓移動箭頭和程序按照它的邏輯運行一樣,都是同一時間只移動一步。
多線程就像一群貓和一個箭頭。(一群貓移動一個箭頭?。?br>
ImageGrabber
的問題是在主線程中使得我們可憐的貓精疲力盡地去做所有的工作。因此,在這個App繪制UI或者相應用戶交互事件之前,不得不先完成所有的耗時操作,比如下載文件,解析HTML等。那么我們該怎樣讓勞累過度的貓喘口氣呢?最簡單的解決方案就是買更多的貓(事實上,我有一個朋友相當在行這)。于是,
主貓
來響應更新UI和用戶的交互事件,而其他的貓則繞著后臺去下載文件,解析HTML,然后傳表視圖(這個貓就退下,等待新的任務)!這就是多線程技術的核心。就像群貓(在后臺)執行各種任務,這程序被放在不同的線程執行。
iOS開發,你習慣用的函數方法(比如
viewDidLoad
,button點擊回調
等)都在主線程,你不想在主線程執行耗時操作,這樣的話你的UI會很卡頓并且主貓
會勞累過度。
孩子們,別再這樣做了!
讓我們一起來看看當前的代碼并且討論它是怎么執行的,以及為什么這樣不好!
ImageGrabber
這個App的rootViewController
是WebViewController
,當你點擊buttonGrab!
后,它會獲取當前頁的HTML,并且傳遞給ImageListViewController
。
在ImageListViewController
的viewDidLoad
里,創建了一個新的ImageManager
對象并執行它的process
方法。ImageListViewController
這個類,不僅處理ImageInfo
信息,還包含所有的耗時操作代碼,比如:解析HTML,從網絡拉取圖片,以及解壓文件。
下面我們來看看ImageManager
和ImageInfo
是干什么用的:
ImageManager.m
的processHTML
方法 : 使用正則表達式匹配去搜索HTML中鏈接,但這可能是耗時的,主要還是看HTML有多大。當它每發現一個zip文件,就去調用retrieveZip:
方法。當它每發現一張圖片(image),就去用initWithSourceURL
創建一個ImageInfo
對象。
_____------
ImageInfo
的initWithSourceURL:
方法 : 調用getImage
方法,用[NSData dataWithContentsOfURL:...];
同步地去網絡拉取image.就像[NSString stringWithContentsOfURL:…]
方法一樣,會阻礙程序繼續執行,除非該方法執行完畢,當然了,這會花費很長時間的!你幾乎從來沒有想要在你的應用程序中使用這種方法。
_-----
ImageManager.m
的retrieveZip
方法 : 和上面的相似,用令人畏懼的[NSData dataWithContentsOfURL:...]
方法,會使得當前線程停滯不前,直到它自己完成任務結束(不要這樣用?。?。該方法結束時,它會調用processZip
方法。
_-----
ImageManager.m
的processZip
方法 : 用第三方庫ZipArchive
來保存下載的數據到本地磁盤,并且解壓數據,以及查找其中的圖片。像這樣的寫入磁盤和解壓文件是相當慢的操作,所以這是另外一個不應該在主線程操作的實例。
你可能還注意到了一些ImageManagerDelegate
的imageInfosAvailable
方法的調用,這就是當有新的數據要展示在tableView上時,ImageManager
怎么通知到tableView的。
現在停下了看一看,確保你理解了當前操作的執行,以及為什么這樣不好。你也許會覺得這樣是有用的,并且可以看到控制臺打印以及一些NSLog
描述信息,當程序運行時。
一旦你知道了該程序當前如何運行,讓我們用多線程繼續前進和提升它(性能更好,效率更高,交互反應時間更短等)。
異步下載
首先,替換同步下載文件這種最慢的操作。雖然蘋果內置的NSURLRequest
和NSURLConnection
類與封裝的類ASIHTTPRequest沒什么不同,但由于我更喜歡封裝好的類,并且ASIHTTPRequest
會使得異步下載更簡單。所以,我們將用這個類庫來下載文件,就讓我們把它加入到ImageGrabber
這個工程中吧。
如果你還沒有ASIHTTPRequest
,請先下載ASIHTTPRequest,一旦你下載成功,右擊ImageGrabber
工程,選擇New Group
,并且給這new group 命名為ASIHTTPRequest
,然后拖拽ASIHTTPRequest\Classes
目錄(ASIAuthenticationDialog.h和其它一些, 但是不要添加 ASIWebPageRequest, CloudFiles, S3, and Tests.)到ASIHTTPRequest
group。確保“Copy items into destination group’s folder (if needed)”選中, 然后再點擊完成。
重復上面的操作,導入ASIHTTPRequest\External\Reachability
,它也是工程需要的。
最后一步是添加ASIHTTPRequest,你需要在你的工程鏈接必須的frameWorks,具體操作:* Build Phases* ---> Link Binary with Libraries,添加CFNetwork.framework
,SystemConfiguration.framework
,MobileCoreServices.framework
.
是時候,用新的異步代碼替換之前的同步代碼了!
打開ImageManager.m
做以下改變:
// Add to top of file
#import "ASIHTTPRequest.h"
// Replace retrieveZip with the following
- (void)retrieveZip:(NSURL *)sourceURL {
NSLog(@"Getting %@...", sourceURL);
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:sourceURL];
[request setCompletionBlock:^{
NSLog(@"Zip file downloaded.");
NSData *data = [request responseData];
[self processZip:data sourceURL:sourceURL];
}];
[request setFailedBlock:^{
NSError *error = [request error];
NSLog(@"Error downloading zip file: %@", error.localizedDescription);
}];
[request startAsynchronous];
}
這種改進方法,通過一個URL,創建一個ASIHTTPRequest
對象,這個對象在請求結束會回調,并且因為某些原因請求失敗也會回調。然后調用startAsynchronous
方法,這個方法立即返回以致于主線程可以繼續處理自己的業務,比如:UI做動畫,相應用戶輸入。與此同時,OS系統會自動運行代碼在后臺下載zip文件,并且在任務完成或者失敗時立即回調!
參考:最初的代碼為:
pragma mark --- pp最初的
- (void)retrieveZip:(NSURL *)sourceURL {
if (!data) {
NSLog(@"Error retrieving %@", sourceURL);
return;
}
}
與此相似,找到ImageInfo.m
并且做類似改變:
// Add to top of file
#import "ASIHTTPRequest.h"
// Replace getImage with the following
- (void)getImage {
NSLog(@"Getting %@...", sourceURL);
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:sourceURL];
[request setCompletionBlock:^{
NSLog(@"Image downloaded.");
NSData *data = [request responseData];
image = [[UIImage alloc] initWithData:data];
}];
[request setFailedBlock:^{
NSError *error = [request error];
NSLog(@"Error downloading image: %@", error.localizedDescription);
}];
[request startAsynchronous];
}
這幾乎和ImageManager.m
中剛才的代碼一樣,都是在后臺下載,下載完成后,設置圖像為可用的結果。
參考:最初的代碼為:
#pragma mark --- 原始的方法(沒有使用多線程)
-(void)getImage
{
NSLog(@"Getting %@...", _sourceURL);
>
NSData * data = [NSData dataWithContentsOfURL:_sourceURL];
if (!data) {
NSLog(@"Error retrieving %@", _sourceURL);
return;
}
_image = [[UIImage alloc] initWithData:data];
}
現在,我們一起看看這樣修改后是不是有效果!編譯運行后點擊Grab!
,在表視圖上很快顯示細節標簽文字,而不是等待很長時間,但是出現了一個主要的問題:
表視圖上的圖片下載成功后并不顯示!你可以通過上下滑動來讓它們顯示出來(這時候能顯示出來是因為它超過屏幕后會reloadData),這是一個問題。我們該怎樣去解決它呢?
介紹NSNotifications
一種簡單的辦法是用蘋果的NSNotifications系統,發送更新信息從一個地方到另一個地方。這樣做事相當簡單的,你獲取到NSNotificationCenter單例(用[NSNotificationCenter defaultCenter])并且:
- 如果你有一個想要發送的更新,你調用
postNotificationName
.你僅僅需要給它一個你自己創建的唯一字符串表示(s如“com.razeware.imagegrabber.imageupdated”)和一個對象(如:一個剛下載完圖片的ImageInfo
對象)。
- 如果你想知道更新什么時候發生,你可以調用
addObserver:selector:name:object
方法。一旦ImageListViewController
知道更新發生,它就會reload恰當的tableViewCell。最好把addObserver:selector:name:object
方法放在viewDidLoad
中。 - 當VC的view
unloaded
時,不要忘記調用removeObserver:name:object
方法,否則,通知會在一個unloaded view(或者 unallocated object)中調用某個方法,而這將是一個不好的事情!
那么就讓我們試試這!打開ImageInfo.m
并且做以下修改:
// Add inside getImage, right after image = [[UIImage alloc] initWithData:data];
[[NSNotificationCenter defaultCenter] postNotificationName:@"com.razeware.imagegrabber.imageupdated" object:self];
這樣一旦圖片下載成功,我們就發一個通知并且傳遞一個已經更新的對象(self).
接下來,跳到ImageListViewController.m
并且做以下修改:
// At end of viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageUpdated:) name:@"com.razeware.imagegrabber.imageupdated" object:nil];
// At end of viewDidUnload
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"com.razeware.imagegrabber.imageupdated" object:nil];
// Add new method
- (void)imageUpdated:(NSNotification *)notif {
ImageInfo * info = [notif object];
int row = [imageInfos indexOfObject:info];
NSIndexPath * indexPath = [NSIndexPath indexPathForRow:row inSection:0];
NSLog(@"Image for row %d updated!", row);
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
imageUpdated
方法通過通知傳遞過來的ImageInfo
對象去imageInfos
數組查找,一旦找到,獲取對應的row,并且告訴tableView刷新該row.
現在編譯運行,你會看到那些圖片被下載完后時不時或者突然出現在表視圖。
Grand Central Dispatch and Dispatch Queues, Oh My!
目前為止,我們的App任然有一個問題。只要詳情頁一加載,如果你點擊Grab!
按鈕并且一直上下滑動,在zip文件下載后,UI界面像冰凍一樣如果正在保存和解壓文件。這是因為,ASIHTTPRequest
的完成回調雖然是在主線程,但是我們處理zip文件也在主線程:
[request setCompletionBlock:^{
NSLog(@"Zip file downloaded.");
NSData *data = [request responseData];
[self processZip:data sourceURL:sourceURL]; // Ack - heavy work on main thread!
}];
那么我們該怎樣讓這繁重的工作在后臺處理呢?
好吧,iOS3.2介紹了一種簡單的(非常有效的)方法來解決這個問題,通過GCD?;镜?,無論什么時候你想在后臺跑一些東西,你只需要調用dispatch_async
并且傳入對應參數即可。GCD會為你處理所有---在它需要的時候它會創建新的線程,并且會重用那些過去的可用的(已經創建過,并且已經使用過,但是截至目前又是空閑的)線程。
當你調用dispatch_async
,你傳入一個dispatch queue參數,你可以認為這是一個存儲你傳入的所有blocks的列表,遵循先進先出
原則。
你也可以自己創建dispatch queue(通過dispatch_create),或者你也可以(通過dispatch_get_main_queue)得到一個特殊的主線程的dispatch queue。這里我們將創建一個用來在后臺執行任務(解析HTML以及保存/解壓zip文件)的名叫“backgroundQueue”的dispatch queue。
Dispatch Queues, Locks, and Cat Food
調度隊列(dispatch queue)默認情況下是串行的,回想我們最早關于貓的舉例,如果兩只貓同時想得到貓食盤會發生什么?這是個大問題。但是我們把所有的貓放在一條線上,并且告訴它們“如果它們想接近貓食盤,你們不得不排成一隊”,要是生活如此簡單鎖好。
這也是最基本是想法使用調度隊列(dispatch queue)來保護數據。你設置你的代碼以致于特殊的數據只能被一個特殊的調度隊列(dispatch queue)訪問。這樣既然調度隊列(dispatch queue)串行運行blocks,就能保證同一時間只有一個調度隊列(dispatch queue)能訪問這個數據結構。
在這個App中,我們有2個數據結構我們必須要保護:
ImageListViewController
里的imageInfos數組。為了保護它,我們將重構我們的代碼以致于它只能在主線程中觸發;ImageManager
里的pendingZips。為了保護它,我將重構我們的代碼以致于它只能在backgroundQueue中觸發。
圖片信息在主線程展示,而圖片獲取以及解壓在后臺處理。
關于GCD我們已經談論不少了,現在我們來嘗試嘗試它。
Grand Central Dispatch in Practice
打開ImageManager.h
并且做如下修改:
// Add to top of file
#import <dispatch/dispatch.h>
// Add new instance variable
dispatch_queue_t backgroundQueue;
用GCD前要先導入頭文件,并且我們也聲明了backgroundQueue用來在后臺處理任務。
接下來打開ImageManager.m
并且做如下修改:
// 1) Add to bottom of initWithHTML:delegate
backgroundQueue = dispatch_queue_create("com.razeware.imagegrabber.bgqueue", NULL);
// 2) Add to top of dealloc
dispatch_release(backgroundQueue);
// 3) Modify process to be the following
- (void)process {
dispatch_async(backgroundQueue, ^(void) {
[self processHtml];
});
}
// 4) Modify call to processZip inside retrieveZip to be the following
dispatch_async(backgroundQueue, ^(void) {
[self processZip:data sourceURL:sourceURL];
});
// 5) Modify call to delegate at the end of processHTML **AND** processZip to be the following
dispatch_async(dispatch_get_main_queue(), ^(void) {
[delegate imageInfosAvailable:imageInfos done:(pendingZips==0)];
});
這些都是簡單的但是重要的調用,讓我們依次討論每一個:
- 創建一個隊列。當你創建一個隊列時你需要給它一個唯一字符串標示,創建唯一標示的一個好的方法是用反向DNS表示法,像這樣。
- 當你創建一個隊列的時候不要忘了釋放它。對這個隊列,我們在
ImageManager
deallocated的時候釋放。- 老的
process
方法直接運行processHTML
方法,因此,在主線程運行它,當遇到解析HTML時,UI就會卡頓?,F在我們在我們自己創建的backgroundQueue后臺運行,用dispatch_async簡單地調用。- 與3相似,之前我們在zip文件下載完成通過
ASIHTTPRequeset
回調在主線程處理zip文件,現在我們把處理zip文件放到后臺,就不會出現之前保存和解壓zip文件時UI卡頓的現象。確保變量pendingZips是受保護的是很重要的。- 我們要確保在主線程的上下文調用代理方法。第一,確保
ImageListViewController
里的imageInfos數組只能通過主線程訪問,根據我們之前的戰略分析;第二,因為代理方法與UIKit對象有交互,而UIKit對象只能在主線程使用。
就這樣,編譯運行你的代碼,ImageGrabber
應該有更好更快的響應!
But Wait!
如果你有iOS編程經驗,你可能聽說過叫做NSOperations的神奇的東西,以及操作隊列(operation queues)。你可能好奇什么時候你應該用它們,什么時候你應該用GCD。實際上,NSOperations 是基于GCD的簡單API。這樣,當你使用 NSOperations 時,你實際上也是在使用GCD。NSOperations 僅僅是提供給你一些你可能喜歡的神奇的特性。你可以創建一些operations依賴于其它的operations,在你提交items后重新排列隊列,還有其它的像這樣的事情。
事實上,ImageGrabber
已經使用了NSOperations 和 operation queues!ASIHTTPRequest在底層使用它們,如果你喜歡,你可以自己配置operationsy用作處理不同行為。
所以你應該使用哪一個?哪個適合你的應用程序。對這個程序我們直接使用GCD是相當簡單,不需要NSOperation的神奇的功能。但是如果你的App需要它們,就去使用吧!
Where To Go From Here?
這有一個簡單的工程,包含上面教程的所有代碼。
現在為止,你已經有了在iOS上使用異步操作和GCD的實踐經驗。但本教程還遠遠不夠——還有很多你可以學習!
我首先建議聽大蘋果關于GCD的視頻。WWDC2010
年和WWDC2011
年都有一些視頻,介紹的很不錯。
如果你真的想學習GCD相關知識,Mike Ash 有一些好的關于GCD的文章你可以去看看.
如果你有任何問題、意見或建議,請在下方留言加入論壇的討論!
團隊
在www.raywenderlich.com
上的每個教程都是由專門的團隊開發人員創建,以此來符合我們的高質量的標準。團隊成員曾參與本教程是:
Ray Wenderlich
Follow Ray Wenderlich on Twitter