如何使用NSOperations和NSOperationQueues(一)

每個蘋果技術開發者可能都遇見過令人沮喪的情況,那就是當你點擊某個ios應用或者mac應用時,或者當你點擊按鈕或者輸入文本時,突然間,用戶交互界面停止了響應。

在一款移動端iOS程序中,用戶期望你的app可以即時地響應他們的觸摸操作,然而app反應遲鈍或者不反應就會讓人非常厭煩,用戶通常會給出不好的評價。

然而說的容易做就難。一旦你的app需要執行多個任務,事情很快就會變得複雜起來。在主運行迴路中並沒有很多時間去執行繁重的工作,並且還有一直提供可響應的UI界面。

兩難的開發者要怎麼做呢?一種方法是通過併發操作將部分任務從主線程中撤離。併發操作意味着你的程序可以在操作中同時執行多個流(或者線程)-這樣,當你執行任務時,交互界面可以保持響應。

一種在iOS中執行併發操作的方法,是使用NSOperation和NSOperationQueue類。在本教程中,你將學習如何使用它們!你會先創建一款不使用多線程的app,這樣它會變得響應非常遲鈍。然後改進程序,添加上並行操作–並且希望–可以提供一個交互響應更好的界面給用戶!

在開始閱讀這篇教程之前,先閱讀我們的 Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial會很有幫助。然而,因爲本篇教程比較通俗易懂,所以也可以不必閱讀這篇文章。

背景知識

在你學習這篇教程之前,有幾個技術概念需要先解決下。

也許你聽說過併發和並行操作。從技術角度來看,併發是程序的屬性,而並行運作是機器的屬性。並行和併發是兩種分開的概念。作爲程序員,你不能保證你的代碼會在能並行執行你的代碼的機器上運行。然而,你可以設計你的代碼,讓它使用併發操作。

首先,有必要定義幾個術語:

任務:一項需要完成的,簡單,單一的任務。

線程:一種由操作系統提供的機制,允許多條指令在一個單獨的程序中同時執行。

進程:一段可執行的代碼,它可以由幾個線程組成。

(注意:在iPhone和Mac中,線程功能是由POSIXThreadsAPI(或者pthreads)提供的,它是操作系統的一部分。這是相當底層的東西,你會發現很容易犯錯;也許線程最壞的地方就是那些極難被發現的錯誤吧!

Foundation框架包含了一個叫做NSThread的類,他更容易處理,但是使用NSThread管理多個線程仍然是件令人頭疼的事情。NSOperation和NSOperationQueue是更高級別的類,他們大大簡化了處理多個線程的過程。)

在這張圖中,你可以看到進程,線程和任務之間的關係:


在這張圖中,線程2執行了讀文件的操作,而線程1執行了用戶界面相關的代碼。這跟你在iOS中構建你的代碼很相似–主線程應該執行任何與用戶界面有關的任務,然後二級線程應該執行緩慢的或者長時間的操作(例如讀文件,訪問網絡等等)

NSOperationvs.GrandCentralDispatch(GCD)

你也許聽說過GrandCentralDispatch(GCD)。簡而言之,GCD包含語言特性,運行時刻庫和系統增強(提供系統性和綜合性的提升,從而在iOS和OSX的多核硬件上支持併發操作)。如果你希望更多的瞭解GCD,你可以閱讀Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial教程。

在MacOSXv10.6和iOS4之前,NSOperation與NSOperationQueue不同於GCD,他們使用了完全不同的機制。從MacOSXv10.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:,實例化一個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、引入UIKitandCoreImage,也就是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、設置tableview的行高爲80.0
5、當這個ListViewControllerunloaded的時候,設置photos爲nil
6、返回這個tableview有多少行
7、這個是UITableViewDelegate的可選的回調方法,然後設置每一行的高度都爲80.0,其實每一行默認的44.0的高度。
8、取得這個dictionay的key,然後得到value,就可以得到url了,然後使用nsdata來下載這個圖像。
9、加入你已經成功下載這個數據,就可以創建圖像,並且可以使用深褐色的濾鏡來處理一下。
10、這個方法就是對這個圖像使用深褐色的濾鏡。假如你想要知道更多關於CoreImagefilters的知識,你可以看看Beginning Core Image in iOS 5 Tutorial

那下面來試試。編譯運行,深褐色圖像也出現了,但是似乎他們出現的有點慢。

是時候想想如何提升用戶體驗了!

發佈了2 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章