如何使用NSOperations和NSOperationQueues
- Tweet
第一部分
學習如何在你的app中使用NSOperations!
這篇博客是由iOS個人開發者Soheil Moayedi Azarpour發布的。
每個人都會在使用iOS或者Mac app,點擊按鈕或者輸入文本時,有過讓人沮喪的經歷,突然間,用戶交互界面停止了響應。
你真幸運 – 你只能盯著沙漏或者旋轉的風火輪一段時間直到能夠再次和UI界面交互為止!挺討厭的,不是嗎?
在一款移動端iOS程序中,用戶期望你的app可以即時地響應他們的觸摸操作,然而當它不響應時,app就會讓人覺得反應遲鈍,通常會導致不好的評價。
然而說的容易做就難。一旦你的app需要執行多個任務,事情很快就會變得復雜起來。在主運行回路中并沒有很多時間去執行繁重的工作,并且還有一直提供可響應的UI界面。
可憐的開發者要怎么做呢?一種方法是通過并發操作將部分任務從主線程中撤離。并發操作意味著你的程序可以在操作中同時執行多個流(或者線程)- 這樣,當你執行任務時,交互界面可以保持響應。
一種在iOS中執行并發操作的方法,是使用NSOperation和NSOperationQueue類。在本教程中,你將學習如何使用它們!你會先創建一款不使用多線程的app,這樣它會變得響應非常遲鈍。然后改進程序,添加上并行操作 – 并且希望 – 可以提供一個交互響應更好的界面給用戶!
在開始閱讀這篇教程之前,先閱讀我們的?Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial會很有幫助。然而,因為本篇教程比較通俗易懂,所以也可以不必閱讀這篇文章。
背景知識
在你學習這篇教程之前,有幾個技術概念需要先解決下。
也許你聽說過并發和并行操作。從技術角度來看,并發是程序的屬性,而并行運作是機器的屬性。并行和并發是兩種分開的概念。作為程序員,你不能保證你的代碼會在能并行執行你的代碼的機器上運行。然而,你可以設計你的代碼,讓它使用并發操作。
首先,有必要定義幾個術語:
- 任務:一項需要完成的,簡單,單一的任務。
- 線程:一種由操作系統提供的機制,允許多條指令在一個單獨的程序中同時執行。
- 進程:一段可執行的代碼,它可以由幾個線程組成。
注意:在iPhone和Mac中,線程功能是由POSIX Threads API(或者pthreads)提供的,它是操作系統的一部分。這是相當底層的東西,你會發現很容易犯錯;也許線程最壞的地方就是那些極難被發現的錯誤吧!
Foundation 框架包含了一個叫做NSThread的類,他更容易處理,但是使用NSThread管理多個線程仍然是件令人頭疼的事情。NSOperation和NSOperationQueue是更高級別的類,他們大大簡化了處理多個線程的過程。
在這張圖中,你可以看到進程,線程和任務之間的關系:
進程,線程和任務
正如你看到的,一個進程包含多個可執行的線程,而且每個線程可以同時執行多項任務。
在這張圖中,線程2執行了讀文件的操作,而線程1執行了用戶界面相關的代碼。這跟你在iOS中構建你的代碼很相似 – 主線程應該執行任何與用戶界面有關的任務,然后二級線程應該執行緩慢的或者長時間的操作(例如讀文件,訪問網絡,等等。)
NSOperation vs. Grand Central Dispatch (GCD)
你也許聽說過?Grand Central Dispatch (GCD)。簡而言之,GCD包含語言特性,運行時刻庫和系統增強(提供系統性和綜合性的提升,從而在iOS和OS X的多核硬件上支持并發操作)。如果你希望更多的了解GCD,你可以閱讀我們的Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial教程。
在Mac OS X v10.6和iOS4之前,NSOperation 與 NSOperationQueue 不同于GCD,他們使用了完全不同的機制。從Mac OS X v10.6和iOS4開始,NSOperation 和 NSOperationQueue是建立在GCD上的。作為一種通例,蘋果推薦使用最高級別的抽象,然而當評估顯示有需要時,會突然降到更低級別。
以下是對兩者的快速比較,它會幫助你決定何時何地去使用GCD或者NSOperation和NSOperationQueue;
- GCD是一種輕量級的方法來代表將要被并發執行的任務單位。你并不需要去計劃這些任務單位;系統會為你做計劃。在塊(block)中添加依賴會是一件令人頭疼的事情。取消或者暫停一個塊會給一個開發者產生額外的工作!:]
- NSOperation和NSOperationQueue 對比GCD會帶來一點額外的系統開銷,但是你可以在多個操作(operation)中添加附屬。你可以重用操作,取消或者暫停他們。NSOperation和?Key-Value Observation (KVO)是兼容的;例如,你可以通過監聽NSNotificationCenter去讓一個操作開始執行。
初步的工程模型
在工程的初步模型中,你有一個由字典作為其數據來源的table view。字典的關鍵字是圖片的名字,每個關鍵字的值是圖片所在的URL地址。本工程的目標是讀取字典的內容,下載圖片,應用圖片濾鏡操作,最后在table view中顯示圖片。
以下是該模型的示意圖:
初步模型
實現 – 你可能會首先想到的方法…
注意:
如果你不想先創建一個非線程版本的工程,而是想直接進入多線程方向,你可以跳過這一節,下載我們在本節中創建的第一版本工程。
所有的圖片來自stock.xchng。在數據源中的某些圖片是有意命名錯誤,這樣就有例子去測試下載圖片失敗的情況。
啟動Xcode并使用iOSApplicationEmpty Application模版創建一個新工程,然后點擊下一步。將它命名為ClassicPhotos。選擇Universal, 勾選上Use Automatic Reference Counting(其他都不要選),然后點擊下一步。將工程保存到任意位置。
從Project Navigator中選擇ClassicPhoto工程。選擇Targets ClassicPhotosBuild Phases 然后展開Link Binary with Libraries。使用+按鈕添加Core Image framework(你將需要Core Image來做圖像濾鏡處理)。
在Project Navigator中切換到AppDelegate.h 文件,然后導入ListViewController文件 — 它將會作為root view controller,接下來你會定義它。ListViewController是UITableViewController的子類。
| #import "ListViewController.h" |
切換到AppDelegate.m文件,找到application:didFinishLaunchingWithOptions:方法。Init和alloc一個ListViewController的實例變量。將它包在UINavigationController中,然后設置它為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或者Interface Builder的情況下,用純代碼形式去創建一個用戶界面的方法。
接下來創建一個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 |
讓我們一段一段地過一遍上面的代碼:
現在,切換到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 {// 3self.title = @"Classic Photos";// 4self.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;}// 8NSString *rowKey = [[self.photos allKeys] objectAtIndex:indexPath.row];NSURL *imageURL = [NSURL URLWithString:[self.photos objectForKey:rowKey]];NSData *imageData = [NSData dataWithContentsOfURL:imageURL];UIImage *image = nil;// 9if (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 |
好的!這里做了很多事情。別害怕 – 以下是對代碼原理的解釋:
就是這樣!試試吧!編譯運行工程。很美,褐色的照片 – 但是…他們..看起來…很…慢! 雖然很好看,但是你只能在等待圖片加載的時候去吃點零食打發時間了. :]
ClassicPhotos (緩慢的版本)
是時候想想如何提升用戶體驗了!
線程
每一個應用程序至少有一個主線程。線程的工作就是去執行一系列的指令。在Cocoa Touch中,主線程包含應用程序的主運行回路。幾乎所有你寫的代碼都會在主線程中執行,除非你特別創建了一個單獨的線程,并在這個新線程中執行代碼。
線程有兩個顯著的特征:
所以,知道這些技術很重要,它們可以去攻克難點,防止意外的錯誤!:] 以下是對多線程應用時面臨的挑戰介紹 – 以及一些如何有效解決它們的提示。
- 資源競爭:當每個線程都去訪問同一段內存時,會導致所謂的資源競爭問題。當有多個并發線程訪問共享數據時,首先訪問內存數據的線程會改變共享數據 – 而且并不能保證哪個線程會首先訪問到內存數據。你也許會假設有一個局部變量擁有你的線程最后一次寫到共享內存的值,但是另一個線程也許會同時改變了共享內存的數據,然后你的局部變量就過時了!如果你知道這種情況會存在你的代碼中(例如你會從多個線程同時讀/寫數據),就應該使用互斥鎖。互斥代表互相排斥。你可以通過使用 “@synchronized block”將實例變量包圍起來,創建一個互斥鎖。這樣你就可以確保在互斥鎖中的代碼一次只能被一個線程訪問:
@synchronized (self) { myClass.object = value; } 在以上代碼中“Self”被稱為一個“信號量”。當一個線程要范圍這段代碼時,它會檢查其他的線程是否也在訪問“self”。如果沒有線程在訪問“self”,這塊代碼會被執行;否則這段線程會被限制訪問直到這個互斥鎖解除為止。
- 原子性:你也許在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 properties都不是線程安全的。想看下一個類是否是線程安全的,可以看看API文檔。如果API文檔沒有提到任何關于線程安全的內容,你可以假設這個類是非線程安全的。
按常規,如果你正在執行一個二級的線程,而且你要對UIKit對象做操作,可以使用performSelectorOnMainThread。
- 死鎖:一個線程被停滯,無限期地等待永遠不會發生的條件。例如,如果兩個線程在互相執行synchronized代碼,每一個線程就會等待另一個線程完成并且打開鎖。但是這種情況永遠不會發生,這樣兩個線程都會成為死鎖。
- 困乏時間:這會發生在有太多的線程同時執行,系統會停滯不前。NSOperationQueue有一個屬性,讓你設置并發線程的數量。
NSOperation API
NSOperation 類有一個相當簡短的聲明。要定制一個操作,可以遵循以下步驟:
創建你自己的自動釋放池的原因是,你不能訪問主線程的自動釋放池,所以你應該自己創建一個。以下是一個例子:
| #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 |
上面的例子代碼展示了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的cancel方法,展示如下:
| // 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”方法,如果沒有把它添加到一個隊列中,這個操作會在main loop中執行。
- 從屬性(Dependency):你可以讓一個操作從屬于其他的操作。任何操作都可以從屬于任意數量的操作。當你讓操作A從屬于操作B時,即使你調用了操作A的“start”方法,它會等待操作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”方法之前,NSOperationQueue會瀏覽所有的操作。那些有較高優先級的操作會被先執行。有同等優先級的操作會按照添加到隊列中的順序去執行(先進先出)。
(歷史注釋:在1997年,火星車中的嵌入式系統遭遇過優先級反轉問題,也許這是說明正確處理優先級和互斥鎖的最昂貴示例了。想對這一事件的背景知識有更多的了解,可以看這個網址:?http://research.microsoft.com/en-us/um/people/mbj/Mars_Pathfinder/Mars_Pathfinder.html?) - Completion block:在NSOperation 類中另一個有用的方法叫setCompletionBlock:。一旦操作完成了,如果你還有一些事情想做,你可以把它放在一個塊中,并且傳遞給這個方法。這個塊會在主線程中執行。
[filterOp removeDependency:downloadOp];
其他一些關于處理線程的提示:
- 如果你需要傳遞一些值和指針到一個線程中,創建你自己的指定初始化方法是一個很好的嘗試:
#import Foundation/Foundation.h@interface MyOperation : NSOperation-(id)initWithNumber:(NSNumber *)start string:(NSString *)string;@end - 如果你的操作需要有一個返回值或者對象,聲明一個委托方法是不錯的選擇。記住委托方法必須在主線程中返回。然而,因為你要繼承NSOperation類,你必須先將這個操作類強制轉換為NSObject對象??梢园凑找韵虏襟E去做:
[(NSObject *)self.delegate performSelectorOnMainThread:(@selector(delegateMethod:)) withObject:object waitUntilDone:NO]; - 要經常檢查isCancelled屬性。如果操作不需要被執行了,你就不想在后臺去運行它了!
- 你并不需要重寫“start”方法。然而,如果你決定去重寫“start”方法,就必須處理好像isExecuting, isFinished, isConcurrent 和 isReady這些屬性。否則你的操作類不會正確的運作。
- 你一旦添加了一個操作到一個隊列(NSOperationQueue的一個實例)中,就要負責釋放它(如果你不使用ARC的話)。NSOperationQueue 獲得操作對象的所有權,調用“start”方法,然后結束時負責釋放它。
- 你不能重用一個操作對象。一旦它被添加到一個隊列中,你就喪失了對它的所有權。如果你想再使用同一個操作類,就必須創建一個新的實例變量。
- 一個結束的操作不能被重啟。
- 如果你取消了一個操作,它不會馬上就發生。它會在未來的某個時候某人在“main”函數中明確地檢查isCancelled == YES 時被取消掉;否則,操作會一直執行到完成為止。
- 一個操作是否成功地完成,失敗了,或者是被取消了,isFinished的值總會被設置為YES。所以千萬不要覺得isFinished == YES就表示所有的事情都順利完成了 — 特別的,如果你在代碼里面有從屬性(dependencies),就要更加注意!
NSOperationQueue API
NSOperationQueue 也有一個相當簡單的界面。它甚至比NSOperation還要簡單,因為你不需要去繼承它,或者重寫任何的方法 — 你可以簡單創建一個。給你的隊列起一個名字會是一個不錯的做法;這樣你可以在運行時識別出你的操作隊列,并且讓調試變得更簡單:
| NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; myQueue.name = @"Download Queue"; |
- ?并發操作:隊列和線程是兩個不同的概念。一個隊列可以有多個線程。每個隊列中的操作會在所屬的線程中運行。舉個例子你創建一個隊列,然后添加三個操作到里面。隊列會發起三個單獨的線程,然后讓所有操作在各自的線程中并發運行。
到底有多少個線程會被創建?這是個很好的問題!:] 這取決與硬件。默認情況下,NSOperationQueue類會在場景背后施展一些魔法,決定如何在特定的平臺下運行代碼是最好的,并且會盡量啟用最大的線程數量??紤]以下的例子。假設系統是空閑的,并且有很多的可用資源,這樣NSOperationQueue會啟用比如8個同步線程。下次你運行程序,系統會忙于處理其他不相關的操作,它們消耗著資源,然后NSOperationQueue只會啟用兩個同步線程了。 - 并發操作的最大值:你可以設定NSOperationQueue可以并發運行的最大操作數。NSOperationQueue會選擇去運行任何數量的并發操作,但是不會超過最大值。
myQueue.MaxConcurrentOperationCount = 3; 如果你改變了主意,想將MaxConcurrentOperationCount設置回默認值,你可以執行下列操作:
myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; - 添加操作:一個操作一旦被添加到一個隊列中,你就應該通過傳送一個release消息給操作對象(如果使用了手動引用計數,非ARC的話),然后隊列會負責開始這個操作。從這點上看,什么時候調用“start”方法由這個隊列說了算。
[myQueue addOperation:downloadOp]; [downloadOp release]; // manual reference counting - 待處理的操作:任何時候你可以詢問一個隊列哪個操作在里面,并且總共有多少個操作在里面。記住只有那些等待被執行的操作,還有那些正在運行的操作,會被保留在隊列中。操作一完成,就會退出隊列。
NSArray *active_and_pending_operations = myQueue.operations; NSInteger count_of_operations = myQueue.operationCount; - 暫停隊列:你可以通過設定setSuspended:YES來暫停一個隊列。這樣會暫停所有在隊列中的操作 — 你不能單獨的暫停操作。要重新開始隊列,只要簡單的setSuspended:NO。
// Suspend a queue [myQueue setSuspended:YES]; . . . // Resume a queue [myQueue setSuspended: NO]; - 取消操作:要取消一個隊列中的所有操作,你只要簡單的調用“cancelAllOperations”方法即可。還記得之前提醒過經常檢查NSOperation中的isCancelled屬性嗎?
原因是“cancelAllOperations”并沒有做太多的工作,他只是對隊列中的每一個操作調用“cancel”方法 — 這并沒有起很大作用!:] 如果一個操作并沒有開始,然后你對它調用“cancel”方法,操作會被取消,并從隊列中移除。然而,如果一個操作已經在執行了,這就要由單獨的操作去識別撤銷(通過檢查isCancelled屬性)然后停止它所做的工作。
| [myQueue cancelAllOperations]; |
- addOperationWithBlock: 如果你有一個簡單的操作不需要被繼承,你可以將它當做一個塊(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 operationNSURL *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}];}];
?
重新定義模型
是時候重新定義初步的非線程模型了!如果你仔細看下初步的模型,你會看到有三個線程區域可以改進。通過把這三個區域區分開來,然后把它們各自放在一個單獨的線程中,主線程會獲得解脫,并且可以保持對用戶交互的迅速響應。
注意:如果你不能馬上理解為什么你的app運作得這么慢 — 而且有時候這并不明顯 — 你應該使用Instruments工具。然而,這需要另一篇教程去講解它了!:]
改進的模型
為了擺脫你的程序的瓶頸限制,你需要一個特定的線程去響應用戶交互事件,一個線程專門用于下載數據源和圖片,還有一個線程用于執行圖片濾鏡處理。在新的模型中,app在主線程中開始,并且加載一個空白的table view。同時,app會開始另一個線程去下載數據源。
一旦數據源下載完畢,你會告訴table view重新加載自己。這會在主線程中完成。這個時候,table view知道有多少行,而且知道需要顯示的圖片的URL地址,但是它還沒有實際的圖片!如果你在這個時候馬上開始下載所有的圖片,這會非常沒有效率,因為你一下子不需要所有的圖片!
怎樣可以把它弄得更好?
一個更好的模型就是去下載在當前屏幕可見的row的圖片。所以你的代碼首先會問table view哪些row是可見的,然后才會開始下載過程。還有,圖片濾鏡處理會在圖片下載完成后才開始。因此,代碼應該等待出現有一個待濾鏡處理的圖片時才開始進行圖片濾鏡處理。
為了讓app的反應變得更加靈敏,代碼會在圖片下載完畢后馬上顯示,而不會等待進行濾鏡處理。一旦圖片的濾鏡處理完成,就會更新UI以顯示濾鏡處理過的圖片。以下是整個處理過程的控制流示意圖:
控制流程
為了達到這些目標,你需要去監測圖片是否正在下載,或者已經完成了下載,還是圖片的濾鏡處理已經完成了。你還需要去監測每個操作的狀態,以及判斷它是一個下載操作還是一個濾鏡處理操作,這樣你才能在用戶滾動table view的時候去做取消,中止或者恢復操作。
好的!現在你準備好開始寫代碼了!:]
打開之前的工程,添加一個命名為?PhotoRecord的NSObject新子類到工程中。打開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 |
是不是覺得上面的語法挺熟悉的?每一個property都有一個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 |
要監測每一個操作的狀態,你需要一個單獨的類。創建另一個命名為PendingOperations的NSObject新類。切換到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 row的indexPath,然后字典的value會是兩個單獨的ImageDownloader和ImageFiltration實例。
注意:你可能會對為什么要監測所有的活躍和等待操作感到好奇。難道不能通過對 [NSOperationQueue operations]的查詢來訪問它們嗎?是的,但是在本工程中,這樣做的話效率不是很高。
每次你需要去用有等待操作的行(row)的indexPath去和可見行的indexPath作對比時,你需要使用幾個迭代循環,這樣的話會是一個很耗資源的操作。通過申明一個額外的NSDictionary實例,你可以方便的了解等待操作(operations),而不需要執行沒有效率的循環操作(operations)。
切換到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方法去利用惰性實例化,所以你并不需要真的去給實例變量分配內存空間,直到他們被訪問為止。你還要給兩個隊列初始化和分配內存空間 — 一個用于下載操作,一個用于濾鏡處理 — 然后設定他們的屬性(properties),所以當你在另外的類中訪問他們時,你不需要擔心他們的初始化操作。 maxConcurrentOperationCount變量在本教程中設定為1。
現在,是時候處理下載和濾鏡處理操作了。創建一個命名為ImageDownloader的NSOperatoin子類。切換到ImageDownloader.h文件,然后添加以下代碼:
| #import Foundation/Foundation.h// 1 #import "PhotoRecord.h"// 2 @protocol ImageDownloaderDelegate;@interface ImageDownloader : NSOperation@property (nonatomic, assign) id delegate;// 3 @property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readonly, strong) PhotoRecord *photoRecord;// 4 - (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id) theDelegate;@end@protocol ImageDownloaderDelegate // 5 - (void)imageDownloaderDidFinish:(ImageDownloader *)downloader; @end |
第二部分
以下是對上面代碼的注解:
切換到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]) {// 2self.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 |
通過代碼注釋,你將看到上面的代碼在做下面的操作:
現在,繼續創建一個NSOperation的子類,用來處理圖片濾鏡操作吧!
創建另一個命名為 ImageFiltration的NSOperation新子類。打開 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 |
再次的,以下是對上面代碼的注釋:
切換到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 consumingCIImage *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 consumingCGImageRef 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參數;在任何系統資源消耗較大的函數調用前后去調用這個濾鏡處理函數,是不錯的做法。一旦濾鏡處理結束了,PhotoRecord實例的值會被恰當的設置好,然后主線程的delegate被通知了。
很好!現在你已經有了在后臺線程中執行操作(operations)的所有工具和基礎了。是時候回到view controller然后恰當的修改它,以便它可以利用好這些新優勢。
注意:在動手之前,你要下載AFNetworking library from GitHub.AFNetworking庫是建立在NSOperation 和 NSOperatinQueue之上的。它提供給你很多便捷的方法,以便你不需要為普通的任務,比如在后臺下載一個文件,創建你自己的操作。
當需要從互聯網下載一個文件的時候,在適當的位置寫一些代碼來檢查錯誤是個不錯的做法。下載數據源,一個只有4kBytes 的property list,不是什么大問題,你并不需要操心去為它創建一個子類。然而,你不能假設會有一個可靠持續的網絡連接。
蘋果為此提供了NSURLConnection類。使用它會是一項額外的工作,特別是當你只是想下載一個小的property list時。AFNetworking是一個開源代碼庫,提供了一種非常方便的方式去實施這類任務。你要傳入兩個塊(blocks),一個在操作成功時傳入,另一個在操作失敗時傳入。接下來你會看到相關的實踐例子。
要添加這個庫到工程中,選擇File > Add Files To …,然后瀏覽選擇你下載好的AFNetworking文件夾,最后點擊“Add”。確保選中了“Copy items into destination group’s folder”選項!是的,你正在使用ARC,但是AFNetworking還沒有從陳舊的手動管理內存的泥潭中爬出來。
如果你遵循著安裝指南,就可以避免編譯錯誤,如果你不遵循的話,你會在編譯時去處理非常多的錯誤。每一個AFNetworking模塊需要在你的Target’s Build Phases標簽包含 “-fno-objc-arc”字段,它在 Compiler Flags部分下面。
要實現它,在導航欄(在左手邊)點擊“PhotoRecords”。在右手邊,選擇“Targets”下面的“ClassicPhotos”。從標簽欄選擇“Build Phases”。在它下面,選擇三角形展開“Compile Sources”項。選上屬于AFNetworking的所有文件。敲擊Enter鍵,一個對話框就會彈出來。在對話框中,輸入 “fno-objc-arc”,然后點擊“Done”。
切換到 ListViewController.h文件,然后根據以下內容更新頭文件:
| ? // 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 |
這里發生了什么事?以下要點對上面的代碼做了解釋:
切換到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) {// 1NSURL *datasourceURL = [NSURL URLWithString:kDatasourceURLString];NSURLRequest *request = [NSURLRequest requestWithURL:datasourceURL];// 2AFHTTPRequestOperation *datasource_download_operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];// 3[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];// 4[datasource_download_operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {// 5NSData *datasource_data = (NSData *)responseObject;CFPropertyListRef plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, (__bridge CFDataRef)datasource_data, kCFPropertyListImmutable, NULL);NSDictionary *datasource_dictionary = (__bridge NSDictionary *)plist;// 6NSMutableArray *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;}// 7self.photos = records;CFRelease(plist);[self.tableView reloadData];[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];} failure:^(AFHTTPRequestOperation *operation, NSError *error){// 8// Connection error messageUIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Oops!"message:error.localizedDescriptiondelegate:nilcancelButtonTitle:@"OK"otherButtonTitles:nil];[alert show];alert = nil;[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];}];// 9[self.pendingOperations.downloadQueue addOperation:datasource_download_operation];}return _photos; } |
以上代碼做了一些操作。下面的內容是對代碼完成內容的一步步解析:
來到 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;// 1UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];cell.accessoryView = activityIndicatorView;}// 2PhotoRecord *aRecord = [self.photos objectAtIndex:indexPath.row];// 3if (aRecord.hasImage) {[((UIActivityIndicatorView *)cell.accessoryView) stopAnimating];cell.imageView.image = aRecord.image;cell.textLabel.text = aRecord.name;}// 4else if (aRecord.isFailed) {[((UIActivityIndicatorView *)cell.accessoryView) stopAnimating];cell.imageView.image = [UIImage imageNamed:@"Failed.png"];cell.textLabel.text = @"Failed to load";}// 5else {[((UIActivityIndicatorView *)cell.accessoryView) startAnimating];cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];cell.textLabel.text = @"";[self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];}return cell; } |
同樣的,花點時間看下下面的評論解析:
現在是時候來實現負責啟動操作的方法了。如果你還沒有實現它,可以在ListViewController.m文件中刪除舊的“applySepiaFilterToImage:”實現方法。
來到代碼的結尾,實現下列方法:
| ? // 1 - (void)startOperationsForPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {// 2if (!record.hasImage) {// 3[self startImageDownloadingForRecord:record atIndexPath:indexPath];}if (!record.isFiltered) {[self startImageFiltrationForRecord:record atIndexPath:indexPath];} } |
以上的代碼相當直接,但是有些東西要解釋下:
現在你需要去實現以上代碼段的startImageDownloadingForRecord:atIndexPath:方法。記住你創建了一個自定義的類,PendingOperations,用于檢測操作(operations)。在這里你開始使用它了。
| - (void)startImageDownloadingForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {// 1if (![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPath]) {// 2 // Start downloadingImageDownloader *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 {// 3if (![self.pendingOperations.filtrationsInProgress.allKeys containsObject:indexPath]) {// 4// Start filtrationImageFiltration *imageFiltration = [[ImageFiltration alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self];// 5ImageDownloader *dependency = [self.pendingOperations.downloadsInProgress objectForKey:indexPath];if (dependency)[imageFiltration addDependency:dependency];[self.pendingOperations.filtrationsInProgress setObject:imageFiltration forKey:indexPath];[self.pendingOperations.filtrationQueue addOperation:imageFiltration];} } |
好的!以下是簡短的解析,以確保你理解了以上代碼的工作原理。
很好!你現在需要去實現ImageDownloader和ImageFiltration的delegate方法了。將下列代碼添加到ListViewController.m文件的末尾:
| - (void)imageDownloaderDidFinish:(ImageDownloader *)downloader {// 1NSIndexPath *indexPath = downloader.indexPathInTableView;// 2PhotoRecord *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方法都有非常相似的實現,所以這里只需要拿其中一個做講解:
更新:關于處理PhotoRecord的實例,來自論壇的“xlledo”提了一個不錯的意見。因為你正在傳一個指針給PhotoRecord,再給NSOperation的子類(ImageDownloader和ImageFiltration),你可以直接修改它們。所以replaceObjectAtIndex:withObject:方法在這里是多余的。
贊!
Wow! 你做到了!你的工程完成了。編譯運行看看實際的提升效果!當你滾動table view的時候,app不再卡死,當cell可見時,就開始下載和濾鏡處理圖片了。
難道這不是很cool嗎?你可以看到一點小小的努力就可以讓你的應用程序的響應變得更加靈敏 — 并且讓用戶覺得更加有趣!
進一步地調整
你已經在本篇教程中進展很久了!你的小工程比起原來的版本變得更加反應靈敏,有了很大的提升。然而,仍然有一些細節需要去處理。你想成為一個優秀的程序員,而不僅僅是好的程序員!
你也許已經注意到當你在table view中滾動時,那些屏幕以外的cell仍然處于下載和濾鏡處理的進程中。難道你沒有在代碼里面設置取消操作?是的,你有 — 你應該好好的利用它們!:]
回到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只有在它沒有滾動時才開始操作(operations)。判斷項是UIScrollView的properties屬性,然后因為UITableView是UIScrollView的子類,它就自動地繼承了這些properties屬性。
現在,來到ListViewController.m文件的結尾,實現下面的UIScrollView委托方法:
| #pragma mark - #pragma mark - UIScrollView delegate- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {// 1[self suspendAllOperations]; }- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {// 2if (!decelerate) {[self loadImagesForOnscreenCells];[self resumeAllOperations];} }- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {// 3[self loadImagesForOnscreenCells];[self resumeAllOperations]; } |
以下是對上面代碼的解析:
好的!現在,在ListViewController.m文件的結尾添加上suspendAllOperations,resumeAllOperations和loadImagesForOnscreenCells方法的實現:
| ? #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 {// 1NSSet *visibleRows = [NSSet setWithArray:[self.tableView indexPathsForVisibleRows]];// 2NSMutableSet *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];// 5for (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;// 6for (NSIndexPath *anIndexPath in toBeStarted) {PhotoRecord *recordToProcess = [self.photos objectAtIndex:anIndexPath.row];[self startOperationsForPhotoRecord:recordToProcess atIndexPath:anIndexPath];}toBeStarted = nil;} |
suspendAllOperations, resumeAllOperations 和 cancelAllOperations 方法都有直接的實現方式。你基本上會使用工廠方法去中止,恢復或者取消操作和隊列。為了方便起見,你將它們放在單獨的方法中。
LoadImagesForOnscreenCells方法有點復雜。以下是對它的解釋:
最后,這個難題的最后項由ListViewController.m文件中的didReceiveMemoryWarning方法解決。
| ? // If app receive memory warning, cancel all operations - (void)didReceiveMemoryWarning {[self cancelAllOperations];[super didReceiveMemoryWarning]; } |
編譯運行工程,你會看到一個響應更加靈敏,有更好的資源管理的應用程序!給自己一點掌聲吧!
ClassicPhotos (改進版本)
現在還可以做什么?
這里是工程改進后的完整代碼。
如果你完成了這個工程,并且花時間真正理解了它,恭喜!相比剛閱讀本教程時,你可以把自己看待成一個更有價值的iOS開發者了!大部分的開發工作室都會幸運的擁有一兩個能真正理解這些原理的人。
但是注意 — 像deeply-nested blocks(塊),無理由地使用線程會讓維護你的代碼的人難以理解。線程會引來不易察覺的bugs,只有當網絡緩慢時才會出現,或者當代碼運行在一個更快(或者更慢)的設備中,或者有不同內核數目的設備中。仔細認真的測試,經常使用Instruments(或者是你自己的觀察)來核實引入的線程是否真的取得了性能提升。
如果你對本教程或者NSOperations有任何的意見或者問題,請加入下面的論壇討論!
總結
以上是生活随笔為你收集整理的如何使用NSOperations和NSOperationQueues的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: KinectSDK + Unity3D学
- 下一篇: linux下安装虚拟天文馆,如何在Ubu