One, foreword

AsyncTask is a lightweight asynchronous task class that is familiar to every developer. At the same time, it has undergone many versions of adjustment, such as implementation from serial execution, to parallel execution, and then changed back to serial execution. AsyncTask has emerged from Android API3, the reason why it can stand for so many years, there must be a lot for us to learn, this article will be based on Android Q(10.0) source code for AsyncTask analysis.

Ii. Regarding Deprecated

When I was about to read the AsyncTask source code, I noticed in the AsyncTask official documentation that it has been marked out of date on Android R (11.0) and that Kotlin’s coroutines are recommended for asynchronous operations.

Google officially lists the following reasons for labeling AsyncTask obsolete, which is one of the things AsyncTask has always been criticized for:

  1. Prone to memory leaks
  2. Forget the callback
  3. Crash caused by vertical switching
  4. Compatibility issues between different versions of AsyncTask

Three, use,

If you haven’t used AsyncTask in a while, I thought it would be worth going over how to use AsyncTask first, if you are already familiar with it, you can skip this section.

AsyncTask is also very easy to use. Only two steps are required to complete the execution of asynchronous tasks, which is covered here.

Inherit the AsyncTask

class MyAsyncTask : AsyncTask<Void, Void, Void>() {
    /**
    *  任务执行前的操作,可以不重写,执行在UI线程
    **/
    override fun onPreExecute() {
        super.onPreExecute()
    }
    
    /**
    * 必须重写的方法,需要在异步线程执行的任务都在这里面执行,执行在子线程
    **/
    override fun doInBackground(vararg params: Void?): Void? {
        Log.i("MyAsyncTask", "doInBackground")
        return null
    }
    
    /**
    * doInBackground执行完会把result返回给这个方法,一般是做UI的更新,执行在UI线程
    **/
    override fun onPostExecute(result: Void?) {
        super.onPostExecute(result)
    }
}
Copy the code

Perform AsyncTask

// Execute AsyncTask when needed
val mTask = MyAsyncTask()
mTask.execute()
Copy the code

Serial or parallel?

I believe that you must know the order of AsyncTas execution, of course, TOO long version of the source code I did not see, I think I know a probably on the line, specific as follows:

  • Android1.6 before, is serial execution, implementation principle is a serial execution of tasks with a child thread.
  • Android1.6 to 2.3 is parallel execution, the implementation principle is to use a thread pool of 5 threads for parallel execution, but if the execution time of the first 5 tasks is too long, it will block the execution of the following tasks, so it is not suitable for concurrent execution of a large number of tasks.
  • After Android3.0, there is serial execution, using a global thread pool for serial processing tasks.

Of course, you may not be able to remember which versions execute. I don’t think that’s important, just remember that the current Version of Android uses serial execution, because the source code for Android Q (10.0) does exactly that.

1. How to verify serial execution?

For example, if three asynctasks are executed at the same time, each AsyncTask’s doInBackground method will output a Log after a delay of 2 seconds. Generally speaking, if executed in parallel, all three logs can be printed out in about 2 seconds (error of tens of milliseconds at most). On the other hand, if it is serial execution, the entire execution process takes about 6s. Now, let’s verify that the code is also very simple.

