MyException - 我的异常网
当前位置:我的异常网» 操作系统 » 聊聊iOS停block + GCD 实现异步非阻塞(转)

聊聊iOS停block + GCD 实现异步非阻塞(转)

www.MyException.Cn  网友分享于:2013-09-09  浏览:71次
聊聊iOS下block + GCD 实现异步非阻塞(转)

本文用示例来说明一下iOS下用block+GCD来在程序中实现非阻塞式执行耗时任务。先说明一下,严格说来“异步”、“后台线程”、“非阻塞”这些概念是有一些小区别的。有些系统API特别是网络和文件I/O是通过系统底层中断来实现”非阻塞”,而一般用户任务比如耗时计算是通过后台线程完成的。但具体到app这一层,开发人员并不关心具体的实现是用了硬件中断还是一个线程,所以在本文的上下文中,没有特意区分这几个概念点,甚至有些混用。本文中的“非阻塞”可以简章理解为,开发人员只需要知道“我的程序执行耗时任务时,UI仍然可以响应用户操作”。

示范代码在附件。可用xcode 4编译,在ios 4及以上运行。

写过程序的都知道,要让程序对用户输入响应及时,避免程序在某个操作时僵死的情况,那就要把耗时操作放到后台去做,然后通过异步的通知或者回调来接着流程往下走。否则的话耗时操作会把主线程阻塞,导致程序很长时间不回到主事件循环。

这在移动平台上尤其重要,一般移动平台上系统都会有一个专门的检查机制,看程序有没有很长时间被阻塞住,没有回来检查主消息队列。发现这种情况一般都是把程序作为“无响应”干掉。iOS一般情况下是10秒为上限。10秒内程序没有回到主消息循环就被干掉。在前台后台切换时更严格,大概是5秒左右。(在一般的PC编程中,对这种情况的容忍度高一些,程序本身会僵死,UI画屏会停止(所以常常会看到空白或者破碎的窗口),有时系统还会弹出“停止响应”警告。但一般来说系统不会主动杀掉这些程序)

但对很多开发人员,尤其是新手来说,这种非阻塞方式是比较违反人类直观思维的做法。比如,当用户点击某个按纽时我想在程序中计算100万位的PI值。从最直观的思维出发,一般都会先想到顺序式的编程方式:

代码:
// 示例1:阻塞方式

// 用户点击了按纽,触发计算操作
- (void) didTapCalcButton {
	// 显示“请等待”提示
	[self showWaitingView];

	// 计算PI值到100万位。运算结束后才返回。
	NSString *result = [self calcPI:1000000];

	// 关闭“请等待”提示
	[self hideWaitingView];

	// 显示结果(当然,这里可能只显示前N位,不然又变成耗时操作了)
	[self displayResult:result];
}

这样做有很多问题。一是前面提到的程序不响应用户输入,甚至被系统判定为失去响应而杀掉的问题。二是“请等待”这个提示根本不会出现。因为任何对UI的操作,在iOS中实际上并不是立刻执行,只是做了个标记,在当前事件循环(runloop)完成后,在下一个事件循环开始前,系统根据做的标记来决定屏幕哪一块需要更新,并进行重绘。照上面这个写法,showLoadingView只是打个标记,但当前runloop要在这个函数返回后才会结束。而结束前我们又调用了hideLoadingView,所以根本不会显示。

要解决这些问题,需要把计算PI值这个操作放到后台异步执行。具体有很多方法。传统的方法无非是自己开线程,或者用iOS提供的高级线程封装NSOperation来完成这样做。(扯远点:在没有线程支持的低端移动平台上还有一种方式,就是每次做很少的计算以避免阻塞,比如计算100位,然后把剩下的工作重新排程到事件队列尾巴上。重复进行,最终结果是分一万次做完。这样的做法非常低效,而且开发人员需要自己保存若干状态,很麻烦)

无论用哪种方法,传统的异步方式来实现这个例子的程序结构大概都是这么一个样子:

代码:
// 示例2:传统的后台任务实现异步

// 用户点击了按纽,触发计算操作
- (void) didTapCalcButton {
	// 显示“请等待”提示
	[self showWaitingView];

	// 计算PI值到100万位。这里只是创建一个后台任务并启动它,然后立刻返回,并不等待任务本身完成
	[self startCalcPI:1000000];

	// 然后程序什么也不干,等着。
}

