原文地址:
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
本文由 大俠自來也(泰然教程組) 翻譯,轉載請註明出處!!!
每個人應該都有使用某款ios或者mac的app的時候出現未響應的現象吧。如果是mac下面的app,要是比較幸運的話,那還會出現無敵風火輪,直到你能夠操作才消失。 如果是ios的app,就只能等了,有些時候還可能就這樣卡閃退了,這樣就會給用戶很差的用戶體驗。
解釋這個現象倒是很簡單:就是你的app需要一些消耗大量cpu計算時間的任務的時候,在主線程裏面就基本上沒時間來處理你的UI交互了,所以看起來就卡了。
一般的一個解決辦法就是通過併發處理來讓當前複雜的計算離開當前的主線程,也就是說使用多線程來執行你的任務。這樣的話,用戶交互就會有反應,不會出現卡的情況。
還有一種在ios中併發處理的方法就是使用NSOperation和NSOperationQueue。在這篇教程裏面,你將會學習如何使用他們。爲了看到他的效果,首先我們會創建一個一點也不使用多線程的app,所以你將會看到這個app運行時是如此的不流暢,交互性如此的不好。然後我們會重寫這個app,這個時候會加上併發處理,會給你提供良好的人機交互感受。
在開始這篇教程的時候,要是你去讀一下ios官方的多線程和GCD教程,會對你有很大的幫助的。不過這篇教程是比較簡單的,所以你也可以不用去讀剛剛的那個教程,不過建議去看看,很好的。
背景
在開始這篇教程的時候,有一些技術概念需要普及一下。你應該聽說過併發處理和並行處理。從技術點上來看,併發是程序的性質,並行是硬件的性質。所以並行和併發其實是兩個不同的概念。作爲一個程序員,你永遠不能保證你的代碼將會運行在一臺能夠使用並行處理的的機器上。但是你可以設計你的代碼以至於你可以使用併發處理。(這裏簡單用一個比喻來說明一下併發和並行。併發就是:假如有三個人,每個人一個水果,但是在他們面前只有一把水果刀,於是每個人都會輪流來使用這把刀來削水果。並行就是:有三把水果刀了,每個人都可以幹自己的事,而不用去等待別人。所以並行效率會很高,但是受硬件限制,併發其實就是多線程)。
首先,知道一些專業術語是很重要的:
作業: 一些需要被處理的簡單的工作。
線程: 在一個應用程序裏,由操作系統提供的,包含了很多可執行指令的集合。
進程: 一塊可執行的二進制代碼,由許多線程組成。
注意: 在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 |
現在讓我們來看看上面代碼的意思吧:
1、 引入UIKit and Core Image,也就是import 頭文件。
2、 爲了方便點,我們就宏定義kDatasourceURLString這個是數據源的地址字符串。
3、 然後讓ListViewController成爲UITableViewController的子類,也就是替換NSObject 爲 UITableViewController。
4、 聲明一個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 |
上面增加了很多代碼,不要驚慌,我們這就來解釋一下:
1、 Synthesize這個photos實例變量。
2、 這裏其實是重寫了photos的get函數,並且在裏面實例化這個數據源對象。
3、 設置這個導航欄上的title。
4、 設置table view的行高爲80.0
5、 當這個ListViewController unloaded的時候,設置photos爲nil
6、 返回這個table view有多少行
7、 這個是UITableViewDelegate的可選的回調方法,然後設置每一行的高度都爲80.0,其實每一行默認的44.0的高度。
8、 取得這個dictionay的key,然後得到value,就可以得到url了,然後使用nsdata來下載這個圖像。
9、 加入你已經成功下載這個數據,就可以創建圖像,並且可以使用深褐色的濾鏡來處理一下。
10、 這個方法就是對這個圖像使用深褐色的濾鏡。假如你想要知道更多關於Core Image filters的知識,你可以看看Beginning Core Image in iOS 5 Tutorial.
那下面來試試。編譯運行。太爽了,深褐色圖像也出現了,但是似乎他們出現的有點慢。不過要是你是一邊吃小吃,一邊等待,你也會覺得沒什麼問題,非常漂亮。
線程
正如我們知道的一樣,每一個app至少都有一個線程,那就是主線程。一個線程的工作就是執行一系列的指令。在Cocoa Touch裏面,主線程包含了應用程序的主循環。也就是幾乎所有的app的代碼都是在主線程裏面執行的,除非你特別的創建一個其他的線程,並且在這個新線程裏面執行一些代碼。
線程有兩個特徵:
1、 每個線程都有共同的權利來使用app的資源,不過除了局部變量。因此任何對象都可能潛在的被任何線程更改,使用。
2、 沒有辦法來估計一個線程將會運行多久,或者那個線程將會首先執行完。
因此,知道一些克服這些問題和避免一些不可料想的錯誤的技術是很重要的。下面就列舉一些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 |
解釋一下上面數字註釋的地方:
1、 聲明一個私有接口,這樣你可以更改這個實例變量的屬性爲讀和寫。
2、 設置屬性
3、 有計劃的去檢查isCancelled,這樣可以確保你儘可能隨時可以終止這個操作。
4、 蘋果建議使用@autoreleasepool塊來代替alloc和初始化NSAutoreleasePool,因爲使用block有更高的效率。你也完全可以使用NSAutoreleasePool來代替的,這樣也是很好的。
5、 強制轉換爲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 |
又來解釋一下代碼:
1、 由於你需要對UIImage實例對象直接操作圖片濾鏡,所以你需要導入UIKit和CoreImage frameworks。你也需要導入PhotoRecord。就像前面的ImageDownloader一樣,你想要調用者使用我們定製的初始化方法。
2、 聲明一個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 |
下面又開始解釋代碼:
1、 在這個類裏面,我們不需要CoreImage了,所以刪除他的頭文件,但是我們需要導入PhotoRecord.h, PendingOperations.h, ImageDownloader.h 和 ImageFiltration.h。
2、 這裏涉及到了AFNetworking庫
3、 確保ListViewController包含了ImageDownloader 和 ImageFiltration delegate的方法。
4、 這裏你已經不再需要數據源了。你將會創建一個使用圖片屬性表的PhotoRecord的實例對象。所以,你應該把“photos”從NSDictionary 變爲 NSMutableArray,這樣你可以更新圖片數組。
5、 這個屬性用於跟蹤掛起的操作的。
切換到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; } |
上面的信息量有點大,我們一步一步的來解釋:
1、 創建一個NSURL 和一個 NSURLRequest來指向數據的地址。
2、 使用到了AFHTTPRequestOperation類,新建並且使用了一個請求來初始化。
3、 給用戶反饋,當在下載這個數據的時候,激活網絡活動指示器。
4、 通過使用setCompletionBlockWithSuccess:failure:方法,我們可以增加兩個block:一個是成功的,一個是失敗的。
5、 在成功的block裏面,下載的這個圖片屬性錶轉化NSData,然後使用toll-free bridging(core foundation 和foundation之間的數據橋接,也就是c和objc的橋接)來轉化這個數據到CFDataRef 和 CFPropertyList,接着轉化到NSDictionary。
6、 新建一個NSMutableArray對象,遍歷這個字典的所有對象和key,通過這些對象和key新建一些PhotoRecord的實例對象,並且儲存在這個數組裏面。
7、 一旦遍歷完成,讓這個_photo變量指向這個記錄數組,並且從新加載table view,還需要停止網絡活動指示器。這裏你需要釋放掉“plist”這個實例變量。
8、 在這個失敗的block裏面,我們將展示一個消息來通知用戶。
9、 最後,增加“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; } |
看看上面代碼,現在來解釋一下:
1、 創建了一個UIActivityIndicatorView,並且設置他爲這個cell的accessory view,用來提供一個反饋給用戶。
2、 數據源包含在PhotoRecord的實例對象中。通過indexpath裏面的row來得到每一個數據。
3、 檢查這個PhotoRecord。假如這個圖像被下載下來了,就顯示這個圖片,顯示圖片的名字,還要停止這個活動指示器。
4、 假如下載圖像失敗了,就在顯示一個失敗的圖片,來告訴用戶下載失敗了,並且要停止這個活動指示器。
5、 假如這個圖片還沒有被下載下來。就開始下載和濾鏡的操作(他們還沒有實現),這個時候顯示一個佔位的圖片和激活活動指示器來提醒用戶正在工作。
現在是時候實現我們一直關注的開始操作的方法了。假如你還沒有準備好,你可以在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]; } } |
上面的代碼相當直接,但是也解釋一下上面做的:
1、 爲了簡單,我們傳遞需要被操作的PhotoRecord的實例對象,順帶和對應的indexpath。
2、 檢查一下是否已經有圖片了。假如有,就可以忽略那個方法了。
3、 假如還沒有圖片,就調用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]; } } |
下面就解釋一下上面代碼做了什麼,確保你能夠懂得。
1、 首先,檢查一下這個indexpath的操作是否已經在downloadsInProgress裏面了。假如在就可以忽略掉。
2、 假如沒有在,就創建一個ImageDownloader的實例對象,並且設置他的delegate爲ListViewController。我們還會傳遞這個indexpath和PhotoRecord的實例變量,然後把這個實例對象增加到下載隊列。你也需要把他增加到downloadsInProgress裏面去保持跟蹤。
3、 相似的,也這樣去檢查圖片是否被過濾了。
4、 假如沒有被過濾,那麼也就初始化一個。
5、 這裏有一點考慮的,你必須檢查是否這個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 方法實現方法非常相似,因此就解釋其中一個就可以了:
1、 得到這個操作的indexpath,無論他是下載還是濾鏡的。
2、 得到PhotoRecord的實例
3、 更新UI
4、 從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]; } |
快速看看上面代碼展示了什麼:
1、 一旦用戶開始滑動了,你就應將所有的操作掛起。後面將會實現suspendAllOperations方法。
2、 假如decelerate的值是NO,那就意味着用戶停止拖動這個table view了。因此你想要恢復掛起的操作,取消那些屏幕外面的cell的操作,開始屏幕內的cell的操作。後面將會實現loadImagesForOnscreenCells 和 resumeAllOperations的方法。
3、 這個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是有一點複雜,下面就解釋一下:
1、 得到可見的行
2、 得到所有掛起的操作(包括下載和圖片濾鏡的)
3、 得到需要被操作的行 = 可見的 – 掛起的
4、 得到需要被取消的行 = 掛起的 – 可見的
5、 遍歷需要取消的,取消他們,並且從PendingOperations裏面移除。
6、 遍歷需要被開始,每一個調用startOperationsForPhotoRecord:atIndexPath:方法。
最後一個需要解決的就是解決ListViewController.m中的didReceiveMemoryWarning方法。
// If app receive memory warning, cancel all operations - (void)didReceiveMemoryWarning { [self cancelAllOperations]; [super didReceiveMemoryWarning]; } |
編譯運行,你應該有一個更好的響應,更好的資源管理程序了。慢慢歡呼吧!
何去何從?
這裏是這個完整的工程。
假如你完成了這個工程,並且花了一些時間懂得這些,那麼祝賀你!你已經比開始這個教程的時候懂得了很多。要想完全懂得這些東西,你需要了解和做很多工作的。線程其實也是有些微的bug,,但是一般都不容易出現,可能會在網絡非常慢,代碼運行在很快或者很慢的設備,或者在多核設備上出現bug。測試需要非常的仔細,並且一般需要藉助工具或者你觀察來覈查這個線程來做一些修改。