Hello, I’m N0tExpectErr0r, an Android developer who loves technology

My personal blog: blog.n0tExpecterr0r.cn

AsyncTask is a thread scheduling framework provided by the Android SDK. It was widely used in the early days of Android, but has been replaced by RxJava, coroutine and other emerging frameworks. Although it has some shortcomings, but its design idea is very interesting, let’s study it today.

Functions overview

First, let’s take a quick look at what it’s designed to do and the problem it’s trying to solve:

AsyncTask is designed to facilitate the thread scheduling of asynchronous tasks. It provides the following interfaces for users:

public abstract class AsyncTask<Params, Progress, Result> {
 		@WorkerThread
    protected abstract Result doInBackground(Params... params);
    
    @MainThread
    protected void onPreExecute() { } @MainThread protected void onPostExecute(Result result) { } @MainThread protected void onProgressUpdate(Progress... values) { } @MainThread protected void onCancelled(Result result) { onCancelled(); }}Copy the code

Start with its three generic parameters: Params represents the type of parameter passed to it, Progress represents the data type used to feedback Progress, and Result represents the Result type of asynchronous execution.

The user can implement the doInBackground method to write the processing to be done in the asynchronous thread, and override the onPostExecute method to get the result of the asynchronous request. In addition, the user can use onPreExecute to do some pre-processing before doInBackground, and can use onProgressUpdate to monitor progress. And onCancelled to realize the monitoring of mission interruption.

When we write an AsyncTask, we just need to call its execute method, and the process of thread scheduling will be done by it for us, which looks very nice.

As you can see from the above code, each method is annotated with an annotation like @mainThread, whose main purpose is to annotate the thread on which the method is running. As you can see, only doInBackground is executed on an asynchronous thread.

create

Next, let’s look at how it was created. We come to its constructor:

public AsyncTask() {
		this((Looper) null);
}
Copy the code

Its no-argument constructor is called to its parameterized constructor, which takes Looper as an argument:

public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
        ? getMainHandler()
        : new Handler(callbackLooper);
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
            mTaskInvoked.set(true);
            Result result = null;
            try {
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //noinspection unchecked
                result = doInBackground(mParams);
                Binder.flushPendingCommands();
            } catch (Throwable tr) {
                mCancelled.set(true);
                throw tr;
            } finally {
                postResult(result);
            }
            returnresult; }}; mFuture = new FutureTask<Result>(mWorker) { @Override protected voiddone() {
            try {
                postResultIfNotInvoked(get());
            } catch (InterruptedException e) {
                android.util.Log.w(LOG_TAG, e);
            } catch (ExecutionException e) {
                throw new RuntimeException("An error occurred while executing doInBackground()", e.getCause()); } catch (CancellationException e) { postResultIfNotInvoked(null); }}}; }Copy the code

First, it builds a Handler based on the Looper passed in. If no Looper is specified or the specified Looper is the main thread’s Looper, it specifies the built-in InternalHandler to process the message. Otherwise, construct a corresponding Handler. As you can see, AsyncTask does not always return to the main thread.

It then builds a WorkerRunnable, which is essentially a simple wrapper around a Callable, except that you can pass in parameters. When mWorker is executed, it first sets the current Task to be executed, then sets the thread priority, calls the doInBackground method and retrieves the return value. It appears that the WorkerRunnable is executed in an asynchronous thread. Whether it succeeds or not, it eventually calls postResult to deliver the result and return the result.

It then builds a FutureTask based on the previous mWorker and, when the execution is complete or cancelled, calls the postResultIfNotInvoked method to pass in the result of the mWorker’s execution.

There are two similar methods, postResult and postResultIfNotInvoked, but the reason for the design is described later.

In general, Handler, WorkerRunnable, and FutureTask are built.

perform

Now let’s see what it does when we call execute:

@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}
Copy the code

As you can see, it calls to the executeOnExecutor method and passes in an Executor object, sDefaultExecutor. Instead of focusing on the Executor design, let’s look at the executeOnExecutor method:

@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec
        Params... params) {
    if(mStatus ! = Status.PENDING) { switch (mStatus) {case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }
    mStatus = Status.RUNNING;
    onPreExecute();
    mWorker.mParams = params;
    exec.execute(mFuture);
    return this;
}
Copy the code

First, it checks the status of the current Task. The AsyncTask has three states: PENDING, RUNNING, and FINISHED, respectively. Only PENDING tasks can be executed.

After that, it first changes the state of the current Task and calls the onPreExecute method for user-implemented preprocessing. After that, it hands the user’s parameters to mWorker and the mFuture to the incoming thread pool for processing.

First, after FutureTask executes, the mWorker is executed, but it executes the mTaskInvoked true and calls postResult to deliver the result:

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}
Copy the code

The result is wrapped in the AsyncTaskResult class and placed on the message queue. This implements thread switching and sends the message from the child thread to the main thread via the Handler. When the Handler receives the message, The message is processed.

We can see how the default InternalHandler works if Looper is not specified at construction time:

private static class InternalHandler extends Handler {
    public InternalHandler(Looper looper) {
        super(looper);
    }
    @SuppressWarnings({"unchecked"."RawUseOfParameterizedType"}) @Override public void handleMessage(Message msg) { AsyncTaskResult<? > result = (AsyncTaskResult<? >) msg.obj; switch (msg.what) {case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break; }}}Copy the code

As you can see, in the MESSAGE_POST_RESULT message, it calls the Finish method after receiving AsyncTaskResult to complete the task:

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}
Copy the code