// 不论哪种异步方式,最后一定要有一个办法通知主线程任务已完成。具体到iOS,有若干方法可以使用,比如:
// delegate, KVO, NSNotification, performSelectorOnMainThread:等。
// 假设以下是回调函数,在主线程上被调用:
- (void) calculationDidFinishWithResult:(NSString *)result {
	// 关闭“请等待”提示
	[self hideWaitingView];

	// 把结果显示在屏幕上
	[self displayResult:result];
}

// 以下示范用NSOperation + KVO来做后台运算。

- (void) startCalcPI:(NSInteger)digits {
	// MyPICalcOperation是一个NSOperation子类。其main方法中直接进行PI的运算,相当于阻塞示例中的calcPI:。
	NSOperation *calcOpeation = [[[MyPICalcOpeartion alloc] init] autorelease];
	// 假设我们选择用KVO方式观察后台任务的结束
	[calcOperation addObserver:self keyPath:@"isFinished" …];
	// 提交任务,开始执行。
	[self.operationQueue addOperation:calcOperation];
}

// 观察后台计算任务的完成。这是一个标准KVO函数,简单说当calcOperation的isFinished属性从FALSE变TRUE后会被调用
- (void) observeValueForKey:keyPath ofObject:object ... {
	if([@"isFinished" isEqualToString:keyPath] && [object isKindOfClass:[MyPICalcOperation class]]) {
		// 观察到了我们想要的状态变化,即运算结束。这里我们调用回调处理结果。确保回调在主线程上进行
		MyPICalcOpeartion *op = (MyPICalcOperation *)object;
		[self performSelectorOnMainThread:@selector(calculationDidFinishWithResult:)
					withObject:op.result
					waitUntilDone:FALSE];
	} else {
		[super observeValueForKey:...];
	}
}

差不多就这样。当然具体代码量的多少和你选用的具体异步实现相关,但总是要有额外的代码去做后台的事情,来判定运算的结束,以及回调。从这方面说没有根本的区别。

对熟练的开发人员来说,这是非常自然的事情,尤其是一个合格的移动平台开发人员,他会认为这是写好一个程序必要的方式。但是现在的问题是,随着android/iOS的出现,越来越多的非专业人士开始写程序。他们有一个很棒的创意,可以做出很有用的东西,但他们毕竟没有受过正统的编程训练,所以很多人会认为异步方式难于理解且代码复杂(看看上面两个例子的代码量比较。第二个例子我还省略了MyCalcPIOperation的实现,不然更长)。所以很多人会自然的选择用同步阻塞的方式来写程序。这样造成的结果就是AppStore上有大量不稳定的程序,莫名其妙的崩溃。或者在iPhone4上能够正常工作,但在慢一点的3GS上就崩溃,因为计算速度变慢导致了阻塞时间过长。

而传统的异步方式需要一些时间才能掌握,而且很容易出现一些常见错误。比如KVO的注册和反注册没有匹配;没有搞清楚观察函数是在主线程还是后台线程上执行,导致UI操作无效;而delegate方式也常会引发内存问题,比如retain delegate造成循环引用;或者assign delegate没有管理好,出现野指针。这一类的问题会让普通开发人员望而却步。

iOS4对这个问题的解决办法,就是引入了block块编程方式以及GCD (Grand Central Dispatch)任务队列管理。这里我们不去花版面介绍枯燥的语法。有需要的同学请自己查阅文档。我们先试着用block+GCD来重写这个计算100万位PI的程序片段:

代码:
// 示例3:block+GCD异步

// 用户点击了按纽,触发计算操作
- (void) didTapCalcButton {
	// 显示“请等待”提示
	[self showWaitingView];

	// 以下两行将任务排程到一个后台线程执行。dispatch_get_global_queue会取得一个系统分配的后台任务队列。
	dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_async(queue, ^{

		// 计算PI值到100万位。和示例1的calcPI:完全一样,唯一区别是现在它在后台线程上执行了。
		NSString *result = [self calcPI:1000000];

		// 计算完成后,因为有UI操作,所以需要切换回主线程。一般原则:
		// 1. UI操作必须在主线程上完成。2. 耗时的同步网络、同步IO、运算等操作不要在主线程上跑,以避免阻塞
		// dispatch_get_main_queue()会返回关联到主线程的那个任务队列。
		dispatch_async(dispatch_get_main_queue(), ^{

			// 关闭“请等待”提示
			[self hideWaitingView];
			
			// 显示结果
			[self displayResult:result];		
		});
	});
}

然后……然后就没有然后了。就这样。

