在壹款移動端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
#import
// 2
#define kDatasourceURLString @"/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。
那下面來試試。編譯運行,深褐色圖像也出現了,但是似乎他們出現的有點慢。
是時候想想如何提升用戶體驗了!