class MyAsyncTask(index: Int) : AsyncTask<Void, Void, Void>() { private var mIndex = index override fun onPreExecute() { super.onPreExecute() Log.i("MyAsyncTask", "AsyncTask$mIndex onPreExecute.") } override fun doInBackground(vararg params: Void?) : Void? { Thread.sleep(2000) Log.i("MyAsyncTask", "AsyncTask$mIndex doInBackground. thread=${Thread.currentThread().name}") return null } override fun onPostExecute(result: Void?) { super.onPostExecute(result) Log.i("MyAsyncTask", "AsyncTask$mIndex onPostExecute.") } } class MainActivity : AppCompatActivity() { private var mTask: MyAsyncTask? = null override fun onCreate(savedInstanceState: {super.onCreate(savedInstanceState) setContentView(r.layout.activity_main) // Loop for (I in 1.. 3) { mTask = MyAsyncTask(i) mTask? .execute() } } }Copy the code

Output result:

The 2020-03-18 11:51:15. 286, 18164-18164 / com. Ryan. Async_task_demo I/MyAsyncTask: AsyncTask1 onPreExecute. 2020-03-18 11:51:15.287 18164-18164/com.ryan. Async_task_demo I/MyAsyncTask: AsyncTask2 onPreExecute. 2020-03-18 11:51:15.287 18164-18164/com.ryan. Async_task_demo I/MyAsyncTask: AsyncTask3 onPreExecute. 2020-03-18 11:51:17.288 18164-18259/com.ryan. Async_task_demo I/MyAsyncTask: AsyncTask1 doinbackground-thread =AsyncTask #1 2020-03-18 11:51:17.290 18164-18164/com.ryan. Async_task_demo I/MyAsyncTask: AsyncTask1 onPostExecute. 2020-03-18 11:51:19.295 18164-18351/com.ryan. Async_task_demo I/MyAsyncTask: AsyncTask2 doinbackground-thread =AsyncTask #2 2020-03-18 11:51:19.296 18164-18164/com.ryan. AsyncTask2 task_demo I/MyAsyncTask: AsyncTask2 onPostExecute. 2020-03-18 11:51:21.299 18164-18352/com.ryan. Async_task_demo I/MyAsyncTask: AsyncTask3 doinBackground-thread =AsyncTask #3 2020-03-18 11:51:21.301 18164-18164/com.ryan. AsyncTask3 doinBackground-thread =AsyncTask #3 2020-03-18 11:51:21.301 18164-18164/com.ryan I/MyAsyncTask: AsyncTask3 onPostExecute.Copy the code

According to the result, the whole process takes about 6s, and the doInBackground of each Task is executed sequentially, which proves that AsyncTask executes asynchronous tasks sequentially by default on Android Q.

2. Can I change it to parallel execution?

Android knows that using the default implementation will not work for a wide variety of scenarios, so it exposes the executeOnExecutor interface and lets developers implement their own thread pool to execute asynchronous tasks, so we can specify AsyncTask thread pool to execute in parallel.

  public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params)
Copy the code

AsyncTask implements a thread pool with a core thread count of 1 and a maximum thread count of 20 and 3 seconds:

private static final int CORE_POOL_SIZE = 1;
private static final int MAXIMUM_POOL_SIZE = 20;
private static final int KEEP_ALIVE_SECONDS = 3;

public static final Executor THREAD_POOL_EXECUTOR;

static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), sThreadFactory);
    threadPoolExecutor.setRejectedExecutionHandler(sRunOnSerialPolicy);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
Copy the code

We can use this thread pool directly to execute AsyncTask in parallel:

for (i in 1.3) { mTask = MyAsyncTask(i) mTask? .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) }Copy the code

Five, the implementation principle

Before explaining how this works, LET me ask you a few questions: Why can AsyncTask be executed serally by default using a thread pool? Why does an AsyncTask become asynchronous when you manually set a thread pool? What does the internal implementation look like? If you still don’t know much, it’s easier to read with these questions in mind.

1. Serial execution principle/execution process principle

AsyncTask defines a static inner class SerialExecutor, which implements the Executor interface and exposes the Execute interface (synchronized). There is also an instance of sDefaultExecutor, which means that sDefaultExecutor has only one object in a process, that is, every AsyncTask shares sDefaultExecutor.

When multiple AsyncTasks call execute, the execute method of sDefaultExecutor will be triggered. Since this method is synchronous, the caller will hold the SerialExecutor lock. When another AsyncTask calls execute, Wait until the previous AsyncTask releases the lock before it can be called, thus realizing the serial execution of multiple AsyncTasks. The specific flow chart is as follows.

