`
jaybril
  • 浏览: 50527 次
  • 性别: Icon_minigender_1
社区版块
存档分类
最新评论

聊聊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

分享到:
评论

相关推荐

    iOS多线程-GCD(异步任务,线程锁)

    本文将深入探讨GCD中的异步任务、线程锁以及如何在iOS中实现它们。 首先,我们要理解什么是GCD。GCD是一种底层的任务调度框架,它管理着系统的所有线程,允许开发者提交任务到队列中,由系统自动决定何时何地运行...

    iOS Block使用教程

    - 通过设置success和failure Block,我们可以在请求成功或失败时执行相应的代码逻辑,实现异步处理网络数据。 7. **避免Block循环引用( retain cycle)** - 当Block内部引用了强引用自身实例的属性时,可能导致...

    GCD异步获取图片

    在iOS开发中,Grand Central Dispatch(GCD)是一种强大的多线程管理工具,它由Apple引入,用于简化并发编程。GCD是基于C语言的,但可以无缝集成到Objective-C和Swift项目中。本篇文章将深入探讨如何使用GCD异步获取...

    ios-block反向传值.zip

    "ios-block反向传值.zip"这个压缩包文件主要关注的是如何利用Block来实现反向传值,即从子线程或异步操作中将结果传递回主线程或调用者。 Block的基本语法: Block本质上是一个对象,它可以捕获并存储其所在上下文...

    ios-block 回调.zip

    在iOS开发中,Block是一种强大的代码组织和回调机制,它允许我们把代码块作为一个对象来传递,这在处理异步操作、事件响应或者简化复杂的逻辑时特别有用。"ios-block 回调.zip"中的"CallbackDemo"很可能是展示了一个...

    iOS block实现,内存管理

    在iOS和OS X开发中,Block常用于异步操作的回调、并发处理等场景。本文将深入探讨Block的实现以及与内存管理的关系。 Block可以视为匿名函数,它们在C语言的基础上进行了扩展。在Objective-C中,Block主要有以下几...

    ios-block 多参数传值.zip

    在iOS应用开发中,Block的使用非常常见,特别是在处理异步操作、事件回调或者复杂逻辑控制时。 Block的基本语法结构如下: ```swift ^(参数列表) -> 返回类型 { // Block体,包含执行的代码 } ``` 例如,一个...

    iOS 异步下载图片实现瀑布流

    GCD(Grand Central Dispatch)和NSURLSession是实现异步下载的主要工具。GCD提供了一种并发执行任务的方式,而NSURLSession则用于处理HTTP请求,两者结合可以高效地下载图片。 异步下载图片的基本步骤如下: 1. ...

    iOS block使用总结

    通过`dispatch_queue_t`创建队列,然后使用`dispatch_async`或`dispatch_sync`提交Block到队列,可以轻松地实现异步或同步操作。 7. **循环引用问题** 当Block作为对象的属性或者成员变量时,如果不小心可能会引起...

    [ios]Block分离DataSource -ios升级日记2

    在iOS开发中,Block是一种强大的语法特性,常用于回调、异步处理等场景。然而,当Block被用作数据源(DataSource)时,可能会导致一些问题,如内存泄漏、代码结构混乱等。本文将深入探讨如何将Block分离出DataSource...

    代理delegate详解,block,gcd

    在iOS和Mac开发中,代理(Delegate)、Block和GCD(Grand Central Dispatch)是三个非常重要的概念,它们各自承担着不同的任务,同时也常被结合使用以实现高效的代码编写。下面将详细阐述这三个知识点。 首先,代理...

    IOS笔试题+答案

    在iOS笔试中,开发者通常会被要求展示他们如何实现特定功能或者解决实际问题的能力。 在解题过程中,考生需要具备良好的编程习惯,理解苹果的MVC(Model-View-Controller)设计模式,以及遵循苹果的Human Interface...

    ios异步下载图片

    本教程将深入探讨如何在iOS中实现异步下载图片,主要涉及多线程编程的概念。 首先,我们需要理解iOS中的线程模型。主线程是负责处理用户交互和UI更新的线程,而其他线程则可用于执行耗时操作,如网络请求和图片下载...

    IOS block回调代码实例Demo

    在iOS开发中,Block常用于异步操作的回调,例如网络请求、定时器或者GCD(Grand Central Dispatch)中。在描述的"Demo块"中,我们可能看到一个简单的例子,如网络请求完成后,通过Block传递数据回主线程进行更新UI。...

    iOS的block回调

    在iOS开发中,Block是一种强大的编程工具,它允许我们在代码中定义可重用的代码块,这些代码块可以像函数一样被传递和调用。Block的使用尤其在处理回调、异步操作和事件响应时非常常见。下面我们将深入探讨iOS中的...

    ios Block和代理的对比

    在iOS开发中,Block和代理是两种常用的回调机制,它们都可以用来实现对象间的通信,但具体用法和特性有所差异。下面将详细讲解Block和代理的对比,以及它们各自的应用场景。 首先,Block是一种内联函数,它可以捕获...

    iOS GCD多核编程

    在GCD中,可以使用`dispatch_async`函数异步执行任务,这样不会阻塞当前线程。例如,我们可以在后台下载数据,同时主线程继续处理UI: ```swift let queue = DispatchQueue.global(qos: .background) queue.async {...

    ios中block的应用

    该文档简单的介绍了ios下block是的使用方法和一些小技巧

    ios-Block基础,block传值,及自定义block方法.zip

    在iOS开发中,Block常与Grand Central Dispatch (GCD)结合使用,实现多线程编程。通过Block,我们可以方便地将任务提交到不同的调度队列,如主队列或并发队列,简化异步编程。 6. **Block与KVO**: Block还可以...

    iOS异步队列下载

    压缩包中的"Dome"文件可能是一个示例项目,包含了使用ASIHTTPRequest库实现异步队列下载的代码和配置。通过分析和运行这个示例,开发者可以更深入地了解如何在实际项目中应用这些技术。 总之,iOS异步队列下载利用...

Global site tag (gtag.js) - Google Analytics