如何使用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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章