Combine the source code together to see its implementation:

(1). Thread pool for executing tasks

AsyncTask creates a thread pool within the AsyncTask. The core thread pool is 1 and the maximum thread pool is 20. This thread pool is ultimately used for asynchronous task execution.

The number of core threads is 1
private static final int CORE_POOL_SIZE = 1;
// Maximum number of threads 20
private static final int MAXIMUM_POOL_SIZE = 20;
// The idle thread lifetime is 3s
private static final int KEEP_ALIVE_SECONDS = 3;

public static final Executor THREAD_POOL_EXECUTOR;

static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), sThreadFactory);
    // Set the policy for the task to be rejected.
    threadPoolExecutor.setRejectedExecutionHandler(sRunOnSerialPolicy);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
Copy the code

(2). Executors that keep serial execution

The AsyncTask also creates a serial Executor, which is the static instance described in the above flow diagram. All asynctasks share the same Executor, and it is volatile to ensure that it is visible to multiple threads.

// Serial Executor
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
// Static instance
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
// SerialExecutor implements the Executor interface
private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    The execute method uses synchronized to ensure that multiple tasks are executed sequentially
    public synchronized void execute(final Runnable r) {
        // After competing for the lock, place it in the ArrayDeque
        mTasks.offer(new Runnable() {
            public void run(a) {
                try {
                    r.run();
                } finally{ scheduleNext(); }}});if (mActive == null) {
            // Trigger executionscheduleNext(); }}protected synchronized void scheduleNext(a) {
        if((mActive = mTasks.poll()) ! =null) {
            // When executing, you can see that
            // The thread pool THREAD_POOL_EXECUTOR is finally returned for processing
            // SERIAL_EXECUTOR serves only as an executor that guarantees serial executionTHREAD_POOL_EXECUTOR.execute(mActive); }}}Copy the code

(3). Trigger the execute method of SerialExecutor

When we call asynctask.execute () in the business layer, we call the Execute method of the asyncTask and assign the excute parameters to the mWorker (which is a Callable). The mFuture wraps the mWorker again. The reason for using FutureTask instead of a Runnable is that FutureTask gets results and Runnable doesn’t. Execute to SerialExecutor is finally triggered, and FutureTask is then plugged into thread pool THREAD_POOL_EXECUTOR.

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    // The AsyncTask executor is not specified
    // sDefaultExecutor is used to trigger serial execution
    return executeOnExecutor(sDefaultExecutor, params);
}

// The execution logic is handled in executeOnExecutor
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;
    // Call the abstract method onPreExecute
    onPreExecute();

    // Copy the argument to mWorker, which is a Callable
    mWorker.mParams = params;
    // mFuture is a FutrueTask, which is the vehicle to execute mWorker
    // Finally triggers the execute method of SerialExecutor in 2
    exec.execute(mFuture);

    return this;
}

Copy the code

2. Realize the principle of parallel execution

As we saw in Serial or Parallel, we can change an AsyncTask from a serial execution to a parallel execution by modifying its internal executor:

mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
Copy the code

ExecuteOnExecutor replaces the default sDefaultExecutor (synchronized) with THREAD_POOL_EXECUTOR (synchronized). THREAD_POOL_EXECUTOR does not change the execute method to synchronous, so it naturally executes asynchronously.

 public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) {... onPreExecute(); mWorker.mParams = params;// Use the passed Executor instead of sDefaultExecutor
    exec.execute(mFuture);

    return this;
}
Copy the code

3. Implementation principles of onProgressUpdate and onPostExecute

We all know that onProgressUpdate does progress updates, and onPostExecute is a method that calls back to the UI thread to update the UI after the child thread completes its task. So both of these methods are executed on the UI thread, so how do the child thread and the UI thread communicate?

