原文鏈接
由于作者文章11年寫的城豁,給的demo以及文中錯誤地方纤垂,在翻譯的時候我已經(jīng)改正!你也可以在Github上看到凶硅!
寫在前面的話:建議通篇先看完,不要一開始就一步一步讀下去扫皱,并且按照作者提供的鏈接下載閱讀或者嘗試足绅,整篇文章講的很好,個人很喜歡作者幽默風趣韩脑,善舉例子說明的風格氢妈,而相關(guān)代碼,由于作者鏈接的網(wǎng)站改版段多,就算運行也獲取不到想要的效果首量,所以沒必要都下載下來看。為此进苍,我用自己最簡短的總結(jié)概括下:
- tableview上加載既有圖片又有文字的數(shù)據(jù)(所有數(shù)據(jù)是基于HTML的加缘,需要解析HTML,拉取圖片ZIP文件觉啊,并解壓ZIP文件)拣宏;
- 最開始作者處理的方案是所有的解析HTML,下載ZIP文件解壓ZIP文件都放在主線程杠人,(結(jié)果卡頓很久勋乾,影響UI和用戶交互);
- 作者開始使用ASI異步下載嗡善,并使用通知回調(diào)辑莫,但是這時上上下下來回滑動,界面就卡死罩引,于是作者加入了dispatch_async,保證所有耗時操作(比如:解析HTML各吨,下載ZIP,解壓ZIP)都放后臺處理蜒程,而更新UI以及顯示圖片都放主線程處理绅你。
- 最后提及NSOperations以及operation queues,而前者是基于GCD的伺帘。
你有沒有遇到這種情況:當你開發(fā)一個app時,某個地方你想處理一 些事情忌锯,但是由于UI長時間沒有反應而停頓了好長時間伪嫁?
通常,這種跡象說明偶垮,你的app需要多線程處理下!
在這個教程张咳,你將獲得關(guān)于iOS上可用的核心多線程API:GCD的相關(guān)經(jīng)驗!
我們?yōu)槟闾峁┮粋€根本沒使用多線程的app,然后使用多線程修改它似舵,你會為前后的不同感到震驚的脚猾!
該教程假設你已經(jīng)熟悉基本的iOS開發(fā)。如果你完全是個iOS開發(fā)新手砚哗,你可以看看其它教程龙助。
廢話少說,痛飲一番碳酸飲料或者嚼嚼泡泡糖蛛芥,開始該教程吧提鸟!你已經(jīng)踏上了多線程之路啦!
為什么我應該在乎?
“呃,為什么你要告訴我這呢仅淑?為啥我應該在乎呢称勋?我才不在乎。你中午吃的啥飯呀涯竟?(我關(guān)心這赡鲜,哈哈)”
如果你像一個木偶人,你可能仍在懷疑你為什么應該關(guān)心這些多線程業(yè)務,那么讓我們通過一個根本不用多線程的app的實例來告訴你為什么庐船。
下載最原始工程银酬,用XCode打開,然后編譯運行醉鳖。你會看到來自vickiwenderlich.com的一個游戲藝術(shù)包展示在屏幕上:
這個APP叫
ImageGrabber
捡硅,它主要是通過這個web頁面的HTML并且檢索其中所有相關(guān)的圖像,顯示在表視圖,這樣你就可以更仔細地看到他們〉量茫酷的是它甚至下載zip文件并查找zip中所有圖片,比如vickiwenderlich.com上的free game art zip。接下來北发,點擊按鈕
Grab!
,看是否有反應纹因。…
…waiting…
…
…waiting…
…
…waiting…
…
哇!它終于有效果了琳拨,但是等了太久瞭恰!這App解析HTML,下載所有圖片和zip文件狱庇,以及解壓zip文件惊畏,都在主線程恶耽。最終的結(jié)果是用戶不得不花費大量寶貴時間等待,還不一定確定這個App是否還在加載!這樣后果是非常可怕的:用戶可能會退出App,系統(tǒng)會在等了太久而終止App颜启,或者生氣的Tomato先生會攻擊你的樹屋偷俭。
幸運的是有了多線程的營救!我們把這些繁重的工作通過蘋果提供的簡單的APIs放到后臺處理缰盏,而不再試都放在主線程中涌萤。
多線程和群貓們
如果你已經(jīng)熟悉多線程的概念,可以隨時跳到下一節(jié),否則,繼續(xù)讀吧口猜,騷年!
當你想到一個程序正在運行時负溪,你可以想象它就像(下圖)一只貓要移動那個箭頭。貓移動箭頭和程序按照它的邏輯運行一樣济炎,都是同一時間只移動一步川抡。
多線程就像一群貓和一個箭頭。(一群貓移動一個箭頭P肷小)
ImageGrabber
的問題是在主線程中使得我們可憐的貓精疲力盡地去做所有的工作崖堤。因此,在這個App繪制UI或者相應用戶交互事件之前恨闪,不得不先完成所有的耗時操作倘感,比如下載文件,解析HTML等咙咽。那么我們該怎樣讓勞累過度的貓喘口氣呢老玛?最簡單的解決方案就是買更多的貓(事實上,我有一個朋友相當在行這)钧敞。于是蜡豹,
主貓
來響應更新UI和用戶的交互事件,而其他的貓則繞著后臺去下載文件溉苛,解析HTML镜廉,然后傳表視圖(這個貓就退下,等待新的任務)愚战!這就是多線程技術(shù)的核心娇唯。就像群貓(在后臺)執(zhí)行各種任務,這程序被放在不同的線程執(zhí)行寂玲。
iOS開發(fā)塔插,你習慣用的函數(shù)方法(比如
viewDidLoad
,button點擊回調(diào)
等)都在主線程,你不想在主線程執(zhí)行耗時操作拓哟,這樣的話你的UI會很卡頓并且主貓
會勞累過度想许。
孩子們,別再這樣做了!
讓我們一起來看看當前的代碼并且討論它是怎么執(zhí)行的流纹,以及為什么這樣不好糜烹!
ImageGrabber
這個App的rootViewController
是WebViewController
,當你點擊buttonGrab!
后漱凝,它會獲取當前頁的HTML疮蹦,并且傳遞給ImageListViewController
。
在ImageListViewController
的viewDidLoad
里碉哑,創(chuàng)建了一個新的ImageManager
對象并執(zhí)行它的process
方法挚币。ImageListViewController
這個類,不僅處理ImageInfo
信息扣典,還包含所有的耗時操作代碼妆毕,比如:解析HTML,從網(wǎng)絡拉取圖片贮尖,以及解壓文件笛粘。
下面我們來看看ImageManager
和ImageInfo
是干什么用的:
ImageManager.m
的processHTML
方法 : 使用正則表達式匹配去搜索HTML中鏈接,但這可能是耗時的湿硝,主要還是看HTML有多大薪前。當它每發(fā)現(xiàn)一個zip文件,就去調(diào)用retrieveZip:
方法关斜。當它每發(fā)現(xiàn)一張圖片(image)示括,就去用initWithSourceURL
創(chuàng)建一個ImageInfo
對象。
_____------
ImageInfo
的initWithSourceURL:
方法 : 調(diào)用getImage
方法痢畜,用[NSData dataWithContentsOfURL:...];
同步地去網(wǎng)絡拉取image.就像[NSString stringWithContentsOfURL:…]
方法一樣垛膝,會阻礙程序繼續(xù)執(zhí)行,除非該方法執(zhí)行完畢丁稀,當然了吼拥,這會花費很長時間的!你幾乎從來沒有想要在你的應用程序中使用這種方法线衫。
_-----
ImageManager.m
的retrieveZip
方法 : 和上面的相似凿可,用令人畏懼的[NSData dataWithContentsOfURL:...]
方法,會使得當前線程停滯不前授账,直到它自己完成任務結(jié)束(不要這樣用?菖堋)。該方法結(jié)束時白热,它會調(diào)用processZip
方法全肮。
_-----
ImageManager.m
的processZip
方法 : 用第三方庫ZipArchive
來保存下載的數(shù)據(jù)到本地磁盤,并且解壓數(shù)據(jù)棘捣,以及查找其中的圖片。像這樣的寫入磁盤和解壓文件是相當慢的操作,所以這是另外一個不應該在主線程操作的實例乍恐。
你可能還注意到了一些ImageManagerDelegate
的imageInfosAvailable
方法的調(diào)用评疗,這就是當有新的數(shù)據(jù)要展示在tableView上時,ImageManager
怎么通知到tableView的茵烈。
現(xiàn)在停下了看一看百匆,確保你理解了當前操作的執(zhí)行,以及為什么這樣不好呜投。你也許會覺得這樣是有用的加匈,并且可以看到控制臺打印以及一些NSLog
描述信息,當程序運行時仑荐。
一旦你知道了該程序當前如何運行雕拼,讓我們用多線程繼續(xù)前進和提升它(性能更好,效率更高粘招,交互反應時間更短等)啥寇。
異步下載
首先,替換同步下載文件這種最慢的操作洒扎。雖然蘋果內(nèi)置的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,創(chuàng)建一個ASIHTTPRequest
對象,這個對象在請求結(jié)束會回調(diào)奥务,并且因為某些原因請求失敗也會回調(diào)物独。然后調(diào)用startAsynchronous
方法,這個方法立即返回以致于主線程可以繼續(xù)處理自己的業(yè)務氯葬,比如:UI做動畫挡篓,相應用戶輸入。與此同時帚称,OS系統(tǒng)會自動運行代碼在后臺下載zip文件官研,并且在任務完成或者失敗時立即回調(diào)!
參考:最初的代碼為:
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
中剛才的代碼一樣戏羽,都是在后臺下載,下載完成后瞻坝,設置圖像為可用的結(jié)果蛛壳。
參考:最初的代碼為:
#pragma mark --- 原始的方法(沒有使用多線程)
-(void)getImage
{
NSLog(@"Getting %@...", _sourceURL);
>
NSData * data = [NSData dataWithContentsOfURL:_sourceURL];
if (!data) {
NSLog(@"Error retrieving %@", _sourceURL);
return;
}
_image = [[UIImage alloc] initWithData:data];
}
現(xiàn)在,我們一起看看這樣修改后是不是有效果所刀!編譯運行后點擊Grab!
,在表視圖上很快顯示細節(jié)標簽文字衙荐,而不是等待很長時間,但是出現(xiàn)了一個主要的問題:
表視圖上的圖片下載成功后并不顯示浮创!你可以通過上下滑動來讓它們顯示出來(這時候能顯示出來是因為它超過屏幕后會reloadData),這是一個問題忧吟。我們該怎樣去解決它呢?
介紹NSNotifications
一種簡單的辦法是用蘋果的NSNotifications系統(tǒng)斩披,發(fā)送更新信息從一個地方到另一個地方溜族。這樣做事相當簡單的,你獲取到NSNotificationCenter單例(用[NSNotificationCenter defaultCenter])并且:
- 如果你有一個想要發(fā)送的更新垦沉,你調(diào)用
postNotificationName
.你僅僅需要給它一個你自己創(chuàng)建的唯一字符串表示(s如“com.razeware.imagegrabber.imageupdated”)和一個對象(如:一個剛下載完圖片的ImageInfo
對象)煌抒。
- 如果你想知道更新什么時候發(fā)生,你可以調(diào)用
addObserver:selector:name:object
方法厕倍。一旦ImageListViewController
知道更新發(fā)生寡壮,它就會reload恰當?shù)膖ableViewCell。最好把addObserver:selector:name:object
方法放在viewDidLoad
中讹弯。 - 當VC的view
unloaded
時况既,不要忘記調(diào)用removeObserver:name:object
方法,否則组民,通知會在一個unloaded view(或者 unallocated object)中調(diào)用某個方法棒仍,而這將是一個不好的事情!
那么就讓我們試試這臭胜!打開ImageInfo.m
并且做以下修改:
// Add inside getImage, right after image = [[UIImage alloc] initWithData:data];
[[NSNotificationCenter defaultCenter] postNotificationName:@"com.razeware.imagegrabber.imageupdated" object:self];
這樣一旦圖片下載成功莫其,我們就發(fā)一個通知并且傳遞一個已經(jīng)更新的對象(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
數(shù)組查找,一旦找到榜配,獲取對應的row,并且告訴tableView刷新該row.
現(xiàn)在編譯運行否纬,你會看到那些圖片被下載完后時不時或者突然出現(xiàn)在表視圖。
Grand Central Dispatch and Dispatch Queues, Oh My!
目前為止蛋褥,我們的App任然有一個問題。只要詳情頁一加載睛驳,如果你點擊Grab!
按鈕并且一直上下滑動烙心,在zip文件下載后,UI界面像冰凍一樣如果正在保存和解壓文件乏沸。這是因為淫茵,ASIHTTPRequest
的完成回調(diào)雖然是在主線程,但是我們處理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蝶缀〉び鳎基本的,無論什么時候你想在后臺跑一些東西翁都,你只需要調(diào)用dispatch_async
并且傳入對應參數(shù)即可碍论。GCD會為你處理所有---在它需要的時候它會創(chuàng)建新的線程,并且會重用那些過去的可用的(已經(jīng)創(chuàng)建過柄慰,并且已經(jīng)使用過鳍悠,但是截至目前又是空閑的)線程。
當你調(diào)用dispatch_async
,你傳入一個dispatch queue參數(shù)坐搔,你可以認為這是一個存儲你傳入的所有blocks的列表藏研,遵循先進先出
原則。
你也可以自己創(chuàng)建dispatch queue(通過dispatch_create)概行,或者你也可以(通過dispatch_get_main_queue)得到一個特殊的主線程的dispatch queue蠢挡。這里我們將創(chuàng)建一個用來在后臺執(zhí)行任務(解析HTML以及保存/解壓zip文件)的名叫“backgroundQueue”的dispatch queue。
Dispatch Queues, Locks, and Cat Food
調(diào)度隊列(dispatch queue)默認情況下是串行的占锯,回想我們最早關(guān)于貓的舉例袒哥,如果兩只貓同時想得到貓食盤會發(fā)生什么?這是個大問題消略。但是我們把所有的貓放在一條線上堡称,并且告訴它們“如果它們想接近貓食盤,你們不得不排成一隊”艺演,要是生活如此簡單鎖好却紧。
這也是最基本是想法使用調(diào)度隊列(dispatch queue)來保護數(shù)據(jù)桐臊。你設置你的代碼以致于特殊的數(shù)據(jù)只能被一個特殊的調(diào)度隊列(dispatch queue)訪問。這樣既然調(diào)度隊列(dispatch queue)串行運行blocks,就能保證同一時間只有一個調(diào)度隊列(dispatch queue)能訪問這個數(shù)據(jù)結(jié)構(gòu)晓殊。
在這個App中断凶,我們有2個數(shù)據(jù)結(jié)構(gòu)我們必須要保護:
ImageListViewController
里的imageInfos數(shù)組。為了保護它巫俺,我們將重構(gòu)我們的代碼以致于它只能在主線程中觸發(fā)认烁;ImageManager
里的pendingZips。為了保護它介汹,我將重構(gòu)我們的代碼以致于它只能在backgroundQueue中觸發(fā)却嗡。
圖片信息在主線程展示,而圖片獲取以及解壓在后臺處理嘹承。
關(guān)于GCD我們已經(jīng)談論不少了窗价,現(xiàn)在我們來嘗試嘗試它。
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)];
});
這些都是簡單的但是重要的調(diào)用,讓我們依次討論每一個:
- 創(chuàng)建一個隊列骤竹。當你創(chuàng)建一個隊列時你需要給它一個唯一字符串標示帝牡,創(chuàng)建唯一標示的一個好的方法是用反向DNS表示法,像這樣瘤载。
- 當你創(chuàng)建一個隊列的時候不要忘了釋放它否灾。對這個隊列,我們在
ImageManager
deallocated的時候釋放鸣奔。- 老的
process
方法直接運行processHTML
方法墨技,因此,在主線程運行它挎狸,當遇到解析HTML時扣汪,UI就會卡頓。現(xiàn)在我們在我們自己創(chuàng)建的backgroundQueue后臺運行锨匆,用dispatch_async簡單地調(diào)用崭别。- 與3相似,之前我們在zip文件下載完成通過
ASIHTTPRequeset
回調(diào)在主線程處理zip文件恐锣,現(xiàn)在我們把處理zip文件放到后臺茅主,就不會出現(xiàn)之前保存和解壓zip文件時UI卡頓的現(xiàn)象。確保變量pendingZips是受保護的是很重要的土榴。- 我們要確保在主線程的上下文調(diào)用代理方法诀姚。第一,確保
ImageListViewController
里的imageInfos數(shù)組只能通過主線程訪問玷禽,根據(jù)我們之前的戰(zhàn)略分析赫段;第二呀打,因為代理方法與UIKit對象有交互,而UIKit對象只能在主線程使用糯笙。
就這樣贬丛,編譯運行你的代碼,ImageGrabber
應該有更好更快的響應给涕!
But Wait!
如果你有iOS編程經(jīng)驗豺憔,你可能聽說過叫做NSOperations的神奇的東西,以及操作隊列(operation queues)稠炬。你可能好奇什么時候你應該用它們焕阿,什么時候你應該用GCD。實際上首启,NSOperations 是基于GCD的簡單API。這樣撤摸,當你使用 NSOperations 時毅桃,你實際上也是在使用GCD。NSOperations 僅僅是提供給你一些你可能喜歡的神奇的特性准夷。你可以創(chuàng)建一些operations依賴于其它的operations钥飞,在你提交items后重新排列隊列,還有其它的像這樣的事情衫嵌。
事實上读宙,ImageGrabber
已經(jīng)使用了NSOperations 和 operation queues!ASIHTTPRequest在底層使用它們楔绞,如果你喜歡结闸,你可以自己配置operationsy用作處理不同行為。
所以你應該使用哪一個?哪個適合你的應用程序酒朵。對這個程序我們直接使用GCD是相當簡單,不需要NSOperation的神奇的功能桦锄。但是如果你的App需要它們,就去使用吧!
Where To Go From Here?
這有一個簡單的工程,包含上面教程的所有代碼蔫耽。
現(xiàn)在為止结耀,你已經(jīng)有了在iOS上使用異步操作和GCD的實踐經(jīng)驗。但本教程還遠遠不夠——還有很多你可以學習!
我首先建議聽大蘋果關(guān)于GCD的視頻匙铡。WWDC2010
年和WWDC2011
年都有一些視頻,介紹的很不錯图甜。
如果你真的想學習GCD相關(guān)知識,Mike Ash 有一些好的關(guān)于GCD的文章你可以去看看.
如果你有任何問題鳖眼、意見或建議,請在下方留言加入論壇的討論!
團隊
在www.raywenderlich.com
上的每個教程都是由專門的團隊開發(fā)人員創(chuàng)建,以此來符合我們的高質(zhì)量的標準黑毅。團隊成員曾參與本教程是:
Ray Wenderlich
Follow Ray Wenderlich on Twitter