This article is the second in a series on Asynchronous processing in Android and iOS development. In this article, we’ll focus on a number of issues related to callbacks to asynchronous tasks.
In iOS, callbacks are usually represented as a delegate; In Android, callbacks usually exist in the form of a listener. But no matter how it manifests itself, callbacks are an integral part of interface design. The quality of callback interface design directly affects the success of the whole interface design.
So what should we consider in the design and implementation of the callback interface? Now let’s list the sub-topics discussed in this article as follows, and then discuss them one by one:
- A result callback must be generated
- Note that failure callbacks & error codes should be as detailed as possible
- There should be a clear correspondence between the calling interface and the callback interface
- Successful and failed result callbacks should be mutually exclusive
- The thread model for callbacks
- Context parameters for the callback (pass-through parameters)
- The callback order
- Closure form Callback Hell
Note: The code that appears in this series has been collated to GitHub (which is constantly updated), and the repository address is:
- Github.com/tielei/Asyn…
Among them, the Java code in this article, the com. Zhangtielei. Demos. Async. Programming. The callback in the package.
A result callback must be generated
When an interface is designed to be asynchronous, the final execution result of the interface is returned to the caller via callbacks.
But the callback interface doesn’t always deliver the end result. In fact, we can classify callbacks into two categories:
- In the middle of the callback
- Results the callback
The result callback contains both success and failure result callbacks.
Intermediate callbacks may be invoked when an asynchronous task starts executing, when the execution schedule is updated, or when other important intermediate events occur; The result callback is not invoked until the end of the asynchronous task execution and there is a definite result (success or failure). The resulting callback completes execution of the asynchronous interface.
The “result callback must be produced” rule is not as easy to follow as you might think. It requires that no matter what exception occurs in the implementation of an asynchronous interface, a result callback is generated within a limited time. For example, receiving invalid input parameters, a program runtime exception, aborting a task halfway through, a task timeout, and unexpected errors are all examples of exceptions.
The difficulty here is that the implementation of the interface has to be careful about all possible error situations and must produce a result callback in either case. Otherwise, the caller’s entire execution process may be interrupted.
Note that failure callbacks & error codes should be as detailed as possible
Let’s start with a code example:
public interface Downloader {
/** * sets the listener. *@param listener
*/
void setListener(DownloadListener listener);
/** * Start the resource download. *@paramUrl The address of the resource to download. *@paramLocalPath The local location where the resource will be stored after downloading. */
void startDownload(String url, String localPath);
}
public interface DownloadListener {
/** * Download end callback. *@paramResult Download result True: download success false: download failure *@paramUrl Resource address *@paramLocalPath Indicates the location where the downloaded resources are stored. This parameter is valid only if result=true
void downloadFinished(boolean result, String url, String localPath);
/** * Download progress callback. *@paramUrl Resource address *@paramDownloadedSize Download size. *@paramTotalSize Total resource size */
void downloadProgress(String url, long downloadedSize, long totalSize);
}Copy the code
This code defines a downloader interface for downloading resources from the specified URL. This is an asynchronous interface where the caller initiates the download task by calling startDownload and then waits for a callback. When the downloadFinished callback occurs, the download task is complete. If result=true is returned, the download succeeds; otherwise, the download fails.
The interface definition is one of the more complete basically, can complete the basic flow of download resources: we can download through the interface to start a task, in the process of download for download progress callback (middle), the results can be obtained when download success, can also be notified when failed download (success and failure are results callback). However, if we want to know more about why a download failed, this interface is not available now.
The specific cause of failure may or may not need to be addressed by the upper-level caller. After a download failure, the upper presentation layer may simply flag the resource that failed to download, regardless of how it failed. Of course, it is also possible that the presentation layer will prompt the user with the specific cause of the failure, so that the user can know what needs to be done to recover the error. For example, if the download fails due to “network unavailable”, the user can be prompted to switch to a better network. If the download fails due to Insufficient storage space, you can prompt the user to clear the storage space. In short, it is up to the upper-level caller to decide whether and how to display the specific error cause, not when defining the underlying callback interface.
Therefore, a failure callback in a result callback should return the error code in as much detail as possible to give the caller more options if an error occurs. This rule seems obvious to the library developers. However, for the developers of upper level applications, it is often not given enough attention. Returning verbose error codes means more effort in failure handling. In order to “save time” and “be pragmatic,” people often take “easy fixes” for error situations, but they create potential problems for future expansion.
To return more detailed error codes, the code for the DownloadListener is modified as follows:
public interface DownloadListener {
/** * error code definition */
public static final int SUCCESS = 0;/ / success
public static final int INVALID_PARAMS = 1;// The input parameter is incorrect
public static final int NETWORK_UNAVAILABLE = 2;// The network is unavailable
public static final int UNKNOWN_HOST = 3;// Domain name resolution failed
public static final int CONNECT_TIMEOUT = 4;// Connection timed out
public static final int HTTP_STATUS_NOT_OK = 5;// Download request returns a non-200
public static final int SDCARD_NOT_EXISTS = 6;//SD card does not exist (downloaded resources do not exist)
public static final int SD_CARD_NO_SPACE_LEFT = 7;// There is not enough space on the SD card.
public static final int READ_ONLY_FILE_SYSTEM = 8;// The file system is read-only.
public static final int LOCAL_IO_ERROR = 9;// Error related to local SD access
public static final int UNKNOWN_FAILED = 10;// Other unknown errors
/** * Download successful callback. *@paramUrl Resource address *@paramLocalPath Storage location of the downloaded resource. */
void downloadSuccess(String url, String localPath);
/** * Download failed callback. *@paramUrl Resource address *@paramErrorCode indicates the errorCode. *@paramErrorMessage brief description of the errorMessage. For the caller to understand the cause of the error. */
void downloadFailed(String url, int errorCode, String errorMessage);
/** * Download progress callback. *@paramUrl Resource address *@paramDownloadedSize Download size. *@paramTotalSize Total resource size */
void downloadProgress(String url, long downloadedSize, long totalSize);
}Copy the code
In iOS, the Foundation Framework has a systematic wrapper around program errors: NSError. It can encapsulate error codes in a very general way, and it can divide errors into different domains. NSError is a good example of a failed callback interface definition.
There should be a clear correspondence between the calling interface and the callback interface
Let’s examine this problem with an example of a real interface definition.
The following is the interface definition code of the video advertisement integral wall from a domestic advertising platform (some irrelevant codes are omitted for clarity).
@class IndependentVideoManager;
@protocol IndependentVideoManagerDelegate <NSObject>
@optional
#pragma Mark - Independent video present callback.Pragma mark - point manage callback points.Pragma mark - Independent video status Callback integration wall state
/** * Whether the video AD wall is available. * Called after get independent video enable status. * * @param IndependentVideoManager * @param enable */
- (void)ivManager:(IndependentVideoManager *)manager
didCheckEnableStatus:(BOOL)enable;
/** * Whether any video ads can be played. * Called after check independent video available. * * @param IndependentVideoManager * @param available */
- (void)ivManager:(IndependentVideoManager *)manager
isIndependentVideoAvailable:(BOOL)available;
@end
@interface IndependentVideoManager : NSObject {}@property(nonatomic.assign)id<IndependentVideoManagerDelegate>delegate; .#pragma mark-init initializes the related methods.Pragma Mark - Independent video present integral wall display related methods
/** * Use App rootViewController to pop up and display the list integral wall. * Present independent video in ModelView way with App's rootViewController. * * @param type Integral wall type */
- (void)presentIndependentVideo; .#pragma Mark - Independent video Status Checks whether the video score wall is available
/** * Check independent video available. */
- (void)checkVideoAvailable;
#pragma Mark - Point Manage points related ads
/** * Checks the credits already earned, and the corresponding method in the proxy is called back on success or failure. * * /
- (void)checkOwnedPoint;
/** * consumes the specified number of credits. Success or failure calls back to the corresponding method in the broker (note that the argument type is unsigned int and the credits to consume are non-negative). * * @param Point Number of points to consume */
- (void)consumeWithPointNumber:(NSUInteger)point;
@endCopy the code
Let’s examine the mapping between the calling and callback interfaces in this interface definition.
Use IndependentVideoManager to call the interface, in addition to the initial interface, there are several main:
- Pop up and display independentVideo
- Check if any video ads are available to play (checkVideoAvailable)
- Score management (checkOwnedPoint and consumeWithPointNumber:)
The callback interface (IndependentVideoManagerDelegate) can be divided into the following categories:
- Video ads display callback classes
- Integral wall state class (ivManager: didCheckEnableStatus: and ivManager: isIndependentVideoAvailable:)
- Integral Management
Generally speaking, the corresponding relationship here is quite clear, these three callback interfaces can basically correspond with the previous three call interfaces.
However, the callback interface for the integrating Wall state class has a bit of a confusing detail: Look the caller in the call after checkVideoAvailable, will receive state class two integral wall callback (ivManager: didCheckEnableStatus: and ivManager: isIndependentVideoAvailable:); However, from the interface name can express the meaning of the term, call checkVideoAvailable is to check whether there is a video advertising can play, so just ivManager: isIndependentVideoAvailable: this a callback interface to return the result of the need, Doesn’t seem to need ivManager: didCheckEnableStatus:. The meaning of the expression from ivManager: didCheckEnableStatus (video advertising wall is available), it seems to be any call interface is called may perform, and should not be only corresponding checkVideoAvailable. The design of the callback interface here is confusing in terms of its correspondence to the calling interface.
In addition, the IndependentVideoManager interface has some issues with the design of context parameters, which will be addressed later in this article.
Successful and failed result callbacks should be mutually exclusive
When an asynchronous task terminates, it either invokes a success result callback or a failure result callback. You can only call one or the other. This is an obvious requirement, but if you don’t pay attention to implementing it, you may not be able to comply with it.
Assume that the Downloader interface we mentioned earlier has the following code when it finally generates the result callback:
int errorCode = parseDownloadResult(result);
if (errorCode == SUCCESS) {
listener.downloadSuccess(url, localPath)
}
else {
listener.downloadFailed(url, errorCode, getErrorMessage(errorCode));
}Copy the code
It turns out that in order to achieve the “must produce result callback” goal, we should consider the possibility of the parseDownloadResult method throwing an exception. So we changed the code to look like this:
try {
int errorCode = parseDownloadResult(result);
if (errorCode == SUCCESS) {
listener.downloadSuccess(url, localPath)
}
else{ listener.downloadFailed(url, errorCode, getErrorMessage(errorCode)); }}catch (Exception e) {
listener.downloadFailed(url, UNKNOWN_FAILED, getErrorMessage(UNKNOWN_FAILED));
}Copy the code
Changing the code this way already ensures that even if something unexpected happens, a failure callback will be generated for the caller.
However, this brings up another problem: what if the implementation code of the callback interface throws an exception when calling listener.downloadSuccess or Listener. downloadFailed? That will cause one more call to listener.downloadFailed. Thus, success and failure callbacks are no longer called mutually exclusive: either success and failure callbacks occur, or two failure callbacks in a row.
The implementation of the callback interface is the responsibility of the caller. Do we need to worry about mistakes made by the caller? First, this is primarily the responsibility of the upper callers, and the implementation of the callback interface (the caller) really shouldn’t throw an exception back when it happens. However, designers of the underlying interfaces should also do their best. As designers of interfaces, we often can’t anticipate how callers will behave, and wouldn’t it be nice if we could ensure that the current error doesn’t break and freeze the entire process when an exception occurs? So, we could try changing the code to something like this:
int errorCode;
try {
errorCode = parseDownloadResult(result);
}
catch (Exception e) {
errorCode = UNKNOWN_FAILED;
}
if (errorCode == SUCCESS) {
try {
listener.downloadSuccess(url, localPath)
}
catch(Throwable e) { e.printStackTrace(); }}else {
try {
listener.downloadFailed(url, errorCode, getErrorMessage(errorCode));
}
catch(Throwable e) { e.printStackTrace(); }}Copy the code
The callback code is a bit more complex, but also more secure.
The thread model for callbacks
Asynchronous interfaces can be implemented on the basis of two main technologies:
- Multithreading (the implementation code of the interface is executed in a different asynchronous thread than the calling thread)
- Asynchronous IO (such as asynchronous network requests). In this case, the asynchronous interface can be implemented even if the entire program has only one thread.)
In either case, we need to have a clear definition of the thread environment in which the callback occurs.
Generally speaking, there are three main modes in which the execution thread environment defines the result callback:
- The result callback occurs on the thread on which the interface is invoked.
- Regardless of which thread the interface is invoked on, the result callback occurs on the main thread (such as Android AsyncTask).
- Callers can customize the thread on which the callback interface occurs. (such as iOS NSURLConnection, through scheduleInRunLoop: forMode: to set up the callback of the Run Loop)
The third pattern is obviously the most flexible, because it includes the first two.
In order to be able to schedule execution code to other threads, we need to use asynchronous processing in the last Android and iOS development article (I) — an overview of some of the last mentioned technologies, such as the GCD, NSOperationQueue, performSelectorXXX methods in iOS, Android has ExecutorService, AsyncTask, Handler, and so on (note: ExecutorService cannot be used to schedule to the main thread, only to asynchronous threads). It’s important to understand the nature of thread scheduling: you can schedule a piece of code to execute on a thread if that thread has an Event Loop. This Loop, as the name implies, is a Loop that continuously pulls messages from the message queue and processes them. When we do thread scheduling, we’re sending messages to this queue. The Queue itself is guaranteed to be a Thread Safe Queue in the system implementation, so the caller circumvents thread-safety issues. In client development, the system always creates a Loop for the main thread, but it is up to the developer to create the non-main thread using appropriate techniques.
In most cases of client programming, we want the result callback to happen on the main thread, because that’s when we update the UI. The thread on which the intermediate callback is executed depends on the application scenario. In the previous Downloader example, the intermediate callback downloadProgress is used to send back the downloadProgress, which is usually displayed on the UI, so downloadProgress is also better scheduled for execution on the main thread.
Context parameters for the callback (pass-through parameters)
When calling an asynchronous interface, we often need to temporarily store a copy of the context data associated with the call, which can be retrieved when the callback occurs after the asynchronous task has executed.
Let’s go back to the previous example of the downloader. For the sake of clarity, let’s assume a slightly more complicated example. Let’s say we want to download several emojis, each containing multiple emojis image files. After downloading all the emojis, we need to install the emojis locally (possibly modifying the local database) so that the user can use them in the input panel.
Suppose the data structure of the emoji is defined as follows:
public class EmojiPackage {
/** ** ID */
public long emojiId;
/** ** list of emoticons */
public List<String> emojiUrls;
}Copy the code
During the download process, we need to save a context structure as follows:
public class EmojiDownloadContext {
/** ** The emojis that are currently being downloaded */
public EmojiPackage emojiPackage;
/** ** Count the emoticons that have been downloaded */
public int downloadedEmoji;
/** ** The local address of the downloaded emoticons */
public List<String> localPathList = new ArrayList<String>();
}Copy the code
Also assume that the emoji loader we want to implement follows the following interface definition:
public interface EmojiDownloader {
/** * Start downloading the specified emojis *@param emojiPackage
*/
void startDownloadEmoji(EmojiPackage emojiPackage);
/** * The interface related to the callback is defined here and ignored. That's not what we're talking about
//TODO:Definition of the callback interface
}Copy the code
If we use the existing Downloader interface to implement the emoticon Downloader, we may have three different approaches depending on how the context is passed:
(1) Globally save a context.
Note: When I say “global” here, I’m talking about the inside of an emoji downloader. The code is as follows:
public class MyEmojiDownloader implements EmojiDownloader.DownloadListener {
/** * Globally save a copy of the emoticon download context. */
private EmojiDownloadContext downloadContext;
private Downloader downloader;
public MyEmojiDownloader(a) {
// The instantiation has a downloader. MyDownloader is an implementation of the Downloader interface.
downloader = new MyDownloader();
downloader.setListener(this);
}
@Override
public void startDownloadEmoji(EmojiPackage emojiPackage) {
if (downloadContext == null) {
// Create download context data
downloadContext = new EmojiDownloadContext();
downloadContext.emojiPackage = emojiPackage;
// Start downloading emoticon image file 0
downloader.startDownload(emojiPackage.emojiUrls.get(0),
getLocalPathForEmoji(emojiPackage, 0)); }}@Override
public void downloadSuccess(String url, String localPath) {
downloadContext.localPathList.add(localPath);
downloadContext.downloadedEmoji++;
EmojiPackage emojiPackage = downloadContext.emojiPackage;
if (downloadContext.downloadedEmoji < emojiPackage.emojiUrls.size()) {
// Continue to download the next emoticon image
String nextUrl = emojiPackage.emojiUrls.get(downloadContext.downloadedEmoji);
downloader.startDownload(nextUrl,
getLocalPathForEmoji(emojiPackage, downloadContext.downloadedEmoji));
}
else {
// The download is complete
installEmojiPackageLocally(emojiPackage, downloadContext.localPathList);
downloadContext = null; }}@Override
public void downloadFailed(String url, int errorCode, String errorMessage) {... }@Override
public void downloadProgress(String url, long downloadedSize, long totalSize) {... }/** * Calculate the download address of the i-th emoticon image file in the emoticon package. */
private String getLocalPathForEmoji(EmojiPackage emojiPackage, int i) {... }/** * Install emojis locally */
private void installEmojiPackageLocally(EmojiPackage emojiPackage, List<String> localPathList) {... }}Copy the code
The downside: only one meme can be downloaded at a time. You have to wait until the previous emoji has been downloaded before you can start downloading a new emoji.
While there are obvious drawbacks to this “globally save a context” approach, in some cases this is the only way to go. We’ll come back to that later.
(2) Use mapping to save context.
Under the current definition of Downloader interface, we can only use URL as the index of this mapping relationship. Since a meme contains multiple urls, we must index a context for each URL. The code is as follows:
public class MyEmojiDownloader implements EmojiDownloader.DownloadListener {
* URL -> EmojiDownloadContext */
private Map<String, EmojiDownloadContext> downloadContextMap;
private Downloader downloader;
public MyEmojiDownloader(a) {
downloadContextMap = new HashMap<String, EmojiDownloadContext>();
// The instantiation has a downloader. MyDownloader is an implementation of the Downloader interface.
downloader = new MyDownloader();
downloader.setListener(this);
}
@Override
public void startDownloadEmoji(EmojiPackage emojiPackage) {
// Create download context data
EmojiDownloadContext downloadContext = new EmojiDownloadContext();
downloadContext.emojiPackage = emojiPackage;
// Create a mapping for each URL
for (String emojiUrl : emojiPackage.emojiUrls) {
downloadContextMap.put(emojiUrl, downloadContext);
}
// Start downloading emoticon image file 0
downloader.startDownload(emojiPackage.emojiUrls.get(0),
getLocalPathForEmoji(emojiPackage, 0));
}
@Override
public void downloadSuccess(String url, String localPath) {
EmojiDownloadContext downloadContext = downloadContextMap.get(url);
downloadContext.localPathList.add(localPath);
downloadContext.downloadedEmoji++;
EmojiPackage emojiPackage = downloadContext.emojiPackage;
if (downloadContext.downloadedEmoji < emojiPackage.emojiUrls.size()) {
// Continue to download the next emoticon image
String nextUrl = emojiPackage.emojiUrls.get(downloadContext.downloadedEmoji);
downloader.startDownload(nextUrl,
getLocalPathForEmoji(emojiPackage, downloadContext.downloadedEmoji));
}
else {
// The download is complete
installEmojiPackageLocally(emojiPackage, downloadContext.localPathList);
// Remove the mapping for each URL
for(String emojiUrl : emojiPackage.emojiUrls) { downloadContextMap.remove(emojiUrl); }}}@Override
public void downloadFailed(String url, int errorCode, String errorMessage) {... }@Override
public void downloadProgress(String url, long downloadedSize, long totalSize) {... }/** * Calculate the download address of the i-th emoticon image file in the emoticon package. */
private String getLocalPathForEmoji(EmojiPackage emojiPackage, int i) {... }/** * Install emojis locally */
private void installEmojiPackageLocally(EmojiPackage emojiPackage, List<String> localPathList) {... }}Copy the code
This approach has its drawbacks: you can’t always find the right variable that uniquely indexes the context data. In the example of the emojiloader, the variable that uniquely identifies the download should be the emojiId, but this value is not available in the Downloader callback interface, so instead each URL has an index to the context data. As a result, if two different memes contain the same URL, there could be a conflict. In addition, the implementation of this approach is complicated.
(3) Create an interface instance for each asynchronous task.
In general, by design, we want to instantiate only one instance of the interface (that is, one Downloader instance) and then use that one instance to start multiple asynchronous tasks. However, if we create an interface instance each time we start a new asynchronous task, then the asynchronous task corresponds to the number of interface instances, and the context data of the asynchronous task can be stored in the interface instance. The code is as follows:
public class MyEmojiDownloader implements EmojiDownloader {
@Override
public void startDownloadEmoji(EmojiPackage emojiPackage) {
// Create download context data
EmojiDownloadContext downloadContext = new EmojiDownloadContext();
downloadContext.emojiPackage = emojiPackage;
// Create a new Downloader for each download
final EmojiUrlDownloader downloader = new EmojiUrlDownloader();
// Save the context data to the downloader instance
downloader.downloadContext = downloadContext;
downloader.setListener(new DownloadListener() {
@Override
public void downloadSuccess(String url, String localPath) {
EmojiDownloadContext downloadContext = downloader.downloadContext;
downloadContext.localPathList.add(localPath);
downloadContext.downloadedEmoji++;
EmojiPackage emojiPackage = downloadContext.emojiPackage;
if (downloadContext.downloadedEmoji < emojiPackage.emojiUrls.size()) {
// Continue to download the next emoticon image
String nextUrl = emojiPackage.emojiUrls.get(downloadContext.downloadedEmoji);
downloader.startDownload(nextUrl,
getLocalPathForEmoji(emojiPackage, downloadContext.downloadedEmoji));
}
else {
// The download is completeinstallEmojiPackageLocally(emojiPackage, downloadContext.localPathList); }}@Override
public void downloadFailed(String url, int errorCode, String errorMessage) {
//TODO:
}
@Override
public void downloadProgress(String url, long downloadedSize, long totalSize) {
//TODO:}});// Start downloading emoticon image file 0
downloader.startDownload(emojiPackage.emojiUrls.get(0),
getLocalPathForEmoji(emojiPackage, 0));
}
private static class EmojiUrlDownloader extends MyDownloader {
public EmojiDownloadContext downloadContext;
}
/** * Calculate the download address of the i-th emoticon image file in the emoticon package. */
private String getLocalPathForEmoji(EmojiPackage emojiPackage, int i) {... }/** * Install emojis locally */
private void installEmojiPackageLocally(EmojiPackage emojiPackage, List<String> localPathList) {... }}Copy the code
The downsides are obvious: creating a Downloader instance for each download task defeats our intent for the Downloader interface. This creates a large number of redundant instances. In particular, when the interface instance is a large, heavy object, this can be costly.
Each of the above three approaches is not ideal. The root cause: the underlying asynchronous Downloader does not support context passing (note that it has nothing to do with the Android context). Such context parameters are called different things by different people:
- Context
- Passthrough parameters
- callbackData
- cookie
- userInfo
No matter what the name of this parameter is, it does the same thing: it is passed in when the asynchronous interface is called, and it is returned when the callback interface occurs. This context parameter is defined by the upper level caller, and the implementation of the lower level interface is not responsible for understanding its meaning, but for passing through.
The Downloader interface that supports context parameters changes as follows:
public interface Downloader {
/** * sets the callback listener. *@param listener
*/
void setListener(DownloadListener listener);
/** * Start the resource download. *@paramUrl The address of the resource to download. *@paramLocalPath The local location to store the resource after it is downloaded. *@paramContextData contextData, which is returned transparently in the callback interface. Can be any type. */
void startDownload(String url, String localPath, Object contextData);
}
public interface DownloadListener {
/** * error code definition */
public static final int SUCCESS = 0;/ / success
public static final int INVALID_PARAMS = 1;// The input parameter is incorrect
public static final int NETWORK_UNAVAILABLE = 2;// The network is unavailable
public static final int UNKNOWN_HOST = 3;// Domain name resolution failed
public static final int CONNECT_TIMEOUT = 4;// Connection timed out
public static final int HTTP_STATUS_NOT_OK = 5;// Download request returns a non-200
public static final int SDCARD_NOT_EXISTS = 6;//SD card does not exist (downloaded resources do not exist)
public static final int SD_CARD_NO_SPACE_LEFT = 7;// There is not enough space on the SD card.
public static final int READ_ONLY_FILE_SYSTEM = 8;// The file system is read-only.
public static final int LOCAL_IO_ERROR = 9;// Error related to local SD access
public static final int UNKNOWN_FAILED = 10;// Other unknown errors
/** * Download successful callback. *@paramUrl Resource address *@paramLocalPath Specifies the location where the downloaded resource is stored. *@paramContextData contextData. */
void downloadSuccess(String url, String localPath, Object contextData);
/** * Download failed callback. *@paramUrl Resource address *@paramErrorCode indicates the errorCode. *@paramErrorMessage brief description of the errorMessage. For the caller to understand the cause of the error. *@paramContextData contextData. */
void downloadFailed(String url, int errorCode, String errorMessage, Object contextData);
/** * Download progress callback. *@paramUrl Resource address *@paramDownloadedSize Download size. *@paramTotalSize Total resource size *@paramContextData contextData. */
void downloadProgress(String url, long downloadedSize, long totalSize, Object contextData);
}Copy the code
With this new Downloader interface, the previous emoticon downloaders have a fourth implementation.
(4) Use asynchronous interfaces that support context passing.
The code is as follows:
public class MyEmojiDownloader implements EmojiDownloader.DownloadListener {
private Downloader downloader;
public MyEmojiDownloader(a) {
// The instantiation has a downloader. MyDownloader is an implementation of the Downloader interface.
downloader = new MyDownloader();
downloader.setListener(this);
}
@Override
public void startDownloadEmoji(EmojiPackage emojiPackage) {
// Create download context data
EmojiDownloadContext downloadContext = new EmojiDownloadContext();
downloadContext.emojiPackage = emojiPackage;
// Start loading emoticon image file 0, pass in the context parameter
downloader.startDownload(emojiPackage.emojiUrls.get(0),
getLocalPathForEmoji(emojiPackage, 0),
downloadContext);
}
@Override
public void downloadSuccess(String url, String localPath, Object contextData) {
// Get the context parameter by down-casting the contextData parameter of the callback interface
EmojiDownloadContext downloadContext = (EmojiDownloadContext) contextData;
downloadContext.localPathList.add(localPath);
downloadContext.downloadedEmoji++;
EmojiPackage emojiPackage = downloadContext.emojiPackage;
if (downloadContext.downloadedEmoji < emojiPackage.emojiUrls.size()) {
// Continue to download the next emoticon imageString nextUrl = emojiPackage.emojiUrls.get(downloadContext.downloadedEmoji); downloader.startDownload(nextUrl, getLocalPathForEmoji(emojiPackage, downloadContext.downloadedEmoji), downloadContext); }else {
// The download is completeinstallEmojiPackageLocally(emojiPackage, downloadContext.localPathList); }}@Override
public void downloadFailed(String url, int errorCode, String errorMessage, Object contextData) {... }@Override
public void downloadProgress(String url, long downloadedSize, long totalSize, Object contextData) {... }/** * Calculate the download address of the i-th emoticon image file in the emoticon package. */
private String getLocalPathForEmoji(EmojiPackage emojiPackage, int i) {... }/** * Install emojis locally */
private void installEmojiPackageLocally(EmojiPackage emojiPackage, List<String> localPathList) {... }}Copy the code
Obviously, the fourth implementation is more reasonable, has more compact code, and does not have the disadvantages of the previous three. However, it does require that the underlying asynchronous interface we call has full support for context passing. In practice, most of the interfaces we need to call are given and cannot be modified. If we encounter an interface that does not support context parameter passing well, we have no choice but to do one of the first three. In summary, we discuss the first three practices here not to worry, but to deal with interfaces that don’t support callback contexts enough, often without meaning to.
A typical situation is that the interface provided to us does not support custom context data passing, and we cannot find a suitable variable that uniquely indexes the context data, forcing us to use the previous “save a global context” approach.
At this point, it’s easy to conclude that a good callback interface definition should have the ability to pass custom context data.
Let’s revisit the callback interface definitions of some systems in terms of context-transitive capabilities. For example in the iOS UIAlertViewDelegate alertView: clickedButtonAtIndex:, or UITableViewDataSource tableView: cellForRowAtIndexPath:, The first argument to each of these callback interfaces returns an instance of the UIView itself (most callback interfaces in UIKit are defined in a similar way). This is context-passing, and it can be used to distinguish between different UIView instances, but not between different callbacks within the same UIView instance. If a UIAlertView box needs to pop up multiple times on the same page, we need to create a new UIAlertView instance each time, and then in the callback, we can distinguish the popup box based on the UIAlertView instance returned. This is similar to the third approach discussed earlier. UIView itself has a predefined tag parameter for passing an integer context, but if we want to pass more of the other types of context, we have to inherit a UIView subclass and place the context parameters in it as we did in the third method.
UIView creates an instance for every new presentation, which in itself shouldn’t be considered too much overhead. After all, the typical use of UIViews is to create them one by one and add them to the View hierarchy for presentation. However, the IndependentVideoManager example we mentioned earlier is different. Its callback interface is designed to be the first parameter back IndependentVideoManager instances, such as ivManager: isIndependentVideoAvailable:, you can guess the callback interface definition must be consulted UIKit. IndependentVideoManager, however, is significantly different in that it typically creates only one instance and then plays the AD multiple times by calling the interface multiple times on the same instance. More important here is the distinction between multiple different callbacks on the same instance, and which context parameters each callback carries. The real need for context-passing is similar to the fourth approach we discussed above, and the context-passing capabilities provided by uiKit-like interface definitions are inadequate.
In the design of a callback interface, a key point of context-passing capability is whether it can distinguish multiple callbacks from a single interface instance.
Let’s look at the Android example. The callback interface on Android is presented as a listener. Typical code is as follows:
Button button = (Button) findViewById(...) ; button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {... }});Copy the code
A single Button instance in this code can correspond to multiple callbacks (multi-click events), but we can’t differentiate between these different callbacks using this code. Fortunately, we don’t.
From the discussion above, we found that the development of a partial “front end” related to the View layer generally requires less differentiation between multiple callbacks to a single interface instance, and therefore less complex context-passing mechanisms. However, asynchronous tasks developed in the “back end”, especially those with long life cycles, require stronger context passing capabilities. That’s why the previous article in this series included the issue of “asynchronous processing” as closely related to “back-end” programming.
On the topic of context parameters, there are a few minor issues worth noting: for example, on iOS, does the context parameter remain a strong or weak reference during the execution of an asynchronous task? If it is a strong reference, then if the caller passes in a large object such as the View Controller, it will cause a circular reference, potentially causing a memory leak. Weak references, however, will cause the temporary object to be destroyed as soon as it is created and not passable at all if the caller passes in the context parameter as a temporary object. This is essentially a dilemma posed by the memory management mechanism of reference counting. It depends on what we expected scenarios, we are here to discuss the context parameters can be used to distinguish between a single interface instance of callback for many times, so the incoming context parameters is unlikely to be the life cycle of large objects, and should be the life cycle and an asynchronous task are basically the same object, it at the beginning of each interface call create, Released when a single asynchronous task ends (the result callback occurs). Therefore, in this expected scenario, we should keep a strong reference to the object passed in by the context parameter.
The callback order
Using the previous example of the downloader interface, suppose we call startDownload twice in a row and start two asynchronous download tasks. Then, it is uncertain which of the two download tasks will be completed first. This means that the download task may start first, but the result callback (downloadSuccess or downloadFailed) is executed first. Whether this inconsistency between the callback order and the initial interface call order (which can be called callback out-of-order) is a problem depends on the application scenario and implementation logic of the caller. However, considering from two aspects, we must note that:
- As interface callers, we have to figure out if the interface we are using is “callback out of order.” If so, we need to be careful when dealing with interface callbacks to ensure that they do not have harmful consequences.
- As interface implementers, we need to make it clear when we implement the interface that we provide a strong guarantee that the callback order will not be out of order. The need to provide this assurance increases the complexity of the interface implementation.
On the implementation side of an asynchronous interface, the following factors can cause callbacks to be out of order:
- Early failure results in callback. In fact, this could easily happen, but it’s hard to realize that it could lead to a callback out of order. A typical example is when the implementation of an asynchronous task is scheduled to be executed by another asynchronous thread, but before the asynchronous thread is scheduled, some serious error (such as an error caused by invalid incoming parameters) is detected, terminating the task and triggering a fail-result callback. In this way, an asynchronous task that started later but failed early may be called back one step earlier than a task that started first but ran properly.
- Early successful result callback. This is similar to the “early failure result callback” situation. A typical example is preemptive hits for multi-level caching. For example, the Memory cache is usually checked synchronously. If a hit is made in the Memory cache first, it is possible that the success callback will occur directly in the current main thread, rather than scheduling the callback to another asynchronous thread.
- Concurrent execution of asynchronous tasks. The implementation behind the asynchronous interface may correspond to a concurrent thread pool, so that each asynchronous task executed concurrently is completed in a random order.
- Other asynchronous tasks that are underlying dependencies are callbacks out of order.
Regardless of whether the callbacks are out of order, there are ways to ensure that the order of the callbacks remains the same as the order of the original interface calls. We can create a queue for this, and we can queue the call parameters and other context parameters each time an asynchronous task is initiated by the call interface, and the callbacks are guaranteed to take place in the order in which they are queued.
Perhaps most of the time, the interface caller is not so demanding, and the occasional out-of-order callback is not catastrophic. That is, of course, if the interface caller is aware of it. This makes it less necessary to ensure that callbacks are not out of order in our interface implementation. Of course, the specific choice depends on the requirements of the specific application scenario and the personal preference of the interface implementer.
Closure form Callback Hell
When the asynchronous interface has fewer methods and the callback interface is simple (the callback interface has only one method), we can sometimes define the callback interface as a closure. On iOS, you can use blocks; On Android, you can take advantage of internal anonymous classes (corresponding to Lambda expressions above Java 8).
Suppose the previous DownloadListener was reduced to a single callback method, as follows:
public interface DownloadListener {
/** * error code definition */
public static final int SUCCESS = 0;/ / success
/ /... Other error code definitions (ignored)
/** * Download end callback. *@paramErrorCode indicates the errorCode. SUCCESS indicates that the download is successful. Other error codes indicate that the download fails@paramUrl Resource address. *@paramLocalPath Specifies the location where the downloaded resource is stored. *@paramContextData contextData. */
void downloadFinished(int errorCode, String url, String localPath, Object contextData);
}Copy the code
The Downloader interface can also be simplified by no longer requiring a separate setListener interface and instead receiving the callback interface directly in the download interface. As follows:
public interface Downloader {
/** * Start the resource download. *@paramUrl The address of the resource to download. *@paramLocalPath The local location to store the resource after it is downloaded. *@paramContextData contextData, which is returned transparently in the callback interface. Can be any type. *@paramListener Callback interface instance. */
void startDownload(String url, String localPath, Object contextData, DownloadListener listener);
}Copy the code
An asynchronous interface defined in this way has the advantage of being simpler to call, and callback interface parameters (listeners) can be passed in as closures. However, if the nesting layer is too deep, a Callback Hell (callbackhell.com) can be created. Imagine downloading three files in a row using the Downloader interface above. The closure has three layers of nesting, as follows:
final Downloader downloader = new MyDownloader();
downloader.startDownload(url1, localPathForUrl(url1), null.new DownloadListener() {
@Override
public void downloadFinished(int errorCode, String url, String localPath, Object contextData) {
if(errorCode ! = DownloadListener.SUCCESS) {/ /... Error handling
}
else {
// Download the second URL
downloader.startDownload(url2, localPathForUrl(url2), null.new DownloadListener() {
@Override
public void downloadFinished(int errorCode, String url, String localPath, Object contextData) {
if(errorCode ! = DownloadListener.SUCCESS) {/ /... Error handling
}
else {
// Download the third URL
downloader.startDownload(url3, localPathForUrl(url3), null.new DownloadListener(
) {
@Override
public void downloadFinished(int errorCode, String url, String localPath, Object contextData) {
/ /... End result processing}}); }}}); }}});Copy the code
For Callback Hell, this article, CallBackhell.com, gives some practical advice, such as Keep Your Code shallow and Modularize. In addition, there are some Reactive Programming based solutions such as ReactiveX (RxJava is already popular on Android) that, when properly packaged, work well for Callback Hell.
However, schemes such as ReactiveX are not suitable for all situations with regard to the whole problem of asynchronous programming for asynchronous task processing. And, for the most part, both the code we read from others and the code we generate ourselves are faced with basic asynchronous programming scenarios. It’s mainly logic that needs to be thought through, not just a framework that will solve everything.
As you can see, much of this article is devoted to what may seem obvious, but may be a little wordy. But if we look closely, we find that many of the asynchronous interfaces we are exposed to are not in the most desirable form. We need to be clearly aware of their shortcomings in order to make better use of them. Therefore, it is worth taking some effort to summarize and re-examine the situation.
After all, defining a good interface takes a lot of skill, and few people who have worked for years can do it. This article does not teach you how to define good interfaces and callback interfaces. In fact, no choice is perfect. We need trade-offs.
Finally, we can try to summarize the criteria for good interfaces (not a strict one), and the following come to mind:
- Complete logic (no overlap and no omission of logic of each interface)
- It speaks for itself
- There is a logical abstract model behind it
- Most important: make the caller comfortable and satisfied
(after)
Other selected articles:
- Asynchronous processing in Android and iOS development (PART 1) — Introduction
- 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