Handler is a tool that implements thread communication in Android. The AsyncTask child thread is a Handler that obtains messages from the UI thread and sends them to the UI thread’s message list. The onProgressUpdate and onPostExecute callbacks are implemented.

(1) Initialize Handler

When AsyncTask is initialized, a UI thread Handler is generated.

public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
                ? getMainHandler()
                : new Handler(callbackLooper);
}
Copy the code

(2) Update progress

When actively called publishProgress is used, the message is pushed to the UI thread’s message queue through the UI thread’s Handler.

protected final void publishProgress(Progress... values) {
    if(! isCancelled()) { getHandler().obtainMessage(MESSAGE_POST_PROGRESS,new AsyncTaskResult<Progress>(this, values)).sendToTarget(); }}private static class InternalHandler extends Handler {
    public InternalHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) { AsyncTaskResult<? > result = (AsyncTaskResult<? >) msg.obj;switch (msg.what) {
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break; }}}Copy the code

(3) Execution results

In the execution principle, I mentioned that a task is a FutrueTask and its implementation is as follows. Once the task is executed, the done method is invoked and the postResultIfNotInvoked(Get ()) method is invoked. The method is to send the message back to the UI thread via a Handler, and then trigger finish on receiving the message, which calls back to onPostExecute.

mFuture = new FutureTask<Result>(mWorker) {
    @Override
    protected void done(a) {
        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); }}};private void postResultIfNotInvoked(Result result) {
    final boolean wasTaskInvoked = mTaskInvoked.get();
    if (!wasTaskInvoked) {
        postResult(result);
    }
}

/** * finally sends the event **/ through Handler
private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

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:
                // Call AsyncTask's Finish method
                result.mTask.finish(result.mData[0]);
                break; }}}private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        // Finally callback to onPostExecute
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}
Copy the code

4. Exception handling

The AsyncTask thread pool is THREAD_POOL_EXECUTOR, which is created with a sRunOnSerialPolicy. If THREAD_POOL_EXECUTOR refuses to execute a task, The processing logic is then dispatched to the rejectedExecution method in sRunOnSerialPolicy, where AsyncTask obtains (or creates) an alternate thread pool with a core of five threads. The standby thread pool is then asked to perform the rejected task.

private static final int BACKUP_POOL_SIZE = 5;

private static ThreadPoolExecutor sBackupExecutor;
private static LinkedBlockingQueue<Runnable> sBackupExecutorQueue;

private static final RejectedExecutionHandler sRunOnSerialPolicy = new RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        // If the task is rejected, it is called back to this method
        synchronized (this) {
            // An alternate thread pool, sBackupExecutor, is triggered for task processing
            if (sBackupExecutor == null) {
                sBackupExecutorQueue = new LinkedBlockingQueue<Runnable>();
                sBackupExecutor = new ThreadPoolExecutor(
                        BACKUP_POOL_SIZE, BACKUP_POOL_SIZE, KEEP_ALIVE_SECONDS,
                        TimeUnit.SECONDS, sBackupExecutorQueue, sThreadFactory);
                sBackupExecutor.allowCoreThreadTimeOut(true); } } sBackupExecutor.execute(r); }};Copy the code

Six, summarized

AsyncTask uses two “thread” pools, one of which is a pseudo-thread pool. It implements the Executor interface and synchronizes the execute method. The other thread pool is the pool of threads that actually perform the task.

In addition, AsyncTask is designed to execute tasks in parallel by modifying its thread pool.

In terms of thread switching, AsyncTask communicates with the sub-thread and UI thread through Handler, so as to realize progress changes and UI updates.

In addition, AsyncTask sets up an alternate thread pool so that tasks in the queue can continue to execute if the pool of threads that normally execute the task refuses to execute the task.

Finally, AsyncTask has been Deprecated in Android R (11.0), which is more or less outdated in today’s era of asynchronous operation frameworks. However, we should not forget its past glory. A generation may grow old, but some people are always young. It has fulfilled its sense of mission well.