Asynchronous processing in Android and iOS development (1)-opening (release GitHub source code)


Preface and introduction


Regarding the topic of " Asynchronous Processing in Android and iOS Development ", I started thinking about it in the first half of this year, and now I have completed three articles (the original plan was a total of seven articles). Up to now, I have been looking for a more appropriate way of expression, and I haven't officially published it.


In the past few days, I have organized all relevant codes on GitHub (https://github.com/tielei/AsyncProgrammingDemos). The code is mainly Android code (the iOS code will be added later).


In this process of constant revision, I have increasingly felt that "asynchronous programming" is a very important topic, and it may be my biggest gain in mobile programming in the past few years. In fact, asynchronous issues have always been an important issue in some distributed systems. Many distributed protocols have been invented to deal with the challenges posed by asynchronous events (maybe we will have a chance to chat together in the future). This problem is limited to the single-process environment of client development. It has its special characteristics, and it is worthy of our conclusion and thinking.


The three articles have been pushed together. It is estimated that it will take a lot of time to read them all. Each article discusses one aspect of the topic, but is basically independent of each other. You can also choose the one you are interested in to read. Since the three articles are pushed together, it is impossible to add references to each other in the article. Students who have not got all three articles can send the word "asynchronous" to the official account (Zhang Tielei), and all relevant content will be pushed all at once Give you.


-- 2016.08.17


The following is the text, welcome to read.




This article is the beginning of a series of "Asynchronous Processing in Android and iOS Development" that I intend to complete.


Since 2012, we started to develop the first iOS version of the Wei Ai App, and I have been in contact with iOS and Android development for the entire team for 4 years. Looking back now to summarize, what are the unique characteristics of iOS and Android development compared with development in other fields? What skills should a qualified iOS or Android developer possess?


If you distinguish carefully, the development of iOS and Android clients can still be divided into two parts: "front-end" and "back-end" (just as server development can be divided into "front-end" and "back-end").


The so-called "front-end" work is the part more related to the UI interface, such as assembling pages, implementing interaction, playing animation, developing custom controls, and so on. Obviously, in order to be able to complete this part of the work with ease, developers need to have an in-depth understanding of the "front-end" technology related to the system, which mainly includes three parts:

  • Rendering (to solve the problem of display content)

  • layout (Solve the problem of display size and position)

  • Event handling (solve interactive problems)


The "back-end" work is something hidden behind the UI interface. For example, manipulation and organization of data, caching mechanism, sending queue, life cycle design and management, network programming, push and monitoring, etc. This part of the work, in the final analysis, deals with "logical" issues, and they are not unique to iOS or Android systems. However, there is a large category of problems that occupies a huge proportion in "back-end" programming, and this is how to "asynchronously process" "asynchronous tasks".


In particular, it is worth pointing out that most of the client developers, their training, learning experience and development experience, seem to be more focused on the "front-end" part, while there are certain gaps in the "back-end" programming part. Therefore, this article will try to summarize the "asynchronous processing" issues closely related to "back-end" programming.


This article is the first in a series of articles "Asynchronous Processing in Android and iOS Development". On the surface, it seems that the topic is not too big, but it is very important. Of course, if I intend to emphasize its importance in client programming, I can also say: Looking at the entire client programming process, it is nothing more than "asynchronous processing" of various "asynchronous tasks"-at least, For the part that has nothing to do with the characteristics of the system, I have no big problems with this.


So, what is meant by "asynchronous processing" here?


In our programming, we often need to perform some asynchronous tasks. After these tasks are started, the caller can continue to do other things without waiting for the task to be executed, and when the task is executed is uncertain and unpredictable. This article will discuss all aspects that may be involved in the process of handling these asynchronous tasks.


In order to make the content to be discussed more clear, an outline is listed as follows:

  • (1) Overview—Introduce common asynchronous tasks and why this topic is so important.

  • (2) Asynchronous task callback-discuss a series of topics related to the callback interface, such as error handling, thread model, transparent transmission parameters, callback sequence, etc.

  • (3) Perform multiple asynchronous tasks

  • (4) Asynchronous tasks and queues

  • (5) Cancellation and suspension of asynchronous tasks, and start ID-Canceling the asynchronous task being executed is actually very difficult.

  • (6) About screen blocking and unblocking

  • (7) Android Service instance analysis-Android Service provides a rigorous framework for executing asynchronous tasks (some other instance analyses may be provided later and added to this series).


Obviously, this article will discuss the first part of the outline.


In order to describe clearly, the code appearing in this series of articles has been organized on GitHub (continuously updated), the code base address is:


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


Among them, the Java code appearing in the current article is located in the package com.zhangtielei.demos.async.programming.introduction; and the iOS code is located in a separate directory of iOSDemos.


Below are two screenshots of the Android App generated from this source code:




Below, we start with a specific small example: Service Binding in Android.




The above example shows a typical usage of the interaction between Activity and Service. Activity is bound to Service when onResume, and unbound from Service when onPause. After the binding is successful, onServiceConnected is called. At this time, the Activity gets the incoming IBinder instance (service parameter), and can communicate with the Service (in-process or cross-process) through method calls. For example, the operations frequently performed in onServiceConnected at this time may include: recording IBinder and storing it in the member variables of Activity for subsequent calls; calling IBinder to obtain the current status of the Service; setting callback methods to monitor subsequent event changes of the Service ; Wait, and so on.


This process looks impeccable on the surface. However, if you consider that bindService is an "asynchronous" call, there will be a logical loophole in the above code. In other words, bindService is called only equivalent to starting the binding process, it does not wait for the end of the binding process before returning. When the binding process ends (that is, onServiceConnected is called) is unpredictable, depending on the speed of the binding process. According to the life cycle of Activity, after onResume, onPause will be executed at any time. In this way, after bindService is executed, onServiceConnected may be executed before onPause, or onPause may be executed before onServiceConnected.


Of course, in general, onPause will not be executed so fast, so onServiceConnected will generally be executed before onPause. However, from a "logical" perspective, we cannot completely ignore another possibility. In fact, it is really possible to happen, such as returning to the background as soon as the page is opened, this possibility can happen with a very small probability. Once this happens, the last executed onServiceConnected will establish the reference and monitoring relationship between Activity and Service. At this time, the application is likely to be in the background, but Activity and IBinder may still refer to each other. This may cause Java objects not to be released for a long time, and other strange problems.


There is one more detail here. The final performance actually depends on the internal implementation of the unbindService of the system. When onPause is executed before onServiceConnected, onPause calls unbindService first. If unbindService can strictly guarantee that the callback of ServiceConnection will no longer occur after the call, it will not eventually cause the aforementioned Activity and IBinder to refer to each other. However, unbindService does not seem to have such external guarantees, and according to personal experience, in different versions of the Android system, unbindService behaves differently on this point.


Like the above analysis, as long as we understand all the possible situations that can be triggered by the asynchronous task bindService, it is not difficult to come up with countermeasures similar to the following.


image


Let's look at a small example of iOS.


Now suppose we want to maintain a long TCP connection from the client to the server. This connection can automatically reconnect when the network status changes. First, we need a class that can monitor network status changes. This class is called Reachability, and its code is as follows:




The above code encapsulates the interface of the Reachability class. When the caller wants to start network status monitoring, it calls startNetworkMonitoring; when the monitoring is complete, it calls stopNetworkMonitoring. The long connection we envision just needs to create and call Reachability objects to handle network state changes. The relevant part of its code may look like the following (class name ServerConnection; header file code ignored):





The persistent connection ServerConnection creates a Reachability instance when it is initialized, and starts the monitoring (call startNetworkMonitoring), and sets the monitoring method (networkStateChanged:) through the system broadcast; when the persistent connection ServerConnection is destroyed (dealloc), it stops monitoring (calls stopNetworkMonitoring).


When the network state changes, networkStateChanged: will be called, and the current network state will be passed in. If the network is found to be available (not in NotReachable state), then the reconnection operation is performed asynchronously.


This process seems reasonable. But there is a fatal problem hidden in it.


During the reconnection operation, we use dispatch_async to start an asynchronous task. When this asynchronous task is executed after it is started, it is unpredictable, depending on the speed of the reconnect operation. Assuming that the reconnect execution is slow (for operations involving the network, this is very likely), then there may be a situation where the reconnect is still running, but the ServerConnection is about to be destroyed. That is to say, all other objects in the entire system have released references to ServerConnection, leaving only a reference to self by block during dispatch_async scheduling.


What are the consequences of this?


This will lead to: when the reconnect is executed, the ServerConnection is really released, and its dealloc method is not executed in the main thread! It is executed on socketQueue.


And what happens next? It depends on the realization of Reachability.


Let's re-analyze the code of Reachability to get the final impact of this event. When this happens, Reachability's stopNetworkMonitoring is called in a non-main thread. But when startNetworkMonitoring was called, it was on the main thread. Now we have seen that if startNetworkMonitoring and stopNetworkMonitoring are not executed on the same thread before and after, then CFRunLoopGetCurrent() in their implementation does not refer to the same Run Loop. This "error" has occurred logically. After this "error" occurred, SCNetworkReachabilityUnscheduleFromRunLoop in stopNetworkMonitoring failed to unload the Reachability instance from the Run Loop originally scheduled on the main thread. In other words, if the network status changes again afterwards, the ReachabilityCallback will still be executed, but the original Reachability instance has already been destroyed (by the destruction of the ServerConnection). According to the current implementation of the above code, at this time the info parameter in ReachabilityCallback points to a Reachability object that has been released, so it is not surprising that a crash occurs next.


Someone may say that the block executed by dispatch_async should not directly reference self, but should use weak-strong dance. That is, change the dispatch_async code to the following form:

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

Does this change have any effect? According to our analysis above, obviously not. ServerConnection's dealloc is still executed on a non-main thread, and the above problems still exist. Weak-strong dance is designed to solve the problem of circular references, but it cannot solve the problem of asynchronous task delay we encountered here.


In fact, even if it is changed to the following form, it still has no effect.

__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. Delay scheduling tasks. This type of task starts to execute after a specified period of time or at a specified point in time, and can be used to implement a structure similar to a retry queue. There are many ways to implement delayed scheduling tasks. In iOS, performSelector:withObject:afterDelay: of NSObject, dispatch_after or dispatch_time of GCD, and NSTimer; in Android, postDelayed and postAtTime of Handler, postDelayed of View, and old-fashioned java.util.Timer, in addition , Android also has a heavier scheduler-AlarmService that can automatically wake up the program when task scheduling is executed.

  5. Asynchronous behavior related to system implementation. There are many types of such behaviors, here are a few examples. For example: startActivity in Android is an asynchronous operation, and there is still a short period of time after the call is called until the activity is created and displayed. Another example: the life cycle of Activity and Fragment is asynchronous, even if the life cycle of Activity has reached onResume, you still don’t know where the life cycle of the Fragment it contains has gone (and whether its view level has been created ). For another example, there are mechanisms for monitoring network status changes on both iOS and Android systems (this is involved in the second code example earlier in this article). When the network status change callback is executed is an asynchronous event. These asynchronous behaviors also require unified and complete asynchronous processing.


This article also needs to clarify a question about the topic at the end. Although this series is named "Asynchronous Processing in Android and iOS Development", the topic of asynchronous task processing is not limited to "iOS or Android development" in practice. For example, it may also be encountered in server development. of. What I want to express in this series is more an abstract logic, not limited to a specific technology of iOS or Android. However, in the front-end development of iOS and Android, asynchronous tasks are so widely used that we should treat it as a more general problem.




Guess you like

Origin blog.51cto.com/15049790/2562663