Android和iOS开发中的异步处理(一)——开篇(发布GitHub源码)


前言和导读前端


关于“Android和iOS开发中的异步处理”这个话题,我从今年上半年就开始构思,如今已经完成了三篇(原计划是共七篇)。中间断断续续修改至今,一直在寻求一个更恰当的表达方式,尚未正式对外发表过。
java


最近几天,我把全部相关代码整理到了GitHub上(https://github.com/tielei/AsyncProgrammingDemos)。代码以Android代码为主(iOS的代码后面有时间再补充)。git


在这个不断修改的过程当中,我愈发感受到“异步编程”是一个很是重要的课题,它也许是过去的几年中,我在移动端编程上的最大的收获了。实际上,异步问题在一些分布式系统中一直以来都是很重要的问题,不少的分布式协议被发明出来就是为了处理异步事件带来的挑战(也许之后有机会咱们能一块儿聊一聊)。而这个问题局限到客户端开发这个单进程内的环境下,有它特殊的特色,值得咱们去总结和思考。github


三篇文章一块儿推送了,估计所有阅读下来,要花去很多的时间。每篇各讨论一个方面的话题,但基本相互独立,你也能够先挑选感兴趣的一篇去阅读。因为三篇一块儿推送的,因此无法在文章中互相加引用连接,没有拿到所有三篇的同窗,能够向公众号(张铁蕾)发送“异步”两个字,所有相关的内容会一会儿推送给你。编程


-- 2016.08.17后端


下面是正文,欢迎阅读。缓存




本文是我打算完成的一个系列《Android和iOS开发中的异步处理》的开篇。服务器


从2012年开始开发微爱App的第一个iOS版本计算,我和整个团队接触iOS和Android开发已经有4年时间了。如今回过头来总结,iOS和Android开发与其它领域的开发相比,有什么独特的特征呢?一个合格的iOS或Android开发人员,应该具有哪些技能呢?网络


若是仔细分辨,iOS和Android客户端的开发工做仍然能够分为“前端”和“后端”两大部分(就如同服务器的开发能够分为“前端”和“后端”同样)。多线程


所谓“前端”工做,就是与UI界面更相关的部分,好比组装页面、实现交互、播放动画、开发自定义控件等等。显然,为了能游刃有余地完成这部分工做,开发人员须要深刻了解跟系统有关的“前端”技术,主要包含三大部分:

  • 渲染绘制(解决显示内容的问题)

  • layout(解决显示大小和位置的问题)

  • 事件处理(解决交互的问题)


而“后端”工做,则是隐藏在UI界面背后的东西。好比,操纵和组织数据、缓存机制、发送队列、生命周期设计和管理、网络编程、推送和监听,等等。这部分工做,归根结底,是在处理“逻辑”层面的问题,它们并非iOS或Android系统所特有的东西。然而,有一大类问题,在“后端”编程中占据了极大的比重,这就是如何对“异步任务”进行“异步处理”。


尤为值得指出的是,大部分客户端开发人员,他们所经历的培训、学习经历和开发经历,彷佛都更偏重“前端”部分,而在“后端”编程的部分存在必定的空白。所以,本文会尝试把与“后端”编程紧密相关的“异步处理”问题进行总结归纳。


本文是系列文章《Android和iOS开发中的异步处理》的第一篇,表面上看起来话题不算太大,却相当重要。固然,若是我打算强调它在客户端编程中的重要性,我也能够说:纵观整个客户端编程的过程,无非就是在对各类“异步任务”进行“异步处理”而已——至少,对于与系统特性无关的那部分来讲,我这么讲是没有什么大的问题的。


那么,这里的“异步处理”,到底指的是什么呢?


咱们在编程当中,常常须要执行一些异步任务。这些任务在启动后,调用者不用等待任务执行完毕便可接着去作其它事情,而任务何时执行完是不肯定的,不可预期的。本文要讨论的就是在处理这些异步任务过程当中所可能涉及到的方方面面。


为了让所要讨论的内容更清楚,先列一个提纲以下:

  • (一)概述——介绍常见的异步任务,以及为何这个话题如此重要。

  • (二)异步任务的回调——讨论跟回调接口有关的一系列话题,好比错误处理、线程模型、透传参数、回调顺序等。

  • (三)执行多个异步任务

  • (四)异步任务和队列

  • (五)异步任务的取消和暂停,以及start ID——Cancel掉正在执行的异步任务,实际上很是困难。

  • (六)关于封屏与不封屏

  • (七)Android Service实例分析——Android Service提供了一个执行异步任务的严密框架 (后面也许会再多提供一些其它的实例分析,加入到这个系列中来)。


显然,本篇文章要讨论的是提纲的第(一)部分。


为了描述清楚,这个系列文章中出现的代码已经整理到GitHub上(持续更新),代码库地址为:


  • https://github.com/tielei/AsyncProgrammingDemos


其中,当前这篇文章中出现的Java代码,位于com.zhangtielei.demos.async.programming.introduction这个package中;而iOS的代码位于iOSDemos单独的目录中。


下面是由这份源码生成的安卓App的两张截图:




下面,咱们先从一个具体的小例子开始:Android中的Service Binding。




上面的例子展现了Activity和Service之间进行交互的一个典型用法。Activity在onResume的时候与Service绑定,在onPause的时候与Service解除绑定。在绑定成功后,onServiceConnected被调用,这时Activity拿到传进来的IBinder的实例(service参数),即可以经过方法调用的方式与Service进行通讯(进程内或跨进程)。好比,这时在onServiceConnected中常常要进行的操做可能包括:将IBinder记录下来存入Activity的成员变量,以备后续调用;调用IBinder获取Service的当前状态;设置回调方法,以监听Service后续的事件变化;等等,诸如此类。


这个过程表面看上去无懈可击。可是,若是考虑到bindService是一个“异步”调用,上面的代码就会出现一个逻辑上的漏洞。也就是说,bindService被调用只是至关于启动了绑定过程,它并不会等绑定过程结束才返回。而绑定过程什么时候结束(也即onServiceConnected被调用),是没法预期的,这取决于绑定过程的快慢。而按照Activity的生命周期,在onResume以后,onPause也随时会被执行。这样看来,在bindService执行完后,可能onServiceConnected会先于onPause执行,也可能onPause会先于onServiceConnected执行。


固然,在通常状况下,onPause不会那么快执行,所以onServiceConnected通常都会赶在onPause以前执行。可是,从“逻辑”的角度,咱们却不能彻底忽视另一种可能性。实际上它真的有可能发生,好比刚打开页面就当即退到后台,这种可能性便能以极小的几率发生。一旦发生,最后执行的onServiceConnected会创建起Activity与Service的引用和监听关系。这时应用极可能是在后台,而Activity和IBinder却可能仍互相引用着对方。这可能形成Java对象长时间释放不掉,以及其它一些诡异的问题。


这里还有一个细节,最终的表现其实还取决于系统的unbindService的内部实现。当onPause先于onServiceConnected执行的时候,onPause先调用了unbindService。若是unbindService在调用后可以严格保证ServiceConnection的回调再也不发生,那么最终就不会形成前面说的Activity和IBinder相互引用的状况出现。可是,unbindService彷佛没有这样的对外保证,并且根据我的经验,在Android系统的不一样版本中,unbindService在这一点上的行为还不太同样。


像上面的分析同样,咱们只要了解了异步任务bindService所能引起的全部可能状况,那就不难想出相似以下的应对措施。


图片


下面咱们再来看一个iOS的小例子。


如今假设咱们要维护一个客户端到服务器的TCP长链接。这个链接在网络状态发生变化时可以自动进行重连。首先,咱们须要一个能监听网络状态变化的类,这个类叫作Reachability,它的代码以下:




上述代码封装了Reachability类的接口。当调用者想开始网络状态监听时,就调用startNetworkMonitoring;监听完毕就调用stopNetworkMonitoring。咱们设想中的长链接正好须要建立和调用Reachability对象来处理网络状态变化。它的代码的相关部分可能会以下所示(类名ServerConnection;头文件代码忽略):





长链接ServerConnection在初始化时建立了Reachability实例,并启动监听(调用startNetworkMonitoring),经过系统广播设置监听方法(networkStateChanged:);当长链接ServerConnection销毁的时候(dealloc)中止监听(调用stopNetworkMonitoring)。


当网络状态发生变化时,networkStateChanged:会被调用,而且当前网络状态会被传入。若是发现网络变得可用了(非NotReachable状态),那么就异步执行重连操做。


这个过程看上去合情合理。可是这里面却隐藏了一个致命的问题。


在进行重连操做时,咱们使用dispatch_async启动了一个异步任务。这个异步任务在启动后何时执行完,是不可预期的,这取决于reconnect操做执行的快慢。假设reconnect执行比较慢(对于涉及网络的操做,这是颇有可能的),那么可能会发生这样一种状况:reconnect还在运行中,但ServerConnection即将销毁。也就是说,整个系统中全部其它对象对于ServerConnection的引用都已经释放了,只留下了dispatch_async调度时block对于self的一个引用。


这会致使什么后果呢?


这会致使:当reconnect执行完的时候,ServerConnection真正被释放,它的dealloc方法不在主线程执行!而是在socketQueue上执行。


而这接下来又会怎么样呢?这取决于Reachability的实现。


咱们来从新分析一下Reachability的代码来获得这件事发生的最终影响。这个状况发生时,Reachability的stopNetworkMonitoring在非主线程被调用了。而当初startNetworkMonitoring被调用时倒是在主线程的。如今咱们看到了,startNetworkMonitoring和stopNetworkMonitoring若是先后不在同一个线程上执行,那么在它们的实现中的CFRunLoopGetCurrent()就不是指的同一个Run Loop。这已经在逻辑上发生“错误”了。在这个“错误”发生以后,stopNetworkMonitoring中的SCNetworkReachabilityUnscheduleFromRunLoop就没有可以把Reachability实例从原来在主线程上调度的那个Run Loop上卸下来。也就是说,此后若是网络状态再次发生变化,那么ReachabilityCallback仍然会执行,但这时原来的Reachability实例已经被销毁过了(由ServerConnection的销毁而销毁)。按上述代码的目前的实现,这时ReachabilityCallback中的info参数指向了一个已经被释放的Reachability对象,那么接下来发生崩溃也就不足为奇了。


有人可能会说,dispatch_async执行的block中不该该直接引用self,而应该使用weak-strong dance. 也就是把dispatch_async那段代码改为下面的形式:

__weak ServerConnection *wself = self;        
dispatch_async(socketQueue, ^{    __strong ServerConnection *sself = wself;    [sself reconnect]; });

这样改有没有效果呢?根据咱们上面的分析,显然没有。ServerConnection的dealloc仍然在非主线程上执行,上面的问题也依然存在。weak-strong dance被设计用来解决循环引用的问题,但不能解决咱们这里碰到的异步任务延迟的问题。


实际上,即便把它改为下面的形式,仍然没有效果。

__weak ServerConnection *wself = self;
dispatch_async(socketQueue, ^{    [wself reconnect]; });

即便拿weak引用(wself)来调用reconnect方法,它一旦执行,也会形成ServerConnection的引用计数增长。结果仍然是dealloc在非主线程上执行。


那既然dealloc在非主线程上执行会形成问题,那咱们强制把dealloc里面的代码调度到主线程执行好了,以下:

- (void)dealloc {    
   dispatch_async(dispatch_get_main_queue(), ^{        [reachability stopNetworkMonitoring];    });    [[NSNotificationCenter defaultCenter] removeObserver:self]; }

显然,在dealloc再调用dispatch_async的这种方法也是行不通的。由于在dealloc执行过以后,ServerConnection实例已经被销毁了,那么当block执行时,reachability就依赖了一个已经被销毁的ServerConnection实例。结果仍是崩溃。


那不用dispatch_async好了,改用dispatch_sync好了。仔细修改后的代码以下:

- (void)dealloc {    
   if (![NSThread isMainThread]) {        
       dispatch_sync(dispatch_get_main_queue(), ^{            [reachability stopNetworkMonitoring];        });    }    
   else {        [reachability stopNetworkMonitoring];    }    [[NSNotificationCenter defaultCenter] removeObserver:self]; }

通过“先后左右”打补丁,咱们如今总算获得了一段能够基本能正常执行的代码了。然而,在dealloc里执行dispatch_sync这种可能耗时的“同步”操做,总难免使人胆战心惊。


那到底怎样作更好呢?


我的认为:并非全部的销毁工做都适合写在dealloc里


dealloc最擅长的事,天然仍是释放内存,好比调用各个成员变量的release(在ARC中这个release也省了)。可是,若是要依赖dealloc来维护一些做用域更广(超出当前对象的生命周期)的变量或过程,则不是一个好的作法。缘由至少有两点:

  • dealloc的执行可能会被延迟,没法确保精确的执行时间;

  • 没法控制dealloc是否会在主线程被调用。


好比上面的ServerConnection的例子,业务逻辑本身确定知道应该在什么时机去中止监听网络状态,而不该该依赖dealloc来完成它。


另外,对于dealloc可能会在异步线程执行的问题,咱们应该特别关注它。对于不一样类型的对象,咱们应该采起不一样的态度。好比,对于起到View角色的对象,咱们的正确态度是:不该该容许dealloc在异步线程执行的状况出现。为了不出现这种状况,咱们应该竭力避免在View里面直接启动异步任务,或者避免在生命周期更长的异步任务中对View产生强引用。


在上面两个例子中,问题出现的根源在于异步任务。咱们仔细思考后会发现,在讨论异步任务的时候,咱们必须关注一个相当重要的问题,即条件失效问题。固然,这也是一个显而易见的问题:当一个异步任务真正执行的时候(或者一个异步事件真正发生的时候),境况极可能已与当初调度它时不一样,或者说,它当初赖以执行或发生的条件可能已经失效。


在第一个Service Binding的例子中,异步绑定过程开始调度的时候(bindService被调用的时候),Activity还处于Running状态(在执行onResume);而绑定过程结束的时候(onServiceConnected被调用的时候),Activity却已经从Running状态中退出(执行过了onPause,已经又解除绑定了)。


在第二个网络监听的例子中,当异步重连任务结束的时候,外部对于ServerConnection实例的引用已经不复存在,实例立刻就要进行销毁过程了。继而形成中止监听时的Run Loop也再也不是原来那一个了。


在开始下一节有关异步任务的正式讨论以前,咱们有必要对iOS和Android中常常碰到的异步任务作一个总结。

  1. 网络请求。因为网络请求耗时较长,一般网络请求接口都是异步的(例如iOS的NSURLConnection,或Android的Volley)。通常状况下,咱们在主线程启动一个网络请求,而后被动地等待请求成功或者失败的回调发生(意味着这个异步任务的结束),最后根据回调结果更新UI。从启动网络请求,到获知明确的请求结果(成功或失败),时间是不肯定的。

  2. 经过线程池机制主动建立的异步任务。对于那些须要较长时间同步执行的任务(好比读取磁盘文件这种延迟高的操做,或者执行大计算量的任务),咱们一般依靠系统提供的线程池机制把这些任务调度到异步线程去执行,以节约主线程宝贵的计算时间。关于这些线程池机制,在iOS中,咱们有GCD(dispatch_async)、NSOperationQueue;在Android上,咱们有JDK提供的传统的ExecutorService,也有Android SDK提供的AsyncTask。无论是哪一种实现形式,咱们都为本身创造了大量的异步任务。

  3. Run Loop调度任务。在iOS上,咱们能够调用NSObject的若干个performSelectorXXX方法将任务调度到目标线程的Run Loop上去异步执行(performSelectorInBackground:withObject:除外)。相似地,在Android上,咱们能够调用Handler的post/sendMessage方法或者View的post方法将任务异步调度到对应的Run Loop上去。实际上,无论是iOS仍是Android系统,通常客户端的基础架构中都会为主线程建立一个Run Loop(固然,非主线程也能够建立Run Loop)。它可让长时间存活的线程周期性地处理短任务,而在没有任务可执行的时候进入睡眠,既能高效及时地响应事件处理,又不会耗费多余的CPU时间。同时,更重要的一点是,Run Loop模式让客户端的多线程编程逻辑变得简单。客户端编程比服务器编程的多线程模型要简单,很大程度上要归功于Run Loop的存在。在客户端编程中,当咱们想执行一个长的同步任务时,通常先经过前面(2)中说起的线程池机制将它调度到异步线程,在任务执行完后,再经过本节提到的Run Loop调度方法或者GCD等机制从新调度回主线程的Run Loop上。这种“主线程->异步线程->主线程”的模式,基本成为了客户端多线程编程的基本模式。这种模式规避了多个线程之间可能存在的复杂的同步操做,使处理变得简单。在后面第(三)部分——执行多个异步任务,咱们还有机会继续探讨这个话题。

  4. 延迟调度任务。这一类任务在指定的某个时间段以后,或者在指定的某个时间点开始执行,能够用于实现相似重试队列之类的结构。延迟调度任务有多种实现方式。在iOS中,NSObject的performSelector:withObject:afterDelay:,GCD的dispatch_after或dispatch_time,另外,还有NSTimer;在Android中,Handler的postDelayed和postAtTime,View的postDelayed,还有老式的java.util.Timer,此外,安卓中还有一个比较重的调度器——能在任务调度执行时自动唤醒程序的AlarmService。

  5. 跟系统实现相关的异步行为。这类行为种类繁多,这里举几个例子。好比:安卓中的startActivity是一个异步操做,从调用后到Activity被建立和显示,仍有一小段时间。再如:Activity和Fragment的生命周期是异步的,即便Activity的生命周期已经到了onResume,你仍是不知道它所包含的Fragment的生命周期走到哪一步了(以及它的view层次有没有被建立出来)。再好比,在iOS和Android系统上都有监听网络状态变化的机制(本文前面的第二个代码例子中就有涉及),网络状态变化回调什么时候执行就是一个异步事件。这些异步行为一样须要统一完整的异步处理。


本文在最后还须要澄清一个关于题目的问题。这个系列虽命名为《Android和iOS开发中的异步处理》,可是对于异步任务的处理这个话题,实际中并不局限于“iOS或Android开发”中,好比在服务器的开发中也是有可能遇到的。在这个系列中我所要表达的,更多的是一个抽象的逻辑,并不局限于iOS或Android某种具体的技术。只是,在iOS和Android的前端开发中,异步任务被应用得如此普遍,以致于咱们应该把它当作一个更广泛的问题来对待了。

相关文章
相关标签/搜索