In the Finish method, the onCancelled method is called for a callback if the task has been cancelled, and the onPostExecute method is called for a callback if the task has completed properly, and its state is set to FINISHED.

But when the FutureTask execution is complete, the postResultIfNotInvoked method is invoked and the execution result of the mWorker is passed in:

private void postResultIfNotInvoked(Result result) {
    final boolean wasTaskInvoked = mTaskInvoked.get();
    if (!wasTaskInvoked) {
        postResult(result);
    }
}
Copy the code

As you can see, the Task is invoked only if the Task is not invoked, but because the mWorker first uses wasTaskInvoked as true when executed, the Task is invoked very rarely.

Let’s look at when onProgressUpdated is called. We can find out when the MESSAGE_POST_PROGRESS message was sent:

protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}
Copy the code

It is called in the publishProgress method, which is provided to the user, so progress updates need to be made by calling publishProgress in doInBackground.

cancel

Next, let’s look at the task cancellation, where we see the cancel method:

public final boolean cancel(boolean mayInterruptIfRunning) {
    mCancelled.set(true);
    return mFuture.cancel(mayInterruptIfRunning);
}
Copy the code

As you can see, the Cancel method ends up calling the mFuture’s Cancel, and it needs to pass mayInterruptIfRunning. When true, the task is interrupted even while running. If false, DoInBackground is still executed until the end.

This design is very strange, it is logical to cancel the scenario only while the task is executing, it is perfectly possible to provide a method with this parameter default true for external calls. This method can only end the task as quickly as possible. If there are some uninterruptible operations in the process, this method will not stop the task.

The thread pool

There is one last problem with AsyncTask that we have not explored, and that is the sDefaultExecutor that it performs the task:

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
Copy the code

You can see that the default value is SERIAL_EXECUTOR, which is an instance of SerialExecutor:

private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;
    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() { try { r.run(); } finally { scheduleNext(); }}});if (mActive == null) {
            scheduleNext();
        }
    }
    protected synchronized void scheduleNext() {
        if((mActive = mTasks.poll()) ! = null) { THREAD_POOL_EXECUTOR.execute(mActive); }}}Copy the code

As you can see, the Executor internally maintains an ArrayDeque queue, which is a task cache queue that holds runnables wrapped around the actual runnables to be executed.

When executed, when mActive is null, it calls the following scheduleNext method, which takes a task from the top of the mTasks queue and assigns it to mActive, which is then executed through the THREAD_POOL_EXECUTOR thread pool. When the task is complete, scheduleNext is called to execute the next task in the queue. With this design, the execution of Runnable becomes sequential, and the next task is executed only after the previous execution, so the execution order of AsyncTask is actually a serial execution.

Prior to Android 1.6, AsyncTasks were executed serially through a single background thread, but since Android 1.6, a thread pool has been introduced to support parallel execution.

Since this change caused concurrency problems in many applications, Android 3.0 changed it back to serial execution (introducing SerialExecutor) and supported parallel execution. But it still executes tasks serially by default, calling the executeOnExecutor method and passing THREAD_POOL_EXECUTOR if parallel execution is required.

What kind of thread pool is THREAD_POOL_EXECUTOR? Let’s take a look at its claim:

// We want at least 2 threads and at most 4 threads inthe core pool, // preferring to have 1 less than the CPU count to avoid saturating // the CPU with background work private static final  int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; private static final int KEEP_ALIVE_SECONDS = 30; private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
Copy the code

As you can see, the configuration of THREAD_OOL_EXECUTOR is as follows:

  • corePoolSize: Number of CPU cores -1, but keep at 2-4.
  • maxPoolSize: 2 x Number of CPU cores + 1
  • keepAliveSeconds30 seconds:
  • workQueue: blocking queue with capacity 128

Deficiency in

AsyncTask seems to be very beautiful, but in fact, there are a lot of shortcomings, which make it gradually withdraw from the historical stage:

  • Life cycle:AsyncTaskThere is no binding to the life cycle of an Activity or Fragment, even if the Activity is destroyeddoInBackgroundThe mission will still go ahead.
  • Cancel a task:AsyncTaskcancelMethod parametersmayInterruptIfRunningIt doesn’t make sense to exist, and it doesn’t guarantee that the task can be canceled, only as soon as possible (for example, the task will still run if some uninterruptible operation is in progress).
  • Memory leak: Because it is not bound to a life cycle such as an Activity, it can still have a longer life cycle than an Activity, and if it is a non-static inner class of an Activity, it will hold a reference to the Activity, causing the Activity’s memory to fail to be freed. (Similar to Handler’s memory leak problem)
  • Parallel/serial: due toAsyncTaskSerial and parallel execution has been modified on multiple versions, so when multipleAsyncTaskWhen executed sequentially, whether it is executed sequentially or in parallel depends on the version of the user’s phone. Specific modifications are as follows:
    • Android before 1.6: variousAsyncTaskExecute in serial order.
    • Before Android 3.0: The designers decided that serial execution was too inefficient, so they changed it to parallel execution, up to fiveAsyncTaskSimultaneous execution.
    • Since Android 3.0: Many applications have concurrency issues due to previous changes, so introducedSerialExecutorChanged back to serial execution, but supported parallel execution.

conclusion

Here, we can basically have a general understanding of the implementation principle of AsyncTask, its principle is actually quite simple: The Executor works with FutureTask to execute tasks asynchronously, and the Handler switches the threads after the task is complete to implement the whole thread scheduling function. By default, asyncTasks are executed sequentially in Android versions after 3.0.

The resources

Bad AsyncTask in Android