iOS 多线程和GCD(Grand Central Dispath) 教程 (一)
本文翻译自 Ray Wenderlich 的博客 点击打开原文链接。全部由本人亲手翻译...童叟无欺~
你有木有遇见过这样的情况,当你在写app的时候,有时候界面就卡住了,要等很长时间,而这段时间你什么也不能做,因为界面不会有任何反应。
这,说明一个问题!少年,你的app该使用多线程了!
在本文中,你会了解到iOS中的核心多线程API:Grand Central Dispath -- 就是我们俗称的GCD。
如果你耐心看完这篇文章,并且跟着操作,你会将一个没有启用多线程的卡到爆的app修改成一个使用多线程操作的app。你一定会震惊的!
这篇文章假装你是了解iOS的基础开发的(当然,你自己千万不要假装,不然看不懂别怪我...)。如果你假装不了,那快去学基础吧,额...我这里木有...请自己觅食!
事不宜迟,来三碗烧酒或半斤牛肉,我们开始学习吧 - 呦!你已经开始在多线程了喔~
我应该关心什么?
“卧槽,这啥玩意东西?我关心它干嘛!我才不管,哥几个,晚上来几圈?!”
如果你就是个搬砖的,那你肯定还搞不明白我们为什么要用多线程。
睁大你的25K氪金狗眼!看看下面这个例子,它根本就没用多线程!
下载这个项目,在xcode中打开它,编译-运行,你将看见一个网页,显示在屏幕上。(如果没有,请打开源码,换个网址,这丫的网址是国外的,估计被墙了。)像下面这样:
这个app叫做:《图片收集器》 它的工作就是通过打开一个HTML的web页面,然后检索所有的链接到的图片,然后把这些图片展示在tableview上,这样你会更加清晰的看到它们。(话说我完全不知道除了好玩,这个app能干嘛...)
这个app最棒的地方是在于它甚至可以下载压缩包文件,然后查看压缩包文件里面的图片!
点击左下角的“Grab”(收集)按钮,看看它会干什么!
...
...这........
...
...到底要........
...
...等多久.......
...
要疯了!!
———————————————————————好—玩—的—分—割—线——————————————————————
我尼玛... 终于好了呀!但是这一辈子都过去了啊!搞毛?!这个app解析了HTML,下载了图片,下载了压缩包文件,然后打开了压缩包,然后又检索了图片...这全是在主线程搞的事情!
有耐心等你这个程序运行结束的用户一定都是真爱!
这个结果是你无法承受的:操作系统会觉得你这破玩意运行的时间太长了,算了,给它退了吧!然后用户直接退出然后秒删你的app!然后你出门都会被砸臭鸡蛋!(不就写了个程序么...至于这样么....)
但是,孩子不哭!多线程来拯救你!我们不把所有这些繁重的任务都交给主线程来完成,我们将使用苹果给我们提供的简单的API来将它们放在暗中偷偷运行!
多线程...和猫?!
如果你对多线程已经有了概念,就请欢快的跳过这一部分,但是,如果你没有 - 继续看!
当你的程序在运行的时候,你可以将它想想成你只猫带着一个大大的箭头指着正在运行的代码,猫合箭头的移动就是你代码运行的逻辑,一次一步。
多线程就像是一大堆的猫猫带着一大堆的箭头。
———————————————————————可—爱—的—分—割—线——————————————————————
这个图片收集的app的问题是我们让这只可怜的小猫猫在主线程做了全部的任务,好可怜!所以在猫猫画界面之前,你先让他去做其他密集的任务,像是下载文件、解析HTML等等等等。
不要让你的小猫猫工作过度了!(不要再主线程操作太多!)
然后,我们该怎样让我们辛勤的猫咪员工休息休息呢?那就是,招更多的喵咪员工啊!
这样你的第一只猫咪就可以专职负责画界面和响应用户的操作了,这个时候你其他的喵咪同时在背后悄悄的下载文件,解析HTML和跳到桌子上。(走开!)
这就是多线程的一个大概主旨,就像那些在背后搞动作的猫咪一样,一个任务可以被分解成多个线程。
在iOS中,你用到的例如viewDidLoad,buttonClicked等方法都是必须在主线程运行的。你永远不会想要主线程去处理密集的工作,否则你的界面就会卡住没有反应,当然,你还会得到一只工作过度的猫...
少年!千万不要这么干!
现在我们看看我们目前的代码然后讨论一下,为什么它会这么糟?!
这个app的根视图控制器就是一个webViewController。当你点击了“收集”按钮的时候,它将得到当前页面的HTML代码,然后将它传递给ImageListViewController。
在ImageListViewController的viewDidLoad方法中,它会创建一个新的ImageManager然后调用其方法。这个类,独立于ImageInfo,它有很多动作,像解析HTML,将图片从网上下载下来,然后解压文件。
让我们看看这两个文件是怎么协同工作的:
· ImageManager:processHTML: 用正则表达式搜索查找HTML中的链接。它会是一个潜在的花费时间的操作,这决定于小子你看的网页是什么样的。它找到的每一个压缩文件,他都会调用retrieveZip(解压压缩文件)。对于每一个它寻找到的图片,它都会创建一个ImageInfo对象,并且调用initWithSourceURL方法进行初始化。
·ImageInfo:initWithSourceURL: 调用 getImage 方法和同步方法[NSData dataWithContentsOfURL:]来从网络上检索图片。就像[NSString stringWithContentsOfURL:…]方法,这个方法会阻止一切数据流动直到它自己完成,这会花费很长时间导致你根本就不想在你的app里面用这些方法。
·ImageInfo:retrieveZip: 跟上面的方法一样,使用了会停止一主线程直到它自己完成的可怕的[NSData dataWithContentsOfURL:]方法 。当这个方法完成后,它会调用processZip(检索压缩包);
·ImageInfo:processZip: 使用ZipArchive库去将下载下来的数据保存到硬盘上,然后解压,然后查看是不是有图片在里面。将数据写到硬盘然后解压看起来就是个浪费时间的过程,这又将是主线程的一个大工程。
你应该也发现了ImageManager会调用它的代理方法:imageInfosAvailable。这就是当有新的数据出现ImageManager怎样提醒tableview刷新界面。
仔细想想现在的执行的逻辑流程,想想为什么会这么差?你可以通过查看控制台等方式看看代码是怎么运行的。
一旦你搞明白了现在它是怎么运行的,就快继续看怎么使用多线程来优化它吧!
下载一些第三方的库来完成部分异步
原文已经很早了,说是可以使用ASIHttpRequest,不过目前这个库已经没有人维护了,所以我们可以使用其他的东西,例如:MKNetWorkKit。就先用ASIHttpRequest做例子吧。
我们使用ASIHttpRequest来优化下载数据的过程。
在xcode中打开刚才的app,打开 ImageManager.m 然后做如下修改:
// 添加在文件头部
#import "ASIHTTPRequest.h"// 使用这个方法代替原方法
- (void)retrieveZip:(NSURL *)sourceURL {NSLog(@"Getting %@...", sourceURL);__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:sourceURL];[request setCompletionBlock:^{NSLog(@"Zip file downloaded.");NSData *data = [request responseData];[self processZip:data sourceURL:sourceURL]; }];[request setFailedBlock:^{NSError *error = [request error];NSLog(@"Error downloading zip file: %@", error.localizedDescription);}];[request startAsynchronous];
}
这种方式就不多赘述了,如果你的本领已经到了看多线程的时候,你应该也能看懂上面的代码。
相同的,点击打开 ImageInfo.m 然后做一些修改:
// 添加到文件的头部
#import "ASIHTTPRequest.h"// 使用下面的方法替换原方法
- (void)getImage {NSLog(@"Getting %@...", sourceURL);__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:sourceURL];[request setCompletionBlock:^{NSLog(@"Image downloaded.");NSData *data = [request responseData];image = [[UIImage alloc] initWithData:data];}];[request setFailedBlock:^{NSError *error = [request error];NSLog(@"Error downloading image: %@", error.localizedDescription);}];[request startAsynchronous];
}
以上操作都是把下载放在了后台,然后当下载完成再进行处理并显示。
运行一下看看,点击“Grab”按钮,它将迅速跳转到下一页,而不是有一个超长时间的暂停。但是,还是有一个大问题:
下载完了以后居然tableview不显示图片?!你可以通过上下滑动让图片出来,但是这也太“专业”了...怎么弄好?
使用 NSNotifications!
这个方法很容易,在获得一个图片对象ImageInfo的实例以后向tableview的类发送通知,让tableview刷新界面。
开始吧!打开ImageInfo.m ,做如下修改:
// 添加在 getImage 方法里,就在 image = [[UIImage alloc] initWithData:data]; 这段代码之后
[[NSNotificationCenter defaultCenter] postNotificationName:@"com.razeware.imagegrabber.imageupdated" object:self];
如果图片下载好了,我们会发送通知并且将新的 ImageInfo 实例发送给tableview,然后tableview更新一下就好了。
然后我们进入:ImageListViewController.m 然后做如下修改:
// 添加在 viewDidLoad 方法最后
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageUpdated:) name:@"com.razeware.imagegrabber.imageupdated" object:nil];// 添加在 viewDidUnload 方法最后
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"com.razeware.imagegrabber.imageupdated" object:nil];// 添加一个新方法
- (void)imageUpdated:(NSNotification *)notif {ImageInfo * info = [notif object];int row = [imageInfos indexOfObject:info];NSIndexPath * indexPath = [NSIndexPath indexPathForRow:row inSection:0];NSLog(@"Image for row %d updated!", row);[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];}
OK,然后运行一下,你会看到图片出现了!
Grand Central Dispath 和 Dispath Queues, 卧槽...
这俩东西才是这次的重点,GCD 和 GCDQueue 。
现在你的app还有问题,如果你点击了“Grab”!然后一直往下拼命的滑动tableview,你会发现,界面好像卡住了呦...这是因为它还在保存和解压缩压缩文件。这是因为ASIHttpRequest 在完成的时候调用的方法是在主线程里面运行的。如下:
[request setCompletionBlock:^{NSLog(@"Zip file downloaded.");NSData *data = [request responseData];[self processZip:data sourceURL:sourceURL]; // 卧槽 - 又特么跑主线程去了!
}];
于是,在iOS3.2以后,苹果发布了一个简单有效的方法来做这个,那就是通过 Grand Central Dispath 系统。基本上你无论什么时候想要在后台运行都可以将代码放在 dispath_async 中运行。
GCD 会帮你完成所有的细节,如果需要的话:创建一个新的线程,或者重用一个已存在的可用线程。
当你调用 dispath_async ,你会把它传递给一个 dispath queue ,你可以把它想象成一个存储了所有你传进去的代码块的队列,当然,先进来的代码块先运行。
你可以创建自己的 dispath 队列, (via dispath_creat),或者你甚至可以从主线程获得一个特殊的 dispath 队列(via dispath_get_main_queue)。在这里我们将要创建一个名叫“backgroundQueue”的线程来完成我们想要完成的任务。
Dispath Queues, 线程锁, 和猫粮
一个dispath队列是串行的,这代表在同一时间放入队列的代码块只会运行一部分,这很方便,因为你需要来保护一些共享的数据。
如果你对多线程的线程锁不熟悉,可以想想前面的猫的例子,如果你所有的猫在同一时间想要吃同一盘猫粮,那会怎么样?!这就是线程锁的作用。让你的猫排成一队来吃,就简单多了。
或许GCD 代表的时 Grand Cat Dispath???
这个想法最开始就是想要使用dispath queues 来保护数据。使用dispath queues只运行一段代码这保证了在同一时间只有一段代码对你的数据进行操作。
在这个app中,我们需要保护两个类型的数据:
1、在ImageListViewController中的linkURLs 数组。为了保护这个,我们将其代码结构化这样保证这个数组只会在主线程中被接触到。
2、在ImageManager中未处理的压缩文件。为了保护这个,我们将其代码结构化这样保证这些数据只会在“backgroundQueue”中被接触到。
开始使用GCD吧!
从 ImageGrabber.h 开始 ,做如下改变:
// 添加到文件的头部
#import <dispatch/dispatch.h>// 添加新的变量
dispatch_queue_t backgroundQueue;
下一个,打开ImageGrabber.m 然后做如下修改
// 1) 添加在 initWithHTML:delegate 的最下面一行
backgroundQueue = dispatch_queue_create("com.razeware.imagegrabber.bgqueue", NULL); // 2) 添加在 dealloc 的最上面一行
dispatch_release(backgroundQueue);// 3) 修改 process 方法,如下
- (void)process { dispatch_async(backgroundQueue, ^(void) {[self processHtml];});
}// 4) 修改 retrieveZip 内部调用 <span style="font-family: Arial, Helvetica, sans-serif;"> processZip 这个方法的代码如下</span>
dispatch_async(backgroundQueue, ^(void) {[self processZip:data sourceURL:sourceURL];
});// 5) 修改 <span style="font-family: Arial, Helvetica, sans-serif;">processHTML **和** </span><span style="font-family: Arial, Helvetica, sans-serif;">processZip 最后的代理,如下</span>
dispatch_async(dispatch_get_main_queue(), ^(void) {[delegate imageInfosAvailable:imageInfos done:(pendingZips==0)];
});
这些都是简单,但是非常重要的调用,因此我们来讨论一下每一步
1、创建了 dispatch queue,当你创建它的时候需要给它一个名字,名字一般都是你的bundleid + queue名,比如例子中的,我们前面说了要创建一个backgroundQueue。
2、当你创建了一个dispath queue,不要忘记释放它!在这里,我们在ImageManager 被释放的时候释放它。
3、旧的process方法只是直接的调用 processHtml 方法,因此它会运行在主线程上,并且阻塞了界面的展现,知道HTML被解析完毕。现在我们简单的使用 dispatch_async 创建一个backgroundQueue 然后让这个方法在线程中运行!
4、同样的,我们在下载完数据以后在ASIHTTPRequest中只是简单的告诉主线程,下载完毕。不同于一直阻塞界面等待数据保存完全,现在我们将它放在backgroundQueue中。当然也要确定pendingZips变量是在被保护的状态下的。
5、我们想要确定我们在调用代理方法的时候是在主线程运行的。首先,根据我们上面所说的,确定 linkURLs 数组在视图控制器中只通过主线程被访问。其次是因为这个方法与 UIKit 的子类相互作用,而 UIKit 的子类是必须在主线程中调用的。
就是这样啦!编译,然后运行你的代码,你会发现,现在它流畅多了!
但是!还有一点!
如果你编程过iOS一段时间,你应该听过 NSOperations 和 operation queues。你会不会在使用的时候不知道到底使用operation 还是GCD ?
其实呢,NSOperations 就是简单的在GCD之上的一个封装,所以当你使用了NSOperations 的时候,你其实还是在用GCD。
只不过是NSOperations 给了你一些比较不错的特征,这也许是你喜欢的。你可以根据其他的operations 来创建一些 operations 或者重新对你提交好的operation queue 进行排序,类似这样的操作。
事实上,ImageGrabber已经用了NSOperations了!ASIHTTPRequest 在其底层就在使用这个东西,你也可以自己配置 operation queue。
但是,我们到底该用哪个呢?就像这篇文章中的app,我们只是在简单的逻辑中用到了简单的线程,所以我们直接使用GCD,因为我们不需要NSOperations 的那些新的特性。但是如果你真的要用到这两个,一定要自由的使用,不要为了用而用,反而不好。
点击这里 下载这篇文章中所提到的APP到最后的完整代码。
后面还会有对GCD 更深层次的详细介绍,各位记得关注哦!