原文地址:html
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueuesios
本文由 大俠自來也(泰然教程組) 翻譯,轉載請註明出處!!!git
每一個人應該都有使用某款ios或者mac的app的時候出現未響應的現象吧。若是是mac下面的app,要是比較幸運的話,那還會出現無敵風火輪,直到你可以操做才消失。 若是是ios的app,就只能等了,有些時候還可能就這樣卡閃退了,這樣就會給用戶不好的用戶體驗。程序員
解釋這個現象卻是很簡單:就是你的app須要一些消耗大量cpu計算時間的任務的時候,在主線程裏面就基本上沒時間來處理你的UI交互了,因此看起來就卡了。github
通常的一個解決辦法就是經過併發處理來讓當前複雜的計算離開當前的主線程,也就是說使用多線程來執行你的任務。這樣的話,用戶交互就會有反應,不會出現卡的狀況。面試
還有一種在ios中併發處理的方法就是使用NSOperation和NSOperationQueue。在這篇教程裏面,你將會學習如何使用他們。爲了看到他的效果,首先咱們會建立一個一點也不使用多線程的app,因此你將會看到這個app運行時是如此的不流暢,交互性如此的很差。而後咱們會重寫這個app,這個時候會加上併發處理,會給你提供良好的人機交互感覺。編程
在開始這篇教程的時候,要是你去讀一下ios官方的多線程和GCD教程,會對你有很大的幫助的。不過這篇教程是比較簡單的,因此你也能夠不用去讀剛剛的那個教程,不過建議去看看,很好的。數組
背景xcode
在開始這篇教程的時候,有一些技術概念須要普及一下。你應該據說過併發處理和並行處理。從技術點上來看,併發是程序的性質,並行是硬件的性質。因此並行和併發實際上是兩個不一樣的概念。做爲一個程序員,你永遠不能保證你的代碼將會運行在一臺可以使用並行處理的的機器上。可是你能夠設計你的代碼以致於你可使用併發處理。(這裏簡單用一個比喻來講明一下併發和並行。併發就是:假若有三我的,每一個人一個水果,可是在他們面前只有一把水果刀,因而每一個人都會輪流來使用這把刀來削水果。並行就是:有三把水果刀了,每一個人均可以幹本身的事,而不用去等待別人。因此並行效率會很高,可是受硬件限制,併發其實就是多線程)。安全
首先,知道一些專業術語是很重要的:
做業: 一些須要被處理的簡單的工做。
線程: 在一個應用程序裏,由操做系統提供的,包含了不少可執行指令的集合。
進程: 一塊可執行的二進制代碼,由許多線程組成。
注意: 在iphone和mac上面,線程功能是由POSIX線程API(或者pthreads)提供的,而且也是操做系統的一部分。這個是至關底層的接口,因此使用的話,是很是容易犯錯的,並且這個錯誤是很難被找到的。
有一個基本的framework包含了一個叫NSThread的類,這個類很是容易使用,可是在管理多線程上面NSThread仍是比較頭疼。NSOperation 和NSOperationQueue是一個高度封裝的類,簡化了操做多線程的難度。
在下面這個圖標裏面,你可以看到進程,線程和做業的關係:
正如你看的同樣,一個進程包含了許多能夠執行的線程,與此同時每一個線程裏面又包含了許多做業。
從圖表裏面咱們能夠看到線程2執行了一個讀文件的操做,此時線程1執行了界面交互相關的代碼。這個例子就是告訴你在ios中如何構建本身的代碼,也就是說,在主線程裏面應該都是和界面交互的工做,在第二等級的線程裏面應該執行那些運行比較慢或者比較長的操做任務(例如讀取文件,或者網絡交互等)。
NSOperation vs. Grand Central Dispatch (GCD)
你可能據說過GCD。簡單的說,GCD就是包含了不少很好的特性,動態運行時庫,加強了系統在多核處理器硬件上的處理能力和對併發的支持能力。假如你想要了解更多GCD的知識,你能夠看看Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.
在mac os x 10.6和ios 4以前,NSOperation 和 NSOperationQueue是不一樣於GCD的,而且兩個使用徹底不一樣的機制。但從在mac os x 10.6和ios 4開始以後,NSOperation 和 NSOperationQueue就是構建在GCD之上了。就通常而言,要是人們有需求,蘋果推薦使用更高級別的抽象概念的時候,就會拋棄底級別的抽象概念。
這裏有一些GCD 和 NSOperation,NSOperationQueue的一些區別,這樣你就能夠決定何時使用什麼了:
GCD是用來呈現將要執行併發工做單元的一種輕量級的方式。你不用去安排這些工做單元,由於系統將會接管這個工做。不過增長依附於blocks可能會有一點頭疼,做爲一位開發者,取消或者掛起block須要一些額外的操做。
NSOperation 和 NSOperationQueue相比於GCD的話,是上升了一個等級的,你能夠依附於各類各樣的操做。你徹底能夠重用,取消或者掛起他們。並且NSOperation很是適合於KVO,例如,你能夠運行一個NSOperation來監聽NSNotificationCenter的消息。
初期項目規劃
在初期的項目規劃上,咱們使用一個dictionary來做爲一個table view的數據源。這個字典的key是一些圖片的名字,這個對應的value就是每一個圖片的地址。那麼目前這個項目的目標就是,讀取這個dictionary的內容,而後下載這些圖片,而後通過圖片濾鏡,最後顯示在table view上面.
下面是這個項目規劃示意圖:
實現規劃的——這應該是你首先接下來會作的
注意:假如你不想進行這個非多線程版本的項目,而是直接想看到多線程的好處,那麼你能夠跳過這節,在這裏下載這個咱們作好的項目文件。
打開xcode,建立一個空的應用程序模板(Empty Application template),命名爲ClassicPhotos,選擇Universal,也就是iphone、Ipad兼容模式。勾選上Use Automatic Reference Counting,其餘都不勾選上了,而後保存在喜歡的地方。
而後在工程導航欄上面選擇ClassicPhoto這個工程,而後在右邊選擇Targets\ ClassicPhotos\Build Phases, 而且展開 Link Binary with Libraries。點擊+按鈕,增長Core Image framework,由於咱們將會用到圖片濾鏡。
切換到AppDelegate.h,引入ListViewController,這個將會是root view controller,後面你會聲明他的,並且他也是UITableViewController子類。
#import 「ListViewController.h」
|
切換到AppDelegate.m,定位到application:didFinishLaunchingWithOptions:,實例化一個ListViewController的對象,而後設置他爲UIWindow的root view controller。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor];
/* ListViewController is a subclass of UITableViewController. We will display images in ListViewController. Here, we wrap our ListViewController in a UINavigationController, and set it as the root view controller. */
ListViewController *listViewController = [[ListViewController alloc] initWithStyle:UITableViewStylePlain]; UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:listViewController];
self.window.rootViewController = navController;
[self.window makeKeyAndVisible]; return YES; } |
注意:加入在這以前你尚未建立界面,這裏給你展現不使用Storyboards或者xib文件,而是程序來建立界面。在這篇教程裏面,咱們就簡單使用一下這樣的方式。
下面就建立一個UITableViewController的子類,命名爲ListViewController。切換到ListViewController.h,作一下修改:
// 1 #import <UIKit/UIKit.h> #import <CoreImage/CoreImage.h>
// 2 #define kDatasourceURLString @」http://www.raywenderlich.com/downloads/ClassicPhotosDictionary.plist」
// 3 @interface ListViewController : UITableViewController
// 4 @property (nonatomic, strong) NSDictionary *photos; // main data source of controller @end |
如今讓咱們來看看上面代碼的意思吧:
一、 引入UIKit and Core Image,也就是import 頭文件。
二、 爲了方便點,咱們就宏定義kDatasourceURLString這個是數據源的地址字符串。
三、 而後讓ListViewController成爲UITableViewController的子類,也就是替換NSObject 爲 UITableViewController。
四、 聲明一個NSDictionary的實例對象,這個也就是數據源。
如今切換到ListViewController.m,也作下面的改變:
@implementation ListViewController // 1 @synthesize photos = _photos;
#pragma mark - #pragma mark – Lazy instantiation
// 2 - (NSDictionary *)photos {
if (!_photos) { NSURL *dataSourceURL = [NSURL URLWithString:kDatasourceURLString]; _photos = [[NSDictionary alloc] initWithContentsOfURL:dataSourceURL]; } return _photos; }
#pragma mark - #pragma mark – Life cycle
- (void)viewDidLoad { // 3 self.title = @」Classic Photos」;
// 4 self.tableView.rowHeight = 80.0; [super viewDidLoad]; }
- (void)viewDidUnload { // 5 [self setPhotos:nil]; [super viewDidUnload]; }
#pragma mark - #pragma mark – UITableView data source and delegate methods
// 6 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger count = self.photos.count; return count; }
// 7 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 80.0; }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kCellIdentifier = @」Cell Identifier」; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; }
// 8 NSString *rowKey = [[self.photos allKeys] objectAtIndex:indexPath.row]; NSURL *imageURL = [NSURL URLWithString:[self.photos objectForKey:rowKey]]; NSData *imageData = [NSData dataWithContentsOfURL:imageURL]; UIImage *image = nil;
// 9 if (imageData) { UIImage *unfiltered_image = [UIImage imageWithData:imageData]; image = [self applySepiaFilterToImage:unfiltered_image]; }
cell.textLabel.text = rowKey; cell.imageView.image = image;
return cell; }
#pragma mark - #pragma mark – Image filtration
// 10 - (UIImage *)applySepiaFilterToImage:(UIImage *)image {
CIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)]; UIImage *sepiaImage = nil; CIContext *context = [CIContext contextWithOptions:nil]; CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil]; CIImage *outputImage = [filter outputImage]; CGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]]; sepiaImage = [UIImage imageWithCGImage:outputImageRef]; CGImageRelease(outputImageRef); return sepiaImage; }
@end |
上面增長了不少代碼,不要驚慌,咱們這就來解釋一下:
一、 Synthesize這個photos實例變量。
二、 這裏實際上是重寫了photos的get函數,而且在裏面實例化這個數據源對象。
三、 設置這個導航欄上的title。
四、 設置table view的行高爲80.0
五、 當這個ListViewController unloaded的時候,設置photos爲nil
六、 返回這個table view有多少行
七、 這個是UITableViewDelegate的可選的回調方法,而後設置每一行的高度都爲80.0,其實每一行默認的44.0的高度。
八、 取得這個dictionay的key,而後獲得value,就能夠獲得url了,而後使用nsdata來下載這個圖像。
九、 加入你已經成功下載這個數據,就能夠建立圖像,而且可使用深褐色的濾鏡來處理一下。
十、 這個方法就是對這個圖像使用深褐色的濾鏡。假如你想要知道更多關於Core Image filters的知識,你能夠看看Beginning Core Image in iOS 5 Tutorial.
那下面來試試。編譯運行。太爽了,深褐色圖像也出現了,可是彷佛他們出現的有點慢。不過要是你是一邊吃小吃,一邊等待,你也會以爲沒什麼問題,很是漂亮。
線程
正如咱們知道的同樣,每個app至少都有一個線程,那就是主線程。一個線程的工做就是執行一系列的指令。在Cocoa Touch裏面,主線程包含了應用程序的主循環。也就是幾乎全部的app的代碼都是在主線程裏面執行的,除非你特別的建立一個其餘的線程,而且在這個新線程裏面執行一些代碼。
線程有兩個特徵:
一、 每一個線程都有共同的權利來使用app的資源,不過除了局部變量。所以任何對象均可能潛在的被任何線程更改,使用。
二、 沒有辦法來估計一個線程將會運行多久,或者那個線程將會首先執行完。
所以,知道一些克服這些問題和避免一些不可料想的錯誤的技術是很重要的。下面就列舉一些app將會面對的一些問題和一些關於如何高效的處理這些問題建議。
Race Condition(資源競爭):實際上每一個線程都能訪問一樣的一塊內存,因此這樣就可能引發資源競爭。
當多線程併發訪問這個共享數據的時候,第一個訪問的這個內存的線程可能修改了這塊共享數據,並且咱們不能保證那個線程將會首先訪問。你可能會假設用一個本地變量來保存你這個線程所寫入這個共享內存的數據,可是可能就在你保存的這個時間,另一個線程已經改變了這個值了,這樣你的數據其實都已通過期了,不是最新的數據了。
假如你知道在你的代碼裏面會存在使用多線程來併發的讀寫一塊數據,那麼你應該使用mutex lock(互斥鎖)。Mutex(互斥)就是互相排斥的意思,你可使用「@synchronized 塊」來包裹你準備使用互斥鎖的實例變量。這樣你就能夠保證在同一個時間,只能有一個線程可以訪問那塊內存。
@synchronized (self) {
myClass.object = value;
}
上面代碼中的self叫「semaphore」(判斷信號),當一個線程執行到那段代碼的時候回去檢測是否其餘的線程在訪問本身的那段代碼,假如沒有線程在訪問,那麼他就會執行那個塊裏面的代碼,要是有其餘線程在訪問,他就會等待,直到這個互斥鎖變成無效的狀態,也就是沒人訪問了。
Atomicity(單元性的):你可能在property裏面已經使用過不少次「nonatomic」。當你聲明這個property爲「atomic」的時候,你通常應該使用「@synchronized 塊」來包裹你的代碼,這樣可使你的代碼線程安全了。固然這樣看的話,這個方法沒有增長一些額外高級的東西。爲了給你直觀的感覺,這裏給你一些atomic property粗略的實現方法:
// If you declare a property as atomic …
@property (atomic, retain) NSString *myString;
// … a rough implementation that the system generates automatically,
// looks like this:
- (NSString *)myString {
@synchronized (self) {
return [[myString retain] autorelease];
}
}
在這個代碼裏面,返回值執行「retain」 和 「autorelease」兩個方法,其實也是多線程訪問了,你也不想在訪問的時候釋放掉這塊內存,因此你首先retain這個值,而後把它放到自動釋放池去。你能夠去讀讀蘋果官方的線程安全的文章。這個真的很是值得去讀,裏面有許多ios程序員都沒注意到的一些細節。提一個專業意見:線程安全這塊能夠做爲面試題目來考察哦。
大多數的UIKit屬性都是沒有線程安全的。查看官方API文檔能夠確認這個類是否線程安全的。假如這個API文檔沒有說起,那麼你就應該假設他沒有線程安全。
一般來講,假如你正執行在子線程裏面,這個時候你要處理一些界面上的東西,使用performSelectorOnMainThread是很是好的。
Deadlock(死鎖):就是一直等待一個永遠也不會出現的條件,這樣就會一直等待,不會進行下一步。舉個例子,就像兩個線程每個都同時執行到一段代碼,而後每個線程將要等待另一個執行完成,而後解開這個鎖,可是這種狀況永遠也不會發生,因此這兩個線程都會死鎖。
Sleepy Time(未響應):這個通常是在同一時刻有太多的線程在執行,系統陷入了混亂,處理不過來了。NSOperationQueue有一個屬性能夠設置同時最大的併發線程數,這樣就不會出現這樣狀況。
NSOperation API
NSOperation類聲明的東西至關簡短。通常經過一下步驟來建立一個定製的操做:
1. 從NSOperation中派生一個子類
2. 重寫「main」函數
3. 在「main」函數中,建立一個「autoreleasepool」
4. 把你的代碼放到「autoreleasepool」中。
這裏建立你本身的autorelease pool的緣由是由於你不該該訪問主線程的autorelease pool,所以你應該本身建立一個,下面是一個例子:
#import <Foundation/Foundation.h>
@interface MyLengthyOperation: NSOperation @end |
@implementation MyLengthyOperation
- (void)main { // a lengthy operation @autoreleasepool { for (int i = 0 ; i < 10000 ; i++) { NSLog(@」%f」, sqrt(i)); } } }
@end
|
上面的例子展現了autorelease pool的ARC的語法結構。你應該很是明確咱們一直在使用ARC。
在線程操做中,你永遠也不知道這個操做何時執行,會執行多久。大多數的時候,你的線程是執行在後臺的,加入你忽然滑動開了,離開了這個頁面,可是你那個線程是會和這個界面相關的,因此這個線程不該該繼續執行了。解決這個的關鍵就是常常去檢查NSOperation類的isCancelled屬性。例如,在上面這個虛擬的例子代碼中,你應該這樣作:
@interface MyLengthyOperation: NSOperation @end
@implementation MyLengthyOperation - (void)main { // a lengthy operation @autoreleasepool { for (int i = 0 ; i < 10000 ; i++) {
// is this operation cancelled? if (self.isCancelled) break;
NSLog(@」%f」, sqrt(i)); } } } @end |
爲了取消這個操做,你應該調用NSOperation的取消方法,正以下面的:
// In your controller class, you create the NSOperation // Create the operation MyLengthyOperation *my_lengthy_operation = [[MyLengthyOperation alloc] init]; . . . // Cancel it [my_lengthy_operation cancel];
|
NSOperation類有一些其餘的方法和屬性:
Start:通常的,你不該該重寫這個方法。重寫「start」函數,須要不少複雜的實現,而且你不得不關心例如isExecuting, isFinished, isConcurrent, 和 isReady.這些屬性。當你把這個操做增長到一個隊列裏(也就是NSOperationQueue的實例對象,這個後面會討論的),這個隊列將會調用「start」函數,這樣將會致使作一些準備活動,接下來是「main」的執行。加入你的NSOperation的實例對象直接調用「start」函數,沒有增長到一個隊列裏面去,那麼這個操做將會運行在主循環裏面。
Dependency(依附):你能夠建立一個依附於其餘操做的操做。任何操做都是能夠依附於其餘操做的。當你建立一個A操做依附於B操做,即便你調用了A操做的「start」函數,他也不會當即執行,除非B操做的isFinished是true,也就是B操做完成了。例如:
MyDownloadOperation *downloadOp = [[MyDownloadOperation alloc] init]; // MyDownloadOperation is a subclass of NSOperation MyFilterOperation *filterOp = [[MyFilterOperation alloc] init]; // MyFilterOperation is a subclass of NSOperation
[filterOp addDependency:downloadOp]; |
移除依附:
[filterOp removeDependency:downloadOp]; |
Priority(優先級):有些時候這個後臺執行的操做不是很重要,能夠設置一個低一點的優先級。你可使用「setQueuePriority:」這個來設置優先級:
[filterOp setQueuePriority:NSOperationQueuePriorityVeryLow]; |
有一些可用現成的現成優先級的設置:NSOperationQueuePriorityLow, NSOperationQueuePriorityNormal, NSOperationQueuePriorityHigh, 和 NSOperationQueuePriorityVeryHigh.
當你增長操做到隊列裏面去的時候,在調用「start」以前,這個隊列會遍歷因此的操做。裏面優先級高的將會先執行,若是這個操做的優先級是相同的,那麼將會按照提交到隊列的順序來執行。
Completion block(完成塊):NSOperation類還有另一個有用的方法就是setCompletionBlock:。假如你想要在這個操做完成的時候作些什麼,你能夠把這個操做放到一個塊裏,而後傳遞給這個函數。可是注意,並不能保證這個塊將會在主線程中執行。
[filterOp setCompletionBlock: ^{ NSLog(@"Finished filtering an image."); }]; |
這裏有一些使用線程時額外的須要注意的地方:
1.假如你須要傳入一些值和指針給這個操做,最好的方法就是本身設計一個初始化函數:
#import <Foundation/Foundation.h>
@interface MyOperation : NSOperation
-(id)initWithNumber:(NSNumber *)start string:(NSString *)string;
@end |
2.假如你的操做須要返回值或者指針,最好的方法就是聲明一個delegate方法。記住啊,這個delegate方法必須在主函數中返回。然而,由於你子類化了NSOperation,因此你必須首先轉換這個操做類到NSObject。就像下面這樣作:
[(NSObject *)self.delegate performSelectorOnMainThread:(@selector(delegateMethod:)) withObject:object waitUntilDone:NO]; |
3.假如這個操做已經再也不須要了在後臺執行了,你須要常常檢查isCancelled屬性。
4.通常你是不須要重寫「start」方法的。然而,若是你真想重寫這個方法,你就不得不主要一些屬性,好比isExecuting, isFinished, isConcurrent, 和 isReady。不然,你的操做將不會正確的執行。
5.一旦你增長這個操做到一個隊列(NSOperationQueue的實例對象)裏去,而後你釋放了他(假如你沒有使用ARC)。NSOperationQueue會設定擁有這個操做,也就是說會讓這個操做的引用計數加一,這樣就不會釋放掉了, 而後就會調用這個操做的「start」函數,而且會在執行完以後釋放他。
6.你不能重用一個操做。一旦你把這個操做增長到了一個隊列,就算你沒有了這個操做的擁有權了,就交給系統了。假如你想要使用一樣一個操做,你就必須建立一個新的實例對象。
7.一個完成的操做不能被從新開始。就像你不能在結束函數裏面,在讓這個操做從頭運行一次,這樣是錯誤的。
8.假如你取消一個操做,這個將不會當即發生。通常是在未來的某個時刻,在「main」方法裏檢測到這個操做的isCancelled == YES,不然這個操做將會一直運行下去的。
9.不管一個操做是成功執行完,仍是不成功執完成,或者是被取消了,isFinished的值都是會被設置成YES的。所以,決不能假設isFinished == YES就意味着一切都執行好了, 特別是假如你的代碼依賴了這個isFinished,那就須要注意了。
NSOperationQueue API
NSOperationQueue的接口也是至關的簡單的。甚至比NSOperation都還簡單,由於你不須要子類化這個類,或者重寫任何方法,你只須要簡單建立一個就能夠了。比較好的作法就是給你的隊列名一個名字,這樣你能夠在運行時區分出你的操做隊列,而且方便調試:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; myQueue.name = @」Download Queue」; |
1. 併發操做:隊列和線程不是一回事。一個隊列可以有不少的線程。隊列裏面的每個操做都是執行在他本身的線程中。舉個例,你建立一個隊列,而後增長了三個操做到裏面。這個隊列將會開啓三個不一樣的線程,而後在他們本身的線程上執行全部的操做。
有多少的線程將會被建立?這是一個很好的問題。其實主要是和硬件有關。可是通常的,NSOperationQueue類將會在場景後面作許多神奇的事情,會決定怎麼樣會讓這個代碼在這個特別平臺上執行效率最高,所以會決定這個線程可能的最大數。
考慮一下的例子。假如這個系統是空閒的,而且有許多有效的資源(感受這裏資源就是,相似於內存不少,cup很空閒,能夠隨時進行計算),所以NSOperationQueue可能可以同時的啓動8個線程。在你下一次運行這個程序的時候,這個系統可能正在忙於其餘不相關的操做,這個時候NSOperationQueue就只能同時啓動2個線程。
2.最大的併發操做:你能夠設置NSOperationQueue可以併發執行的最大操做數。NSOperationQueue可能會選擇執行任意的併發操做,可是永遠不會超過設置的這個最大的數量。
myQueue.MaxConcurrentOperationCount = 3; |
假如你想設置MaxConcurrentOperationCount爲默認的數量,你能夠像下面這樣作:
myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; |
3.增長操做: 一旦一個操做被加到一個隊列裏面去了,你應該經過發送釋放消息給這個操做對象來解除這個擁有關係(假如你使用的是人工引用計數,沒有使用ARC),接下來這個隊列將會接管而且開始這個操做。因此說這個隊列會決定何時執行「start」。
[myQueue addOperation:downloadOp]; [downloadOp release]; // manual reference counting |
4.未完成的操做:在任什麼時候候你能夠詢問在這個隊列裏面有那些操做,一共有多少的操做。記住這一點,只有正在等待被執行和正在執行的操做能夠被獲得。一旦這個操做完成了,他就會從隊列裏面移除。
NSArray *active_and_pending_operations = myQueue.operations; NSInteger count_of_operations = myQueue.operationCount; |
5.暫停(掛起)隊列:你能夠經過設置setSuspended:YES來暫停一個隊列。這個將會把隊列裏面全部的操做掛起,注意不能單獨掛起操做。你只須要設置setSuspended:NO來恢復這個隊列。
// Suspend a queue [myQueue setSuspended:YES]; . . . // Resume a queue [myQueue setSuspended: NO]; |
5. 取消操做:想要取消隊列裏面全部的操做,你只須要簡單調用「cancelAllOperations」.可是你是否記起來前面咱們提醒你的代碼應該在NSOperation裏常常的檢查isCancelled的屬性?
主要緣由是「cancelAllOperations」效果不明顯,除了在隊列裏面的每一個操做裏面調用「cancel」方法,否則效果然的很差。假如一個操做尚未開始,你調用「cancel」方法,這個操做就將會被取消,而且從這個隊列裏面移除。然而假如一個操做已經在執行了,那麼只有這個單獨的線程本身察覺取消了(也就是檢查isCancelled屬性),而後中止正在執行的東西。
[myQueue cancelAllOperations]; |
6. addOperationWithBlock: 假如你有一個簡單的操做,並且不想子類化一個,那麼你能夠簡單的經過block方式來傳遞到一個隊列裏面。假如你想要從這個block裏面返回獲得一些數據,那麼請記住你不該該傳遞任何strong類型的指針到這個block中,相反的,你應該使用weak類型的指針。假如在這個block中你的操做是和UI相關的,你就必須在主線程中執行這個操做。
UIImage *myImage = nil;
// Create a weak reference __weak UIImage *myImage_weak = myImage;
// Add an operation as a block to a queue [myQueue addOperationWithBlock: ^ {
// a block of operation NSURL *aURL = [NSURL URLWithString:@"http://www.somewhere.com/image.png"]; NSError *error = nil; NSData *data = [NSData dataWithContentsOfURL:aURL options:nil error:&error]; If (!error) [myImage_weak imageWithData:data];
// Get hold of main queue (main thread) [[NSOperationQueue mainQueue] addOperationWithBlock: ^ { myImageView.image = myImage_weak; // updating UI }];
}]; |
從新定義模型
是時候從新定義最初的設計的模型了。假如你仔細看過以前的模型,你應該能夠看出有3個地方能夠改用線程的。經過拆解這三部分,把每個放到他們本身線程中去,這樣主線程的壓力就會減輕,就能夠專一於交互了。
注意:假如你不能直接的觀察到爲何你的app運行的如此之慢,有些時候這種狀況也不是很明顯,那麼你應該藉助一些工具。不過那就是須要另一個教程來介紹了。
爲了解決這些問題,你須要一個線程來負責交互相關的,一個線程專一於下載數據源和圖片,還有一個線程來執行圖片濾鏡。在這個新的模型裏面,這個app在主線程裏面啓動,加載一個空的table view。在這個時候,這個app啓動了第二個線程來下載數據源。
一旦這個數據源被下載下來以後,你應該告訴table view來從新加載數據。這些都是在主線程中完成的。這個時刻,table view知道有多少行,並且也知道須要展現圖片的ur,可是他卻沒有這個真正的圖片數據。假如這個時候你直接開始如今這個圖片,這將會很是糟糕的決定,其實這個時候你並不須要全部的圖片的。
那麼怎麼作纔是比較好的喃?
一個比較好的方式是隻開始下載將會顯示在屏幕上的圖片。所以你應該首先詢問這個table view那些行是可見的,而後纔是那些可見的行纔開始這個下載過程了。正如前面討論的,這個圖片濾鏡處理應該在這個圖片被下載完後,並且尚未被處理,才能開始這個圖片處理過程。
爲了讓這個app有更好的響應方式,這個代碼應該能夠先展現這個沒有處理的圖片。一旦這個圖片濾鏡處理完成,就能夠更新到這個UI上了。下面的圖表展現了這個整個原理的流程。
爲了完成這個目標,你須要跟蹤這些操做,是否正在下載圖片,是否已經下載完成,是否已經處理完圖片濾鏡了。你須要跟蹤這些每一步操做,看他在下載或者處理濾鏡的狀態,這樣你能夠在滑動界面的時候,取消,暫停或者恢復這些操做。
Okay,如今咱們開始編碼!
打開以前留下的那個工程,增長一個NSObject的子類,名字爲PhotoRecord的類。打開PhotoRecord.h,在頭文件裏面增長下面的:
#import <UIKit/UIKit.h> // because we need UIImage
@interface PhotoRecord : NSObject
@property (nonatomic, strong) NSString *name; // To store the name of image @property (nonatomic, strong) UIImage *image; // To store the actual image @property (nonatomic, strong) NSURL *URL; // To store the URL of the image @property (nonatomic, readonly) BOOL hasImage; // Return YES if image is downloaded. @property (nonatomic, getter = isFiltered) BOOL filtered; // Return YES if image is sepia-filtered @property (nonatomic, getter = isFailed) BOOL failed; // Return Yes if image failed to be downloaded
@end |
上面的語法看起來是否熟悉?每個屬性都有getter 和 setter方法。特別是有些的getter方法在這個屬性裏面特別的指出了這個方法的名字的。
切換到PhotoRecord.m,增長一下的:
@implementation PhotoRecord
@synthesize name = _name; @synthesize image = _image; @synthesize URL = _URL; @synthesize hasImage = _hasImage; @synthesize filtered = _filtered; @synthesize failed = _failed;
- (BOOL)hasImage { return _image != nil; }
- (BOOL)isFailed { return _failed; }
- (BOOL)isFiltered { return _filtered; }
@end |
爲了跟蹤每個操做的狀態,你須要另一個類,因此從NSObject派生一個名爲PendingOperations的子類。切換到這個PendingOperations.h,作下面的改變:
#import <Foundation/Foundation.h>
@interface PendingOperations : NSObject
@property (nonatomic, strong) NSMutableDictionary *downloadsInProgress; @property (nonatomic, strong) NSOperationQueue *downloadQueue;
@property (nonatomic, strong) NSMutableDictionary *filtrationsInProgress; @property (nonatomic, strong) NSOperationQueue *filtrationQueue;
@end
|
這個開起來也很簡單。你聲明瞭兩個字典來跟蹤下載和濾鏡時候活動仍是完成。這個字典的key和table view的每一行的indexPath有關係,字典的value將會分別是ImageDownloader 和 ImageFiltration的實例對象。
注意:你能夠想要直到爲何不得不跟蹤這個操做的活動和完成的狀態。難道不能夠簡單的經過在[NSOperationQueue operations]中查詢這些操做獲得這些數據麼?答案是固然能夠的,不過在這個工程中沒有必要這樣作。
每一個時候你須要可見行的indexPath同全部行的indexPath的比較,來獲得這個完成的操做,這樣你將須要不少迭代循環,這些操做都很費cpu的。經過聲明瞭一個額外的字典對象,你能夠方便的跟蹤這些操做,並且不須要這些無用的循環操做。
切換到PendingOperations.m,增長下面的:
@implementation PendingOperations @synthesize downloadsInProgress = _downloadsInProgress; @synthesize downloadQueue = _downloadQueue;
@synthesize filtrationsInProgress = _filtrationsInProgress; @synthesize filtrationQueue = _filtrationQueue;
- (NSMutableDictionary *)downloadsInProgress { if (!_downloadsInProgress) { _downloadsInProgress = [[NSMutableDictionary alloc] init]; } return _downloadsInProgress; }
- (NSOperationQueue *)downloadQueue { if (!_downloadQueue) { _downloadQueue = [[NSOperationQueue alloc] init]; _downloadQueue.name = @」Download Queue」; _downloadQueue.maxConcurrentOperationCount = 1; } return _downloadQueue; }
- (NSMutableDictionary *)filtrationsInProgress { if (!_filtrationsInProgress) { _filtrationsInProgress = [[NSMutableDictionary alloc] init]; } return _filtrationsInProgress; }
- (NSOperationQueue *)filtrationQueue { if (!_filtrationQueue) { _filtrationQueue = [[NSOperationQueue alloc] init]; _filtrationQueue.name = @」Image Filtration Queue」; _filtrationQueue.maxConcurrentOperationCount = 1; } return _filtrationQueue; }
@end |
這裏你重寫了一些getter方法,這樣直到他們被訪問的時候纔會實例化他們。這裏咱們也實例化兩個隊列,一個是下載的操做,一個濾鏡的操做,而且設置了他們的一些屬性,以致於你在其餘類中訪問這些變量的時候,不用去關心他們初始化。在這篇教程裏面,咱們設置了maxConcurrentOperationCount爲1.
如今是時候關心下載和濾鏡的操做了。建立一個NSOperation的子類,名叫ImageDownloader。切換到ImageDownloader.h,,增長下面的:
#import <Foundation/Foundation.h>
// 1 #import 「PhotoRecord.h」
// 2 @protocol ImageDownloaderDelegate;
@interface ImageDownloader : NSOperation
@property (nonatomic, assign) id <ImageDownloaderDelegate> delegate;
// 3 @property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readonly, strong) PhotoRecord *photoRecord;
// 4 - (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>) theDelegate;
@end
@protocol ImageDownloaderDelegate <NSObject>
// 5 - (void)imageDownloaderDidFinish:(ImageDownloader *)downloader; @end |
下面解釋一下上面相應編號處的代碼的意思:
1. 引入PhotoRecord.h,這樣當下載完成的時候,你能夠直接設置這個PhotoRecord的圖像屬性。假以下載失敗了,能夠設置失敗的值爲yes。
2. 聲明一個delegate,這樣一旦這個操做完成,你能夠通知這個調用者。
3. 聲明一個indexPathInTableView,這樣你能夠方便的直到調用者想要操做哪裏行。
4. 聲明一個特定的初始化方法。
5. 在你的delegate方法裏面,你傳遞了整個這個類給調用者,這樣調用者能夠訪問indexPathInTableView 和 photoRecor。由於你須要轉換這個操做爲一個對象,而且返回到主線程中,並且這裏這樣作有個好處,就是隻用返回一個變量。
Switch to ImageDownloader.m and make the following changes:
切換到ImageDownloader.m,作下面的改變:
// 1 @interface ImageDownloader () @property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readwrite, strong) PhotoRecord *photoRecord; @end
@implementation ImageDownloader @synthesize delegate = _delegate; @synthesize indexPathInTableView = _indexPathInTableView; @synthesize photoRecord = _photoRecord;
#pragma mark - #pragma mark – Life Cycle
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>)theDelegate {
if (self = [super init]) { // 2 self.delegate = theDelegate; self.indexPathInTableView = indexPath; self.photoRecord = record; } return self; }
#pragma mark - #pragma mark – Downloading image
// 3 - (void)main {
// 4 @autoreleasepool {
if (self.isCancelled) return;
NSData *imageData = [[NSData alloc] initWithContentsOfURL:self.photoRecord.URL];
if (self.isCancelled) { imageData = nil; return; }
if (imageData) { UIImage *downloadedImage = [UIImage imageWithData:imageData]; self.photoRecord.image = downloadedImage; } else { self.photoRecord.failed = YES; }
imageData = nil;
if (self.isCancelled) return;
// 5 [(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageDownloaderDidFinish:) withObject:self waitUntilDone:NO];
} }
@end |
解釋一下上面數字註釋的地方:
一、 聲明一個私有接口,這樣你能夠更改這個實例變量的屬性爲讀和寫。
二、 設置屬性
三、 有計劃的去檢查isCancelled,這樣能夠確保你儘量隨時能夠終止這個操做。
四、 蘋果建議使用@autoreleasepool塊來代替alloc和初始化NSAutoreleasePool,由於使用block有更高的效率。你也徹底可使用NSAutoreleasePool來代替的,這樣也是很好的。
五、 強制轉換爲NSObject對象,而且在主線程中通知這個調用者。
如今繼續建立一個NSOperation的子類來負責圖像濾鏡的功能!
建立一個NSOperation的子類,名爲ImageFiltration,打開ImageFiltration.h,而且作下面的修改。
// 1 #import <UIKit/UIKit.h> #import <CoreImage/CoreImage.h> #import 「PhotoRecord.h」
// 2 @protocol ImageFiltrationDelegate;
@interface ImageFiltration : NSOperation
@property (nonatomic, weak) id <ImageFiltrationDelegate> delegate; @property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readonly, strong) PhotoRecord *photoRecord;
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate;
@end
@protocol ImageFiltrationDelegate <NSObject> - (void)imageFiltrationDidFinish:(ImageFiltration *)filtration; @end |
又來解釋一下代碼:
一、 因爲你須要對UIImage實例對象直接操做圖片濾鏡,因此你須要導入UIKit和CoreImage frameworks。你也須要導入PhotoRecord。就像前面的ImageDownloader同樣,你想要調用者使用咱們定製的初始化方法。
二、 聲明一個delegate,當操做完成的時候,通知調用者。
切換到ImageFiltration.m,增長下面的代碼:
@interface ImageFiltration () @property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readwrite, strong) PhotoRecord *photoRecord; @end
@implementation ImageFiltration @synthesize indexPathInTableView = _indexPathInTableView; @synthesize photoRecord = _photoRecord; @synthesize delegate = _delegate;
#pragma mark - #pragma mark – Life cycle
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate {
if (self = [super init]) { self.photoRecord = record; self.indexPathInTableView = indexPath; self.delegate = theDelegate; } return self; }
#pragma mark - #pragma mark – Main operation
- (void)main { @autoreleasepool {
if (self.isCancelled) return;
if (!self.photoRecord.hasImage) return;
UIImage *rawImage = self.photoRecord.image; UIImage *processedImage = [self applySepiaFilterToImage:rawImage];
if (self.isCancelled) return;
if (processedImage) { self.photoRecord.image = processedImage; self.photoRecord.filtered = YES; [(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageFiltrationDidFinish:) withObject:self waitUntilDone:NO]; } }
}
#pragma mark - #pragma mark – Filtering image
- (UIImage *)applySepiaFilterToImage:(UIImage *)image {
// This is expensive + time consuming CIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)];
if (self.isCancelled) return nil;
UIImage *sepiaImage = nil; CIContext *context = [CIContext contextWithOptions:nil]; CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil]; CIImage *outputImage = [filter outputImage];
if (self.isCancelled) return nil;
// Create a CGImageRef from the context // This is an expensive + time consuming CGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]];
if (self.isCancelled) { CGImageRelease(outputImageRef); return nil; }
sepiaImage = [UIImage imageWithCGImage:outputImageRef]; CGImageRelease(outputImageRef); return sepiaImage; }
@end |
上面的實現方法和ImageDownloader的比較相似。圖像濾鏡的方法就是以前ListViewController.m中已經實現的那個方法,只是咱們放到這個地方,做爲一個在後臺單獨的操做。你也應該常常檢查isCancelled,一個好的編程習慣是通常在一個很消耗cpu的操做前調用這個檢查,能夠避免一些沒必要要的消耗。一旦這個圖像濾鏡完成,PhotoRecord的實例變量的值就在適當的時候設置爲這個新的,而且還須要通知主線程,完成了。
太好了!如今你已經有了全部的在後臺操做的工具和一些基礎了。是時候回到view controller了,而且適當的修改一下,這樣你就能夠利用這個新的特性了。
注意:在繼續進行工程前,你須要到GitHub去下載AFNetworking庫。
AFNetworking是構架於NSOperation 和 NSOperationQueue之上的。他提供了許多很方便的方法用於在後臺下載。蘋果也提供了NSURLConnection,這個也能夠用於咱們下載這個記錄了全部圖片的一張表的操做,可是你徹底沒有必要爲了這個表來作一些額外的工做,因此直接使用AFNetworking是很方便的。你只須要傳遞兩個block進來就能夠了,一個是當下載成功完成的時候,一個是當操做失敗的時候,後面會給你詳細說明的。
如今把這個庫增長到你的工程中,選擇File > Add Files To …,而後選擇到你下載下來的AFNetworking,而後點擊「Add」。這裏你要肯定勾選了「Copy items into destination group’s folder」。這裏咱們使用了ARC的,目前最新的AFNetworking已經支持ARC了,要是你使用的是之前手動管理內存的方法,你須要作一些更改,否則會有不少錯誤的。
在左上角點擊導航欄下面點擊「PhotoRecords」。而後在右邊,在「Targets」下面選擇「ClassicPhotos」。而後選擇「Build Phases」,在這個下面,展開「Compile Sources」。選擇全部屬於AFNetworking的文件,而後點擊Enter,彈出一個對話框,在這個對話框裏面輸入「fno-objc-arc」,點擊「Done」完成。其實在AFNetworking的Github上,這個說明很清楚的,能夠去看看。
切換到ListViewController.m,而且作下面的修改: // 1 #import <UIKit/UIKit.h> // #import <CoreImage/CoreImage.h> … you don’t need CoreImage here anymore. #import 「PhotoRecord.h」 #import 「PendingOperations.h」 #import 「ImageDownloader.h」 #import 「ImageFiltration.h」 // 2 #import 「AFNetworking/AFNetworking.h」
#define kDatasourceURLString @」https://sites.google.com/site/soheilsstudio/tutorials/nsoperationsampleproject/ClassicPhotosDictionary.plist」
// 3 @interface ListViewController : UITableViewController <ImageDownloaderDelegate, ImageFiltrationDelegate>
// 4 @property (nonatomic, strong) NSMutableArray *photos; // main data source of controller
// 5 @property (nonatomic, strong) PendingOperations *pendingOperations; @end |
下面又開始解釋代碼:
一、 在這個類裏面,咱們不須要CoreImage了,因此刪除他的頭文件,可是咱們須要導入PhotoRecord.h, PendingOperations.h, ImageDownloader.h 和 ImageFiltration.h。
二、 這裏涉及到了AFNetworking庫
三、 確保ListViewController包含了ImageDownloader 和 ImageFiltration delegate的方法。
四、 這裏你已經再也不須要數據源了。你將會建立一個使用圖片屬性表的PhotoRecord的實例對象。因此,你應該把「photos」從NSDictionary 變爲 NSMutableArray,這樣你能夠更新圖片數組。
五、 這個屬性用於跟蹤掛起的操做的。
切換到ListViewController.m,作下面的改變:
// Add this to the beginning of ListViewController.m @synthesize pendingOperations = _pendingOperations; . . . // Add this to viewDidUnload [self setPendingOperations:nil]; |
在簡單實例化「photos」以前,咱們先實例化「pendingOperations」:
- (PendingOperations *)pendingOperations { if (!_pendingOperations) { _pendingOperations = [[PendingOperations alloc] init]; } return _pendingOperations; } |
到實例化「photos」的地方,作下面的修改:
- (NSMutableArray *)photos {
if (!_photos) {
// 1 NSURL *datasourceURL = [NSURL URLWithString:kDatasourceURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:datasourceURL];
// 2 AFHTTPRequestOperation *datasource_download_operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
// 3 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
// 4 [datasource_download_operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// 5 NSData *datasource_data = (NSData *)responseObject; CFPropertyListRef plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, (__bridge CFDataRef)datasource_data, kCFPropertyListImmutable, NULL);
NSDictionary *datasource_dictionary = (__bridge NSDictionary *)plist;
// 6 NSMutableArray *records = [NSMutableArray array];
for (NSString *key in datasource_dictionary) { PhotoRecord *record = [[PhotoRecord alloc] init]; record.URL = [NSURL URLWithString:[datasource_dictionary objectForKey:key]]; record.name = key; [records addObject:record]; record = nil; }
// 7 self.photos = records;
CFRelease(plist);
[self.tableView reloadData]; [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
// 8 // Connection error message UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@」Oops!」 message:error.localizedDescription delegate:nil cancelButtonTitle:@」OK」 otherButtonTitles:nil]; [alert show]; alert = nil; [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; }];
// 9 [self.pendingOperations.downloadQueue addOperation:datasource_download_operation]; } return _photos; } |
上面的信息量有點大,咱們一步一步的來解釋:
一、 建立一個NSURL 和一個 NSURLRequest來指向數據的地址。
二、 使用到了AFHTTPRequestOperation類,新建而且使用了一個請求來初始化。
三、 給用戶反饋,當在下載這個數據的時候,激活網絡活動指示器。
四、 經過使用setCompletionBlockWithSuccess:failure:方法,咱們能夠增長兩個block:一個是成功的,一個是失敗的。
五、 在成功的block裏面,下載的這個圖片屬性錶轉化NSData,而後使用toll-free bridging(core foundation 和foundation之間的數據橋接,也就是c和objc的橋接)來轉化這個數據到CFDataRef 和 CFPropertyList,接着轉化到NSDictionary。
六、 新建一個NSMutableArray對象,遍歷這個字典的全部對象和key,經過這些對象和key新建一些PhotoRecord的實例對象,而且儲存在這個數組裏面。
七、 一旦遍歷完成,讓這個_photo變量指向這個記錄數組,而且重新加載table view,還須要中止網絡活動指示器。這裏你須要釋放掉「plist」這個實例變量。
八、 在這個失敗的block裏面,咱們將展現一個消息來通知用戶。
九、 最後,增長「datasource_download_operation」 到PendingOperations的 「downloadQueue」裏面去。
轉到tableView:cellForRowAtIndexPath:方法,作下面的修改:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kCellIdentifier = @」Cell Identifier」; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone;
// 1 UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; cell.accessoryView = activityIndicatorView;
}
// 2 PhotoRecord *aRecord = [self.photos objectAtIndex:indexPath.row];
// 3 if (aRecord.hasImage) {
[((UIActivityIndicatorView *)cell.accessoryView) stopAnimating]; cell.imageView.image = aRecord.image; cell.textLabel.text = aRecord.name;
} // 4 else if (aRecord.isFailed) { [((UIActivityIndicatorView *)cell.accessoryView) stopAnimating]; cell.imageView.image = [UIImage imageNamed:@"Failed.png"]; cell.textLabel.text = @」Failed to load」;
} // 5 else {
[((UIActivityIndicatorView *)cell.accessoryView) startAnimating]; cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"]; cell.textLabel.text = @」"; [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath]; }
return cell; } |
看看上面代碼,如今來解釋一下:
一、 建立了一個UIActivityIndicatorView,而且設置他爲這個cell的accessory view,用來提供一個反饋給用戶。
二、 數據源包含在PhotoRecord的實例對象中。經過indexpath裏面的row來獲得每個數據。
三、 檢查這個PhotoRecord。假如這個圖像被下載下來了,就顯示這個圖片,顯示圖片的名字,還要中止這個活動指示器。
四、 假以下載圖像失敗了,就在顯示一個失敗的圖片,來告訴用戶下載失敗了,而且要中止這個活動指示器。
五、 假如這個圖片尚未被下載下來。就開始下載和濾鏡的操做(他們尚未實現),這個時候顯示一個佔位的圖片和激活活動指示器來提醒用戶正在工做。
如今是時候實現咱們一直關注的開始操做的方法了。假如你尚未準備好,你能夠在ListViewController.m中刪除「applySepiaFilterToImage:」的之前實現方式。
到最下面的代碼的地方,實現下面的方法: // 1 - (void)startOperationsForPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {
// 2 if (!record.hasImage) { // 3 [self startImageDownloadingForRecord:record atIndexPath:indexPath];
}
if (!record.isFiltered) { [self startImageFiltrationForRecord:record atIndexPath:indexPath]; } } |
上面的代碼至關直接,可是也解釋一下上面作的:
一、 爲了簡單,咱們傳遞須要被操做的PhotoRecord的實例對象,順帶和對應的indexpath。
二、 檢查一下是否已經有圖片了。假若有,就能夠忽略那個方法了。
三、 假如尚未圖片,就調用startImageDownloadingForRecord:atIndexPath:來開始下載圖片的操做(後面將會簡單實現)。圖片濾鏡也是這樣操做的,假如尚未進行圖片過濾,就調用startImageFiltrationForRecord:atIndexPath:來過濾圖片(後面將會簡單實現)。
注意:這裏把圖像下載和濾鏡分開是有緣由的,假如這個圖片被下載下來了,可是用戶滑動了,這個圖片就看不到了,咱們將不會進行圖片濾鏡處理。可是下一次他又滑動回來了,這個時候,圖片咱們已經有了,就只須要進行圖片濾鏡的操做了。這樣會更有效率的。
如今咱們須要實現上面一小段帶面裏面的startImageDownloadingForRecord:atIndexPath:方法了。咱們以前建立一個跟蹤操做的一個類,PendingOperations,這裏咱們就會用到他:
- (void)startImageDownloadingForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath { // 1 if (![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPath]) {
// 2 // Start downloading ImageDownloader *imageDownloader = [[ImageDownloader alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self]; [self.pendingOperations.downloadsInProgress setObject:imageDownloader forKey:indexPath]; [self.pendingOperations.downloadQueue addOperation:imageDownloader]; } }
- (void)startImageFiltrationForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath { // 3 if (![self.pendingOperations.filtrationsInProgress.allKeys containsObject:indexPath]) {
// 4 // Start filtration ImageFiltration *imageFiltration = [[ImageFiltration alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self];
// 5 ImageDownloader *dependency = [self.pendingOperations.downloadsInProgress objectForKey:indexPath]; if (dependency) [imageFiltration addDependency:dependency];
[self.pendingOperations.filtrationsInProgress setObject:imageFiltration forKey:indexPath]; [self.pendingOperations.filtrationQueue addOperation:imageFiltration]; } } |
下面就解釋一下上面代碼作了什麼,確保你可以懂得。
一、 首先,檢查一下這個indexpath的操做是否已經在downloadsInProgress裏面了。假如在就能夠忽略掉。
二、 假如沒有在,就建立一個ImageDownloader的實例對象,而且設置他的delegate爲ListViewController。咱們還會傳遞這個indexpath和PhotoRecord的實例變量,而後把這個實例對象增長到下載隊列。你也須要把他增長到downloadsInProgress裏面去保持跟蹤。
三、 類似的,也這樣去檢查圖片是否被過濾了。
四、 假如沒有被過濾,那麼也就初始化一個。
五、 這裏有一點考慮的,你必須檢查是否這個indexpath對應的已經被掛起來下載了,假如是這樣的,那麼就使你的過濾操做依附於他。不然就能夠不依附了。
太好了。你如今須要實現這個delegate的ImageDownloader 和 ImageFiltration方法了。把下面這些增長到ListViewController.m中去:
- (void)imageDownloaderDidFinish:(ImageDownloader *)downloader {
// 1 NSIndexPath *indexPath = downloader.indexPathInTableView; // 2 PhotoRecord *theRecord = downloader.photoRecord; // 3 [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; // 4 [self.pendingOperations.downloadsInProgress removeObjectForKey:indexPath]; }
- (void)imageFiltrationDidFinish:(ImageFiltration *)filtration { NSIndexPath *indexPath = filtration.indexPathInTableView; PhotoRecord *theRecord = filtration.photoRecord;
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; [self.pendingOperations.filtrationsInProgress removeObjectForKey:indexPath]; } |
這兩個delegate 方法實現方法很是類似,所以就解釋其中一個就能夠了:
一、 獲得這個操做的indexpath,不管他是下載仍是濾鏡的。
二、 獲得PhotoRecord的實例
三、 更新UI
四、 從downloadsInProgress (或者 filtrationsInProgress)裏面移除這個操做。
更新:爲了可以更好的掌控PhotoRecord。由於你傳遞PhotoRecord的指針到ImageDownloader 和 ImageFiltration中,你能夠隨時直接修改的。所以,使用replaceObjectAtIndex:withObject:方法來更新數據源。詳情見最終的工程。
酷哦!
Wow!咱們成功了!編譯運行,你如今操做一下,發現這個app都不卡,而且下載圖片和圖片濾鏡均可用的。
難道這個還不酷麼?咱們還能夠作點改變,這樣咱們的app能夠有更好的人機交互和性能。
簡單調整
咱們已經經歷了一個很長的教程!如今的工程相比之前的已經作了不少的改變了。可是有一個細節咱們尚未注意到。咱們是想要成爲一個偉大的程序員,而不是一個好的程序員。因此咱們應該改掉這個。你可能已經注意到了,當咱們滾動這個table view的時候,下載和圖片濾鏡依然在運行。因此咱們應該在滑動的時候取消這些東西。
回到xcode,切換到ListViewController.m。轉到tableView:cellForRowAtIndexPath:的實現方法的地方,將[self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];用一個if包裹起來:
// in implementation of tableView:cellForRowAtIndexPath: if (!tableView.dragging && !tableView.decelerating) { [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath]; } |
這樣就是當這個table view 不滾動的時候纔開始操做。這些其實UIScrollView的屬性,可是UITableView是繼承至UIScrollView,因此他也就自動繼承了這些屬性。
如今到ListViewController.m的最下面,實現下面的UIScrollView的delegate方法:
#pragma mark - #pragma mark – UIScrollView delegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // 1 [self suspendAllOperations]; }
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { // 2 if (!decelerate) { [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } }
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // 3 [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } |
快速看看上面代碼展現了什麼:
一、 一旦用戶開始滑動了,你就應將全部的操做掛起。後面將會實現suspendAllOperations方法。
二、 假如decelerate的值是NO,那就意味着用戶中止拖動這個table view了。所以你想要恢復掛起的操做,取消那些屏幕外面的cell的操做,開始屏幕內的cell的操做。後面將會實現loadImagesForOnscreenCells 和 resumeAllOperations的方法。
三、 這個delegate方法是告訴你table view中止滾動了,所作的和第二步同樣的作法。
如今就來實現suspendAllOperations, resumeAllOperations, loadImagesForOnscreenCells方法,把下面的加到ListViewController.m的下面:
#pragma mark - #pragma mark – Cancelling, suspending, resuming queues / operations
- (void)suspendAllOperations { [self.pendingOperations.downloadQueue setSuspended:YES]; [self.pendingOperations.filtrationQueue setSuspended:YES]; }
- (void)resumeAllOperations { [self.pendingOperations.downloadQueue setSuspended:NO]; [self.pendingOperations.filtrationQueue setSuspended:NO]; }
- (void)cancelAllOperations { [self.pendingOperations.downloadQueue cancelAllOperations]; [self.pendingOperations.filtrationQueue cancelAllOperations]; }
- (void)loadImagesForOnscreenCells {
// 1 NSSet *visibleRows = [NSSet setWithArray:[self.tableView indexPathsForVisibleRows]];
// 2 NSMutableSet *pendingOperations = [NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgress allKeys]]; [pendingOperations addObjectsFromArray:[self.pendingOperations.filtrationsInProgress allKeys]];
NSMutableSet *toBeCancelled = [pendingOperations mutableCopy]; NSMutableSet *toBeStarted = [visibleRows mutableCopy];
// 3 [toBeStarted minusSet:pendingOperations]; // 4 [toBeCancelled minusSet:visibleRows];
// 5 for (NSIndexPath *anIndexPath in toBeCancelled) {
ImageDownloader *pendingDownload = [self.pendingOperations.downloadsInProgress objectForKey:anIndexPath]; [pendingDownload cancel]; [self.pendingOperations.downloadsInProgress removeObjectForKey:anIndexPath];
ImageFiltration *pendingFiltration = [self.pendingOperations.filtrationsInProgress objectForKey:anIndexPath]; [pendingFiltration cancel]; [self.pendingOperations.filtrationsInProgress removeObjectForKey:anIndexPath]; } toBeCancelled = nil;
// 6 for (NSIndexPath *anIndexPath in toBeStarted) {
PhotoRecord *recordToProcess = [self.photos objectAtIndex:anIndexPath.row]; [self startOperationsForPhotoRecord:recordToProcess atIndexPath:anIndexPath]; } toBeStarted = nil;
} |
suspendAllOperations, resumeAllOperations 和 cancelAllOperations都是一些簡單的實現。你通常想要使用工廠方法來掛起,恢復或者取消這些操做和隊列。可是爲了方便,把他們放到每個單獨的方法裏面。
LoadImagesForOnscreenCells是有一點複雜,下面就解釋一下:
一、 獲得可見的行
二、 獲得全部掛起的操做(包括下載和圖片濾鏡的)
三、 獲得須要被操做的行 = 可見的 – 掛起的
四、 獲得須要被取消的行 = 掛起的 – 可見的
五、 遍歷須要取消的,取消他們,而且從PendingOperations裏面移除。
六、 遍歷須要被開始,每個調用startOperationsForPhotoRecord:atIndexPath:方法。
最後一個須要解決的就是解決ListViewController.m中的didReceiveMemoryWarning方法。
// If app receive memory warning, cancel all operations - (void)didReceiveMemoryWarning { [self cancelAllOperations]; [super didReceiveMemoryWarning]; } |
編譯運行,你應該有一個更好的響應,更好的資源管理程序了。慢慢歡呼吧!
何去何從?
這裏是這個完整的工程。
假如你完成了這個工程,而且花了一些時間懂得這些,那麼祝賀你!你已經比開始這個教程的時候懂得了不少。要想徹底懂得這些東西,你須要瞭解和作不少工做的。線程其實也是有些微的bug,,可是通常都不容易出現,可能會在網絡很是慢,代碼運行在很快或者很慢的設備,或者在多核設備上出現bug。測試須要很是的仔細,而且通常須要藉助工具或者你觀察來覈查這個線程來作一些修改。