This article is the beginning of a series I intend to complete called Asynchronous Processing in Android and iOS Development.
Since 2012, I started to develop the first iOS version of the Micro-love App. The whole team and I have been in touch with iOS and Android development for 4 years. Coming back to the summary, what makes iOS and Android development unique compared to other areas of development? What skills should a qualified iOS or Android developer have?
If you look closely, iOS and Android client development can still be divided into “front end” and “back end” (just as server development can be divided into “front end” and “back end”).
The “front end” work is the more relevant parts of the UI interface, such as assembling pages, implementing interactions, playing animations, developing custom controls, and so on. Obviously, in order to do this smoothly, developers need to have a deep understanding of the “front end” technology associated with the system, which consists of three main parts:
- Render rendering (solve the problem of displaying content)
- Layout (Solve display size and location problems)
- Event handling (solving interaction problems)
The “back end” work is what lies behind the UI interface. Examples include manipulating and organizing data, caching mechanisms, sending queues, life cycle design and management, network programming, pushing and listening, and so on. This part of the work, ultimately, deals with “logical” issues that are not unique to iOS or Android. There is, however, one big class of problems that accounts for a large portion of “back-end” programming: how to “asynchronously process” “asynchronous tasks.”
In particular, the training, learning, and development experiences of most client developers seem to be more focused on the “front end” part, with a gap in the “back end” part of programming. Therefore, this article will attempt to summarize the “asynchronous processing” issues that are closely related to “back-end” programming.
This article is the first in a series called Asynchronous Processing in Android and iOS development, and on the surface it doesn’t seem like a big topic, but it’s important. Of course, if I’m going to emphasize the importance of it on the client side programming, I also can say: throughout the client-side programming process, does not go in for all kinds of “asynchronous tasks” “asynchronous processing” — at least, for has nothing to do with the system characteristics of the part, so I speak is no big problem.
So what do we mean by “asynchronous processing”?
In programming, we often need to perform some asynchronous tasks. After these tasks are started, the caller can go on to do other things without waiting for the completion of the task, and the completion of the task is uncertain and unpredictable. This article discusses all the aspects that can be involved in handling these asynchronous tasks.
To make it clear what we are going to discuss, let’s make an outline as follows:
-
Overview — Introduce common asynchronous tasks and why this topic is important.
-
Callbacks for asynchronous tasks – discuss a range of topics related to the callback interface, such as error handling, threading model, passthrough parameters, callback order, etc.
-
(3) Multiple asynchronous task cooperation
-
(4) Asynchronous tasks and queues
-
(5) Asynchronous task cancellation and suspension, and start ID — Cancel asynchronous task execution, actually very difficult.
-
(six) on the sealing and non-sealing screen
-
(7) Android Service Instance Analysis — Android Services provide a rigorous framework for performing asynchronous tasks (more instance analysis may be provided later in this series).
Obviously, this article deals with part (1) of the outline.
For clarity, the code that appears in this article series has been collated to GitHub (which is being updated continuously) and the code base address is:
- Github.com/tielei/Asyn…
Among them, the Java code in this article, the com. Zhangtielei. Demos. Async. Programming. The introduction of the package; The iOS code is in a separate directory of iOSDemos.
Let’s start with a small concrete example: Service Binding in Android.
public class ServiceBindingDemoActivity extends Activity {
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
// Unreference and listen the Activity from the Service. }@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// Establish a reference and listener relationship between the Activity and the Service. }};@Override
public void onResume(a) {
super.onResume();
Intent intent = new Intent(this, SomeService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
@Override
public void onPause(a) {
super.onPause();
// Unreference and listen the Activity from the Service. unbindService(serviceConnection); }}Copy the code
The example above shows a typical use of interaction between an Activity and a Service. An Activity is bound to a Service during onResume and unbound from a Service during onPause. Once the binding is successful, onServiceConnected is called, and the Activity gets the IBinder instance (the service parameter) passed in. It can then communicate with the Service (within or across processes) through method calls. For example, common actions in onServiceConnected ected nodes might include logging the IBinder into an Activity’s member variables for future calls; Call IBinder to get the current state of the Service; Set up callback methods to listen for subsequent changes to the Service; And so on and so on.
On the surface, the process looks impeccable. However, if you consider that bindService is an “asynchronous” call, there is a logical hole in the above code. That is, when bindService is called, it simply starts the binding process, and it does not wait for the binding process to end before returning. When the binding process ends (called onServiceConnected) is unpredictable, depending on how fast the binding process is going. According to the Activity lifecycle, onPause is also executed at any time after onResume. In this case, onServiceConnected ected blocks may come before onPause cubes, or onPause blocks may come before onServiceConnected blocks.
Of course, onPause is usually not executed that fast, so onServiceConnected is usually executed before onPause. However, from a “logical” point of view, we cannot completely ignore another possibility. In fact, it can happen, such as backing out of the background as soon as you open a page, with a very small probability. Once this happens, the last executed onServiceConnected interface will establish a reference and listening relationship between the Activity and the Service. The application is likely to be in the background, and the Activity and IBinder may still refer to each other. This can cause Java objects to hang around for a long time, and other weird problems.
As a further detail, the final performance depends on the internal implementation of the system’s unbindService. When onPause executes before onServiceConnected, onPause calls unbindService first. If unbindService strictly guarantees that the ServiceConnection callback does not occur after being called, then the Activity and IBinder reference each other will not eventually occur. However, unbindService does not seem to have such guarantees, and in my experience unbindService behaves differently at this point across different versions of The Android system.
As in the above analysis, it is not difficult to come up with a solution like the following once we understand all the possible scenarios that can arise from the asynchronous task bindService.
public class ServiceBindingDemoActivity extends Activity {
/** * Indicates whether the Activity is in the running state: if onResume is executed, it becomes running. * /
private boolean running;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
// Unreference and listen the Activity from the Service. }@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (running) {
// Establish a reference and listener relationship between the Activity and the Service. }}};@Override
public void onResume(a) {
super.onResume();
running = true;
Intent intent = new Intent(this, SomeService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
@Override
public void onPause(a) {
super.onPause();
running = false;
// Unreference and listen the Activity from the Service. unbindService(serviceConnection); }}Copy the code
Let’s take a look at a small example of iOS.
Now suppose we want to maintain a client-to-server TCP long connection. This connection is automatically reconnected when the network state changes. First, we need a class to listen for changes in network state. This class is called Reachability.
//
// Reachability.h
//
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
extern NSString *const networkStatusNotificationInfoKey;
extern NSString *const kReachabilityChangedNotification;
typedef NS_ENUM(uint32_t, NetworkStatus) {
NotReachable = 0,
ReachableViaWiFi = 1,
ReachableViaWWAN = 2
};
@interface Reachability : NSObject {
@private
SCNetworkReachabilityRef reachabilityRef;
}
/** * Start network status monitoring */
- (BOOL)startNetworkMonitoring;
/** * End network status monitoring */
- (BOOL)stopNetworkMonitoring;
/** * Synchronizes the current network status */
- (NetworkStatus) currentNetworkStatus;
@end
//
// Reachability.m
//
#import "Reachability.h"
#import <sys/socket.h>
#import <netinet/in.h>
NSString *const networkStatusNotificationInfoKey = @"networkStatus";
NSString *const kReachabilityChangedNotification = @"NetworkReachabilityChangedNotification";
@implementation Reachability
- (instancetype)init {
self = [super init];
if (self) {
struct sockaddr_in zeroAddress;
memset(&zeroAddress, 0.sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
reachabilityRef = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)&zeroAddress);
}
return self;
}
- (void)dealloc {
if (reachabilityRef) {
CFRelease(reachabilityRef); }}static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) {
Reachability *reachability = (__bridge Reachability *) info;
@autoreleasepool {
NetworkStatus networkStatus = [reachability currentNetworkStatus];
[[NSNotificationCenterdefaultCenter] postNotificationName:kReachabilityChangedNotification object:reachability userInfo:@{networkStatusNotificationInfoKey : @(networkStatus)}]; }} - (BOOL)startNetworkMonitoring {
SCNetworkReachabilityContext context = {0, (__bridge void * _Nullable)(self), NULL.NULL.NULL};
if(SCNetworkReachabilitySetCallback(reachabilityRef, ReachabilityCallback, &context)) {
if(SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)) {
return YES; }}return NO;
}
- (BOOL)stopNetworkMonitoring {
return SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
}
- (NetworkStatus) currentNetworkStatus {
// This code ignores...
}
@endCopy the code
The code above encapsulates the interface to the Reachability class. StartNetworkMonitoring is called when the caller wants to start network state monitoring; Call stopNetworkMonitoring when listening is complete. The long connections we envision just require creating and calling Reachability objects to handle network state changes. The relevant part of its code might look something like this (Class name ServerConnection; Header code ignored) :
//
// ServerConnection.m
//
#import "ServerConnection.h"
#import "Reachability.h"
@interface ServerConnection(a){
// GCD queue where the user performs socket operations
dispatch_queue_t socketQueue;
Reachability *reachability;
}
@end
@implementation ServerConnection
- (instancetype)init {
self = [super init];
if (self) {
socketQueue = dispatch_queue_create("SocketQueue".NULL);
reachability = [[Reachability alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkStateChanged:) name:kReachabilityChangedNotification object:reachability];
[reachability startNetworkMonitoring];
}
return self;
}
- (void)dealloc {
[reachability stopNetworkMonitoring];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)networkStateChanged:(NSNotification *)notification {
NetworkStatus networkStatus = [notification.userInfo[networkStatusNotificationInfoKey] unsignedIntValue];
if(networkStatus ! = NotReachable) {// Network changes, reconnection
dispatch_async(socketQueue, ^{
[selfreconnect]; }); }} - (void)reconnect {
// This code ignores...
}
@endCopy the code
Long connection ServerConnection creates an instance of Reachability at initialization, starts listening (call startNetworkMonitoring), sets the listening method (networkStateChanged 🙂 via system broadcast; Stop listening (call stopNetworkMonitoring) when the long connection ServerConnection is destroyed (dealloc).
NetworkStateChanged: is called when the network state changes, and the current network state is passed in. If the network is found to be available (in the non-notreachable state), reconnection is performed asynchronously.
The process seems reasonable. But therein lies a fatal problem.
When reconnecting, we start an asynchronous task with dispatch_async. Depending on how fast the reconnect operation executes, it is unpredictable when the asynchronous task will complete after it starts. Given that the reconnect executes slowly (which is likely for operations involving networks), you might have a situation where the Reconnect is still running, but the ServerConnection is about to be destroyed. That is, all other references to the ServerConnection in the entire system have been released, leaving only a reference from block to self for dispatch_async.
Where does this lead?
This will result in the ServerConnection actually being released when reconnect completes and its dealloc method is not executed on the main thread! Instead, it executes on socketQueue.
And what happens next? It depends on the implementation of Reachability.
Let’s reanalyze the code at Reachability to get the final impact of this incident. StopNetworkMonitoring for Reachability is called on the off-main thread when this happens. StartNetworkMonitoring is called from the main thread. As you can see now, startNetworkMonitoring and stopNetworkMonitoring do not refer to the same RunLoop in their implementations if they are not executed on the same thread. This is already logically “wrong”. After this “error” occurred, StopNetworkMonitoring SCNetworkReachabilityUnscheduleFromRunLoop, no to the Reachability of instance from the original that Run on the main thread scheduling unloaded on the Loop. That is, if the network state changes again thereafter, the ReachabilityCallback will still be executed, but at this point the original Reachability instance has been destroyed (by the ServerConnection destruction). With the current implementation of the code above, the info parameter in the ReachabilityCallback points to a Reachability object that has been freed, so it is not surprising that a subsequent crash will occur.
One might say that instead of referring directly to self in a block executed by dispatch_async, weak-strong dance should be used. Change the dispatch_async code to the following form:
__weak ServerConnection *wself = self;
dispatch_async(socketQueue, ^{
__strong ServerConnection *sself = wself;
[sself reconnect];
});Copy the code
Does this change have any effect? Apparently not, according to our analysis above. ServerConnection’s dealloc is still executed on the non-main thread, and the above problem still exists. Weak-strong dance is designed to solve the problem of circular references, but it does not solve the problem of asynchronous task delay that we encountered here.
In fact, even if you change it to the form below, it still has no effect.
__weak ServerConnection *wself = self;
dispatch_async(socketQueue, ^{
[wself reconnect];
});Copy the code
Even if you call reconnect with the weak reference (wself), it will cause the ServerConnection reference count to increase once executed. The result is still that dealloc is executed on a non-main thread.
Since executing dealloc on a non-main thread can cause problems, we force the dealloc code to execute on the main thread as follows:
- (void)dealloc {
dispatch_async(dispatch_get_main_queue(), ^{
[reachability stopNetworkMonitoring];
});
[[NSNotificationCenter defaultCenter] removeObserver:self];
}Copy the code
Obviously, calling dispatch_async at dealloc isn’t going to work either. Because the ServerConnection instance has been destroyed after dealloc has been executed, reachability relies on a ServerConnection instance that has been destroyed when the block executes. And it crashed.
Instead of dispatch_async, use dispatch_sync. The carefully modified code looks like this:
- (void)dealloc {
if(! [NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[reachability stopNetworkMonitoring];
});
}
else {
[reachability stopNetworkMonitoring];
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}Copy the code
After “back and forth” patches, we now have a piece of code that basically works. However, executing dispatch_sync, a potentially time-consuming “synchronous” operation within Dealloc, is always scary.
So what better way to do it?
Personal opinion: Not all destruction work is suitable for dealloc.
What Dealloc does best, of course, is free memory, such as calling the release of individual member variables (which is also omitted in ARC). However, it is not a good idea to rely on Dealloc to maintain variables or procedures that are more scoped (beyond the current object’s life cycle). There are at least two reasons:
- The execution of dealloc may be delayed to ensure precise execution time;
- There is no control over whether dealloc will be called on the main thread.
As in the ServerConnection example above, the business logic must know when to stop listening for network state and should not rely on Dealloc to do it.
In addition, we should pay special attention to the possibility that dealloc will execute on asynchronous threads. We should adopt different attitudes towards different types of objects. For example, for objects that play the role of View, the correct attitude is that dealloc should not be allowed to execute on asynchronous threads. To avoid this, we should try to avoid starting asynchronous tasks directly in the View or making strong references to the View in asynchronous tasks with longer life cycles.
In both of the examples above, the root cause of the problem is the asynchronous task. When we think about it, we have to focus on one of the most important issues when discussing asynchronous tasks: conditional failure. Of course, this is also an obvious problem: by the time an asynchronous task actually executes (or an asynchronous event actually occurs), the circumstances may well be different from when it was originally scheduled, or the conditions under which it was originally executed or occurred may no longer be valid.
In the first Service Binding example, the Activity is still Running (executing onResume) when the asynchronous Binding process starts scheduling (when bindService is called). At the end of the binding process (when onServiceConnected is called), the Activity exits from its Running state (after onPause, the Activity is unbound again).
In the second network listening example, when the asynchronous reconnection task ends, the external reference to the ServerConnection instance no longer exists and the instance is about to be destroyed. The Run Loop that stops listening is no longer the same.
Before moving on to the next formal discussion of asynchronous tasks, it’s worth summarizing the asynchronous tasks that iOS and Android often encounter.
-
Network request. Because network requests take a long time, network request interfaces are usually asynchronous (such as NSURLConnection for iOS, or Volley for Android). Typically, we start a network request on the main thread, then passively wait for a successful or failed callback to occur (meaning the end of the asynchronous task), and then update the UI based on the result of the callback. The time between starting a network request and knowing the definite result of the request (success or failure) is uncertain.
-
Asynchronous tasks that are actively created through the thread pool mechanism. For tasks that need to be executed synchronously for a long period of time (such as high-latency operations such as reading disk files, or performing high-computation tasks), we usually rely on the thread pool mechanism provided by the system to schedule these tasks to asynchronous threads to save precious calculation time of the main thread. For these thread pool mechanisms, in iOS we have GCD (dispatch_async), NSOperationQueue; On Android, we have traditional ExecutorService provided by the JDK, and AsyncTask provided by the Android SDK. Either way, we’ve created a lot of asynchronous tasks for ourselves.
-
Run Loop Schedules tasks. On iOS, we can call the method of several performSelectorXXX NSObject will task scheduling to the target thread’s Run Loop up executed asynchronously (except performSelectorInBackground: withObject:). Similarly, on Android, we can call Handler’s POST /sendMessage method or View’s POST method to asynchronously schedule tasks to the corresponding Run Loop. In fact, in both iOS and Android systems, a Run Loop is typically created in the client infrastructure for the main thread (of course, non-main thread can also create a Run Loop). It allows long-lived threads to periodically process short tasks, and to sleep when there are no tasks to execute, thus efficiently and timely responding to event processing without consuming excessive CPU time. Also, more importantly, the Run Loop mode simplifies the multithreaded programming logic on the client side. Client-side programming is simpler than the multithreaded model of server programming, thanks in large part to Run Loop. In client programming, when we want to execute a long synchronous task, we usually first schedule it to the asynchronous thread through the thread pool mechanism mentioned in (2) above, and then re-schedule it to the Run Loop of the main thread through the Run Loop scheduling method mentioned in this section or GCD and other mechanisms. This pattern of “main thread -> asynchronous thread -> main thread” has become the basic pattern of client multithreaded programming. This mode avoids complex synchronization operations that may exist between multiple threads and makes processing simple. We’ll have a chance to continue this discussion later in part (3) — performing multiple asynchronous tasks.
-
Delay scheduling tasks. This type of task starts execution after a specified period of time or at a specified point in time and can be used to implement structures like retry queues. Deferred scheduling tasks can be implemented in many ways. In iOS, NSObject performSelector: withObject: afterDelay:, the GCD dispatch_after or dispatch_time, in addition, there are NSTimer; On Android, postDelayed with Handler and postAtTime, postDelayed with View, and old-fashioned java.util.Timer, Another heavy scheduler in Android is AlarmService, which automatically wakes up the program when a task is scheduled.
-
Asynchronous behavior associated with the system implementation. There are many different kinds of behaviors, but here are a few examples. For example, startActivity in Android is an asynchronous operation, and there is still a small amount of time between the time the Activity is called and the time it is created and displayed. Another example: The Activity and Fragment life cycles are asynchronous, and even if the Activity’s life cycle reaches onResume, you still don’t know where the Fragment’s life cycle is (and whether its view level has been created). For example, both iOS and Android systems have mechanisms to listen for network state changes (as in the second code example earlier in this article), and when a network state change callback is executed is an asynchronous event. These asynchronous behaviors also require unified and complete asynchronous processing.
At the end of this paper, there is a question about the title that needs to be clarified. This series is titled “Asynchronous Processing in Android and iOS Development,” but the topic of processing asynchronous tasks is not limited to “iOS or Android development,” and can be encountered in server development, for example. What I want to express in this series is more of an abstract logic, not limited to a specific technology of iOS or Android. It’s just that asynchronous tasks are so widely used in iOS and Android front-end development that it should be treated as a more common problem.
(after)
Other selected articles:
- Authentic technology with wild way
- How annoying is Android push?
- Manage App numbers and red dot tips with a tree model
- A diagram to read thread control in RxJava
- The descriptor of the End of the Universe (2)
- Redis internal data structure details (5) – QuickList
- Redis internal data structure (4) – Ziplist
- The programmer’s cosmic timeline
- Redis internal data structure (1) – dict