可以比较一下示例1和3。基本上是完全一样的代码,3只是加了两行dispatch指令,把任务在前台后台间切换来切换去。但3完全不会造成主线程阻塞,哪怕计算PI值要花一个小时都不会有问题。“请等待”提示也可以正确显示和消失。block+GCD可以说是既保留了顺序编程的直观和简洁,又在技术上实现了异步特征以提高程序响应。可以说是一种比较完美的方式,代码也非常好理解。

这里对示例3有几点需要说明:

1. didTapCalcButton并没有等到计算完成才返回。当计算任务被扔到后台队列(甚至都未必开始执行)后就立刻返回了。后续的操作由系统自己记住并完成

2. block的一大特征是自动管理变量的生存期。传统的异步做法一般都要把计算状态或者结果保留为类的成员变量。但示例3中我们直接把NSString *result申请成局部变量,然后在另外一个块中可以直接使用。这是比较颠覆的一种做法,因为从传统的变量生存周期来看,result这个变量只在第一个块中有效。在最后这个displayResult所在的块中应该已经出了scope,不再有效。但针对block,编译器做了一些特别的事情,它会自动分析出变量的跨块引用并进行跨块的传址(需要使用__block方式)、传值、或者retain(对object或者其属性及方法的调用)。所以对开发人员来说,块间的变量生存周期是很灵活的,基本上是“前面有定义后面就可用”。

如果大家熟悉java inner class,其实第二点是很象的。non-static inner class允许访问外层的局部变量,但外层必须申请为final即传值模式。但java是没有传址模式(相当于__block)的,所以inner class不能修改外部局部变量的值(假如我没记错的话)。inner class对外层class的成员变量和方法的引用,编译器也是通过创建一系列Outter.access$100等匿名方法实现的。从这一点看,block借鉴了相当多的java inner class的概念。而GCD只是管理一堆前台后台任务队列,并允许程序把任务在队列间切来切去而已。GCD选择block作为任务定义的语法,是因为block这种自动跨块生存周期管理很适合这种切换。

另外要提醒的是,这种方式也并非万能:

1. 一个好的程序,对任何耗时操作都要给用户提供半路取消的选择。要做到这一点,还是需要增加一些代码

2. block就象一个object,也有自己的生存周期问题,也会出现类似野指针和内存泄漏的情况。如果你自己做一个基于block的异步库供别人使用,非常容易产生循环引用的错误(对方的app class retain了你的异步库,你的异步库retain了app提供的回调block,而block中一般又通过self引用了app class本身),需要特别小心。

3. 假如在运算完成前用户就退出这个页面(比如回退到上一页),运算还是会进行,view controller的销毁被延后到运算结束的时候。假如不想要这个效果的话,一是要实现1中的取消机制,二是要在块中避免引用self(否则会被自动retain)。具体看文档。

个人浅见。错漏难免。欢迎讨论。

附件是示范代码。NBExample1,2,3ViewController三个类分别示范三种做法。可以看出,Example 1的同步方式体验很差,而且程序很可能被系统中止。2 & 3都做到了非阻塞,任务进行中UI还可以响应(列表可以滚动),但3的代码简洁得多。

 

 

转载自:http://bbs.et8.net/bbs/showthread.php?t=1019931

文章评论

程序员都该阅读的书
程序员都该阅读的书
程序员和编码员之间的区别
程序员和编码员之间的区别
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
鲜为人知的编程真相
鲜为人知的编程真相
我是如何打败拖延症的
我是如何打败拖延症的
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
为什么程序员都是夜猫子
为什么程序员都是夜猫子
程序员应该关注的一些事儿
程序员应该关注的一些事儿
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
10个调试和排错的小建议
10个调试和排错的小建议
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
编程语言是女人
编程语言是女人
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
 程序员的样子
程序员的样子
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
Java程序员必看电影
Java程序员必看电影
如何成为一名黑客
如何成为一名黑客
总结2014中国互联网十大段子
总结2014中国互联网十大段子
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
我的丈夫是个程序员
我的丈夫是个程序员
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
那些争议最大的编程观点
那些争议最大的编程观点
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
漫画:程序员的工作
漫画:程序员的工作
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
每天工作4小时的程序员
每天工作4小时的程序员
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
程序员的鄙视链
程序员的鄙视链
旅行,写作,编程
旅行,写作,编程
老程序员的下场
老程序员的下场
一个程序员的时间管理
一个程序员的时间管理
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
程序员必看的十大电影
程序员必看的十大电影
代码女神横空出世
代码女神横空出世
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有