Android development art exploration Chapter 11 - Android threads and thread pools

1. Preface

This paper mainly summarizes the threads and thread pools of Android.

2. Text

2.1 main thread and sub thread

2.1.1 what classes can play the role of thread in Android?

Thread, AsyncTask, HandlerThread and IntentService. In Jetpack, a new WorkManager is added.

classdescribepurpose
AysncTaskEncapsulates the thread pool and HandlerTo facilitate developers to handle asynchronous tasks and update data in the UI thread.
HandlerThreadIs a thread with message loop, which uses Handler insideIt is convenient to build threads with message loops.
IntentServiceIs a service that internally uses HandlerThread to execute asynchronous tasks. When the task is completed, IntentService will exit automatically.It can easily execute asynchronous tasks, combining threads (asynchronous) and services (not easy to be killed by the system).

2.1.2 what are the benefits of thread pooling?

Thread features:

  • Thread is a kind of limited system resource, that is, thread cannot be generated without limitation;

  • The creation and destruction of threads will have corresponding overhead;

  • Thread scheduling is carried out by the system through time slice rotation, which also has a certain overhead. When there are many threads, the overhead is greater.

Thread pool can cache a certain number of threads to avoid the system overhead caused by frequent creation and destruction of threads.

  • Reuse the threads in the thread pool to avoid the performance overhead caused by the creation and destruction of threads;
  • It can effectively control the maximum concurrent number of thread pool and avoid the blocking phenomenon caused by a large number of threads preempting system resources;
  • It can simply manage threads, and provide timed execution and specified interval cyclic execution.

2.1.3 what are the responsibilities of main thread and sub thread in Android?

The main thread is mainly responsible for the logic of interface interaction. It must ensure a high response speed at any time, otherwise the interface will feel stuck, and the main thread cannot handle time-consuming tasks;

Sub threads are also called work threads. All threads except the main thread are sub threads, which are mainly responsible for processing time-consuming operations.

If time-consuming operations are performed in the main thread, such as network access, from Android 3 0 will fail to start network access and throw NetworkOnMainThreadException; For I/O operations or other time-consuming operations, the main thread will exceed the response time limit, resulting in ANR phenomenon.

2.2 thread form in Android

2.2.1 how to use asynctask in actual development?

public class AsyncTaskActivity extends Activity {
    private static final String TAG = AsyncTaskActivity.class.getSimpleName();
    private static final String url = "https://github.com/jhwsx/android-art-research/raw/"
            + "f6257e9f1e46848400f7ff2635991fd5a850d4f8/chapter_11/app-debug.apk";
    TextView mTvProgress;
    TextView mTvLength;
    private DownloadTask mDownloadTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_asynctask);
        Button btnStartDownload = (Button) findViewById(R.id.btn_start_download);
        Button btnCancelDownload = (Button) findViewById(R.id.btn_cancel_download);
        mTvProgress = (TextView) findViewById(R.id.tv_download_progress);
        mTvLength = (TextView) findViewById(R.id.tv_download_length);
        btnStartDownload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDownloadTask = new DownloadTask(AsyncTaskActivity.this);
                mDownloadTask.execute(url);
            }
        });
        btnCancelDownload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDownloadTask.cancel(false);
            }
        });
    }
    public static class DownloadTask extends AsyncTask<String, Integer, Long> {
        private WeakReference<AsyncTaskActivity> weakReference;
        private ProgressDialog mProgressDialog;

        public DownloadTask(AsyncTaskActivity activity) {
            weakReference = new WeakReference<>(activity);
        }
        @Override
        protected void onPreExecute() {
            super.onPreExecute();

            mProgressDialog = new ProgressDialog(weakReference.get());
            mProgressDialog.setTitle("download");
            mProgressDialog.setMessage("Get data...");
            mProgressDialog.show();
            Log.d(TAG, "onPreExecute: threadName=" + Thread.currentThread().getName());
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            if (mProgressDialog != null && mProgressDialog.isShowing()) {
                mProgressDialog.dismiss();
                mProgressDialog = null;
            }
            Log.d(TAG, "onProgressUpdate: progress=" + values[0] + ", threadName=" + Thread.currentThread().getName());
            AsyncTaskActivity asyncTaskActivity = weakReference.get();
            if (asyncTaskActivity != null) {
                asyncTaskActivity.mTvProgress.setText("Download progress: " + values[0] + "%");
            }

        }

        @Override
        protected Long doInBackground(String... urls) {
            Log.d(TAG, "doInBackground: threadName=" + Thread.currentThread().getName());
            HttpURLConnection connection = null;
            InputStream inputStream = null;
            OutputStream outputStream = null;
            long contentLength = -1;
            try {
                URL url = new URL(urls[0]);
                connection = (HttpURLConnection) url.openConnection();
                connection.setConnectTimeout(30 * 1000);
                connection.setReadTimeout(30 * 1000);
                connection.connect();
                if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                    return -1L;
                }
                contentLength = connection.getContentLength();
                inputStream = connection.getInputStream();
                outputStream = new FileOutputStream(new File(weakReference.get().getExternalFilesDir(null), "download.apk"));
                byte[] buffer = new byte[4 * 1024];
                int total = 0;
                int length;
                while ((length = inputStream.read(buffer)) != -1) {
                    total += length;
                    final int progress = (int) (total * 100f / contentLength);
                    if (isCancelled()) {
                        break;
                    }
                    publishProgress(progress);
                    outputStream.write(buffer, 0, length);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                CloseUtils.closeIOQuietly(inputStream, outputStream);
                if (connection != null) {
                    connection.disconnect();
                }
            }
            return contentLength;
        }

        @Override
        protected void onPostExecute(Long aLong) {
            super.onPostExecute(aLong);
            Log.d(TAG, "onPostExecute: threadName=" + Thread.currentThread().getName());
            AsyncTaskActivity asyncTaskActivity = weakReference.get();
            if (asyncTaskActivity != null) {
                asyncTaskActivity.mTvLength.setText("Bytes downloaded: " + aLong);
                asyncTaskActivity.mTvLength.setVisibility(View.VISIBLE);
            }
        }

        @Override
        protected void onCancelled() {
            super.onCancelled();
            Log.d(TAG, "onCancelled: " + Thread.currentThread().getName());
            AsyncTaskActivity asyncTaskActivity = weakReference.get();
            if (asyncTaskActivity != null) {
                asyncTaskActivity.mTvLength.setVisibility(View.VISIBLE);
                asyncTaskActivity.mTvLength.setText("Task cancelled");
            }
        }
    }
}

Click the start download button to print the log as follows:

D/AsyncTaskActivity: onPreExecute: threadName=main
D/AsyncTaskActivity: doInBackground: threadName=AsyncTask #1
D/AsyncTaskActivity: onProgressUpdate: progress=0, threadName=main
D/AsyncTaskActivity: onProgressUpdate: progress=1, threadName=main
D/AsyncTaskActivity: onProgressUpdate: progress=2, threadName=main
...
D/AsyncTaskActivity: onProgressUpdate: progress=98, threadName=main
D/AsyncTaskActivity: onProgressUpdate: progress=100, threadName=main
D/AsyncTaskActivity: onPostExecute: threadName=main

2.2.2 what are the two operation methods, three generic parameters and five core methods of asynctask?

AsyncTask is an abstract generic class that provides three generic parameters: Params, Progress, and Result. Where Params represents the type of input parameters of asynchronous task, such as download link, Progress represents the type of execution Progress of asynchronous task, such as the percentage of Progress, and Result represents the type of return Result of asynchronous task, such as the length of download file. If you really don't need a specific parameter, you can pass in Void instead.

Five core methods:

methodExecution threadeffect
onPreExecute()Main threadThis method is called back before the asynchronous task is executed. Generally, some preparations can be made.
Result doInBackground(Params... params)Thread poolIt is used to execute asynchronous tasks. The params parameter represents the input parameter of asynchronous tasks, and the return value represents the result of onPostExecute method to be returned. In addition, in this method, the progress of asynchronous tasks is updated through the publishProgress(Progress... values) method, and finally the onProgressUpdate method will be called.
onProgressUpdate(Progress... values)Main threadThis method will be called back when the execution progress of asynchronous tasks changes.
onPostExecute(Result result)Main threadAfter the asynchronous task is executed, this method will be called back to receive the return value of doInBackground.
onCancelled(Result result)Main threadWhen the asynchronous task is cancelled, this method will be called back, and onPostExecute will not be called back at this time.

Two operation methods:

methodCalling threadeffect
AsyncTask<Params, Progress, Result> execute(Params... params)Main threadStart asynchronous task
boolean cancel(boolean mayInterruptIfRunning)No restrictionsCancel execution of asynchronous task

2.2.3 what are the restrictions on the use of asynctask?

  1. AsyncTask's class must be loaded in the main thread. This is mainly to ensure that the creation of sHandler is in the main thread. AsyncTask after Android 22 has changed;
  2. The execute() method is called in the main thread, which is mainly to ensure that the onPreExecute method is called in the main thread;
  3. Do not directly call onPreExecute(), onPostExecute(), doInBackground and onProgressUpdate methods in the program;
  4. An AsyncTask object can only call the execute method once, and multiple calls will report runtime exceptions;
  5. On Android 1 Before 6, AsyncTask executed tasks serially, Android 1 6 to Android 2 3. AsyncTask uses thread pool to process parallel tasks, Android3 Starting from 0, AsyncTask uses a thread to execute tasks serially.

The reasons for these limitations will be explained one by one later in this article.

2.2.4 where is the class loading of asynctask completed? What will be done?

The class of asynctask is loaded in Android 4.0 1 and above have been automatically completed by the system, specifically in ActivityThread's main method, calling AsyncTask's init method to complete.

public static void main(String[] args) {
    // Creates the Looper object of the main message loop
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    // Initialize AsyncTask
    AsyncTask.init();
    // Start main message loop
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

We know that static members are initialized when loading classes, so we should also pay attention to which static members of AsyncTask are initialized.

public abstract class AsyncTask<Params, Progress, Result> {
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE = 1;
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };
    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
    private static final InternalHandler sHandler = new InternalHandler();
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    public static void init() {
        sHandler.getLooper();
    }
    private static class InternalHandler extends Handler {
    }

}

As you can see, two thread pools (THREAD_POOL_EXECUTOR and SERIAL_EXECUTOR) and a handler handler are initialized.

It should be noted that the object of sHander is created in the main thread, so that the data can be switched from the working thread to the main thread through sHander in the working thread.

2.2.5 how does asynctask work?

Construction of AsyncTask

public abstract class AsyncTask<Params, Progress, Result> {

    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;    
    public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);

                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                return postResult(doInBackground(mParams));
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("", e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }
}

There are not many FutureTask classes in development. Here is the corresponding class diagram:

It can be seen that FutureTask implements the RunnableFuture interface, and the RunnableFuture interface inherits the Runnable interface, so FutureTask also implements the Runnable interface, so that FutureTask objects can be placed in the process pool for execution.

When the task encapsulated by FutureTask is executed, the thread pool will call its run method, and the call method of Callable object will be called inside the run method.

Here, the mFuture object will be put into the thread pool for execution. The thread pool will call its run method. The call method of the mWorker object will be called inside the run method to execute asynchronous tasks in the call method.

Here is the inclusion diagram between them for easy understanding:

Asynchronous task submission

Call the execute method to start executing the task, and the executeOnExecutor method will be called internally:

public abstract class AsyncTask<Params, Progress, Result> {

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

	public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
			Params... params) {
        // If the task state is not PENDING, an exception will be thrown.
		if (mStatus != Status.PENDING) {
			switch (mStatus) {
				case RUNNING:
					throw new IllegalStateException("");
				case FINISHED:
					throw new IllegalStateException("");
			}
		}
        // The current task status is PENDING. Update the task status to RUNNING.
		mStatus = Status.RUNNING;
        // Call this method to make some preparations before executing the task.
		onPreExecute();
        // Assign the parameter array to the member variable mparms of mWorker
		mWorker.mParams = params;
        // Submit asynchronous tasks to the thread pool
		exec.execute(mFuture);
        // Return the AsyncTask object so that the external can get it to cancel, publish the progress, and judge whether to cancel.
		return this;
	}
	
	public enum Status {
		/**
		 * The task has not been carried out yet
		 */
		PENDING,
		/**
		 * Task is running
		 */
		RUNNING,
		/**
		 * The task is over, including the execution is completed or cancelled
		 */
		FINISHED,
	}
}

Through the control of mStatus, it is ensured that an AsyncTask object can only call the execute method once, otherwise it will throw a running exception.

Calling the execute method actually submits the asynchronous task to the default thread pool for execution, which is a serial thread pool. So, how to ensure the serial? You can see the following source code:

// A pool of threads that can execute tasks in parallel
public static final Executor THREAD_POOL_EXECUTOR
        = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
// Execute one task at a time, that is, the thread pool for serial execution of tasks
// It should be noted that SERIAL_EXECUTOR is unique to the process.
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private static class SerialExecutor implements Executor {
    // Used to store asynchronous tasks to be executed, because SERIAL_EXECUTOR is unique to the process, so mTask is also unique to the process.
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    // Record whether there are asynchronous tasks in progress
    Runnable mActive;
    // Add built-in lock to ensure thread safety
    public synchronized void execute(final Runnable r) {
        // When new tasks arrive, they are added to the end of the queue first.
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    // When a task is completed, the next task in the queue is scheduled to be executed.
                    scheduleNext();
                }
            }
        });
        // If there is no asynchronous task being executed, the task is taken from the queue for execution.
        if (mActive == null) {
            scheduleNext();
        }
    }
    protected synchronized void scheduleNext() {
        // Take a task from the head of the queue and assign it to active. If it is not empty, it will be put into the thread pool for execution.
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

Because SERIAL_EXECUTOR is unique to the process, and serial is used by default_ Executor is used as the thread pool, so all asynctasks in a process will be queued for execution in this serial thread pool.

The SerialExecutor class is not unique in AsyncTask. In fact, it comes from the Executor document in Java. It changes the parallel mode of task scheduling to serial mode by using ArrayDeque array and double ended queue.

This can be regarded as the application of static proxy mode. Executor is a public interface, SerialExecutor is a proxy class, and ThreadPoolExecutor is a real entity, that is, the proxied class.

It should be noted that exec execute(mFuture); Just submitting the asynchronous task to the thread pool does not mean that the asynchronous task will be executed immediately. The annotation of this method can prove this: execute the given task some in the future

Execution of asynchronous tasks

We know that the submission of asynchronous tasks has been completed. Where is the execution of asynchronous tasks?

Of course, the execution of asynchronous tasks is in the thread pool. Specifically, when executing asynchronous tasks in the runWorker of ThreadPoolExecutor, the run method of mFutureTask object (which is a Runnable object) will be called internally, and the call method of mWorker object (which is a Callable object) will be called internally, Because we know that the call method will eventually be executed in the thread pool.

private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
mWorker = new WorkerRunnable<Params, Result>() {
    public Result call() throws Exception {
        mTaskInvoked.set(true);
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        return postResult(doInBackground(mParams));
    }
};

protected abstract Result doInBackground(Params... params);

In the call method, three main things are done:

  • Set the value of the Boolean member variable of the atom mTaskInvoked to true;
  • Execute the doInBackground(Params... params) of AsyncTask. This method receives the input parameters of asynchronous task (such as download link) and executes the code of asynchronous task (such as download file);
  • Pass the return value of the doInBackground method (such as the size of the downloaded file) to the postResult method.

Progress and results of asynchronous tasks

The main thread needs to get the progress (such as the percentage of downloaded files) and results (such as file size or cancelled) of the asynchronous task, so as to display the download progress and results in the UI. However, asynchronous tasks are executed in the thread pool, and its progress and results are currently in the sub thread.

So, how to switch the progress and result data of the sub thread to the main thread? Of course, the Handler is needed.

First look at how to switch the progress of asynchronous tasks in the child thread to the main thread, and call the publishProgress method in the doInBackground method.

// The sHandler is initialized when the AsyncTask class is loaded, and the AsyncTask class is loaded in the main thread, so the sHandler is created in the main thread.
private static final InternalHandler sHandler = new InternalHandler();
protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}

It can be seen that when the isCancelled() method is not true, that is, when the asynchronous task is not cancelled, first encapsulate the AsyncTask object and progress information in the AsyncTaskResult object, then encapsulate the AsyncTaskResult object in the Message object, and finally send the Message object to the Message queue through the sHandler.

private static class AsyncTaskResult<Data> {
    final AsyncTask mTask;
    final Data[] mData;
    AsyncTaskResult(AsyncTask task, Data... data) {
        mTask = task;
        mData = data;
    }
}

In fact, the name AsyncTaskResult is used here to represent the progress information, which is very inaccurate. I think it's appropriate to call asynctaskyprogress, right?

Next, let's take a look at the place where the message is received. Naturally, it is in the handleMessage method overridden by the InternalHandler class:

private static class InternalHandler extends Handler {
    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        // This method is executed in the main thread.
        AsyncTaskResult result = (AsyncTaskResult) msg.obj;
        switch (msg.what) {
            ...
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

result.mTask is the current AsyncTask object, result MDATA is the progress value to be updated. It is an array.

result.mTask.onProgressUpdate(result.mData); This is to call the onprogressupdate method of AsyncTask and call back the progress value, so that you can get the progress value in the onprogressupdate method of the rewritten AsyncTask to update the progress value on the page.

The results of asynchronous tasks are sent through the postResult method:

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

In AsyncTask, the postResult method will be called in two places:

  • After the execution of the asynchronous task code is completed, after the execution of the doInBackground method is completed, the postResult method is called to send the result:

    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
            mTaskInvoked.set(true);
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            return postResult(doInBackground(mParams));
        }
    };
    
  • After calling the cancel method of AsyncTask, the done method of FutureTask will be called finally:

    public final boolean cancel(boolean mayInterruptIfRunning) {
        // Set the value of mCancelled to true to indicate that the cancellation operation has been performed
        mCancelled.set(true);
        return mFuture.cancel(mayInterruptIfRunning);
    }
    mFuture = new FutureTask<Result>(mWorker) {
        @Override
        protected void done() {        
            postResultIfNotInvoked(get());
        }
    };
    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        // If the task has not been called, send the result.
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }
    

Then look at the postResult method. It still encapsulates the AsyncTask object and result information in the AsyncTask result object, then encapsulates the AsyncTask result object in the Message object, and finally sends the Message object to the Message queue through the sHandler.

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

The result is received in the handleMessage method of InternalHandler:

case MESSAGE_POST_RESULT:
    // Called in the main thread
    result.mTask.finish(result.mData[0]);
    break;
private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

You can see that in the finish method, you will first judge whether to cancel the operation through isCancelled; If it has been cancelled, the onCancelled method will be called. If it has not been cancelled, it means that the result of the asynchronous task is obtained, and the onPostExecute method will be called; Finally, change the value of mStatus to FINISHED.

From here, we can conclude that for an AsyncTask object, only one of its onCancelled method and onPostExecute method will be called; An AsyncTask object cannot call its execute method again after executing an asynchronous task because its mStatus is FINISHED instead of PENDING. Calling the execute method again will throw a running exception.

2.2.6 how does asynctask memory leak occur? How to solve it?

When AsyncTask is created by anonymous class or internal class in Activity, AsyncTask will hold the reference of external class Activity by default; If the asynchronous task of AsyncTask has not been completed after the Activity is closed, the AsyncTask object will not be released, and the Activity object it holds will not be released, resulting in the failure of timely recycling of the Activity object, which will lead to memory leakage.

Solution: use the static inner class to subclass AsyncTask. If the Activity is to be used inside the subclass, use the weak reference method to reference the instance of Activity; When the Activity ends, cancel the AsyncTask task in time.

2.2.7 how to make AsyncTask execute tasks in parallel?

We know that AsyncTask executes tasks serially by default. Is there any way to make it execute tasks in parallel? yes , we have.

Use the executeOnExecutor method of AsyncTask to specify the thread pool from the outside and avoid using the internal default SERIAL_EXECUTOR serial thread pool.

public void parallel_execute(View view) {
    new MyAsyncTask("AsyncTask#1").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
    new MyAsyncTask("AsyncTask#2").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
    new MyAsyncTask("AsyncTask#3").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
    new MyAsyncTask("AsyncTask#4").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "");
}

Print log:

D/AsyncTaskActivity: AsyncTask#2 finished at 2022-01-29 15:27:28
D/AsyncTaskActivity: AsyncTask#3 finished at 2022-01-29 15:27:28
D/AsyncTaskActivity: AsyncTask#1 finished at 2022-01-29 15:27:28
D/AsyncTaskActivity: AsyncTask#4 finished at 2022-01-29 15:27:28

As you can see, parallel execution is indeed carried out.

2.2.8 what is the working process of handlerthread?

HandlerThread is used to create a new thread with a Looper object. Developers can use the Looper object of HandlerThread to create a Handler object. Note that you need to call the start() method to start HandlerThread.

The source code of HandlerThread is divided into three parts: creating Looper, obtaining Looper and terminating Looper.

Create Looper

After calling the start method, the run method is executed:

@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

Through Looper Prepare () method to create a Looper object corresponding to the thread;

Enter the synchronization code block through Looper Mylooper() gets the Looper object of this thread, assigns it to the member variable mLooper of HandlerThread, and calls notifyAll() to wake up other threads waiting for locks;

Call onLooperPrepared method to indicate that HandlerThread has prepared the Looper object;

Call looper The loop () method opens the message loop.

Get Looper

public Looper getLooper() {
    if (!isAlive()) {
        return null;
    }
    // If the HandlerThread thread has been started, the Looper object will wait until it is created.
    synchronized (this) {
        while (isAlive() && mLooper == null) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
    }
    return mLooper;
}

First judge whether the HandlerThread thread thread object survives. If it does not survive, it will directly return null;

Then enter the synchronization code block, execute the wait() operation, and wrap it with while. The loop condition is that the thread survives and mloop is null. In other words, if the thread dies or mloop is not null, it will not be waiting.

When will there be such a scene? Here is a scenario:

static class MyHandlerThread extends HandlerThread {
    public MyHandlerThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        // Deliberately sleep for 3s before executing the run method of the parent class
        SystemClock.sleep(3000L);
        super.run();
    }
}
public void handlerThread(View view) {
    final HandlerThread handlerThread = new MyHandlerThread("WorkThread");
    handlerThread.start();
    // Start Thread1 and get the looper object of handlerThread in its run method.
    new Thread(new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "run: threadName=" + Thread.currentThread().getName());
            Looper looper = handlerThread.getLooper();
            Log.d(TAG, "run: threadName=" + Thread.currentThread().getName() + ",looper=" + looper);
        }
    }, "Thread1").start();
}

Print log:

2022-01-29 08:30:21.650 D/MainActivity: run: threadName=Thread1
2022-01-29 08:30:24.653 D/MainActivity: run: threadName=Thread1,looper=Looper (WorkThread, tid 198) {2b5639a7}

When HandlerThread is called in the run method of Thread1 When getLooper(), the run method of HandlerThread class has not started to execute, so in getLooper method, Thread1 will get the lock object and enter the synchronization code block: isalive() & & mloop = = null condition is true, call Thread1's wait() method, Thread1 will enter the waiting state and release the lock object.

After 3s, the run method of HandlerThread is executed to obtain the lock object, enter the synchronization code block, assign the created Looper object to the mloop member variable, and call notifyAll() to wake up the waiting thread with the lock, which also wakes up Thread1.

After Thread1 is awakened, judge again the condition of isalive() & & mloop = = null. At this time, mloop is not null, so the loop condition is not tenable. Exit the synchronization code block and directly return to the mloop object.

Terminate Looper

Call the quit method or the quitsafe method of HandlerThread to terminate Looper. Loop's quit and quitsafe methods are called internally. This is to terminate the message loop when the message loop is not used, so as to avoid the problem of memory leakage.

Benefits of using HandlerThread:

  • The Looper object can be obtained safely and conveniently;
  • Looper can be easily terminated.

In the Android system source code, the HandlerThread is used by ActivityManagerService, PackageManagerService and PowerManagerService. Its main function is to send data or tasks from the main thread to the working thread for execution.

2.2.9 what is the difference between intentservice and Service?

Contact: IntentService inherits from Service.

difference:

  • IntentService is an abstract class that contains an abstract method onHandleIntent. Therefore, its subclass must be created to create IntentService, and Service is a concrete class.
  • IntentService can execute time-consuming tasks asynchronously. When the task is completed, it will stop automatically; The Service runs on the main thread and cannot directly execute time-consuming tasks. You must open the working thread to execute time-consuming tasks.

2.2.10 what is the working process of intentservice?

Subclass IntentService

Because IntentService is an abstract class containing the abstract method onHandleIntent, subclasses need to implement the abstract method onHandleIntent method of IntentService;

Because IntentService defines a constructor with parameters, there is no longer a constructor without parameters, so subclasses need to explicitly call the constructor with parameters of the parent class.

public class MyIntentService extends IntentService {
    private static final String TAG = MyIntentService.class.getSimpleName();

    public MyIntentService() {
        super(TAG);
    }
    @Override
	protected void onHandleIntent(Intent intent) {
    }
}
public abstract class IntentService extends Service {
    private String mName;
    public IntentService(String name) {
        super();
        mName = name;
    }
}

The name passed to the parent class constructor will be used to create the Thread name of the HandlerThread object (don't forget that HandlerThread inherits from the Thread class).

Callback onCreate() method of IntentService

private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
@Override
public void onCreate() {
    super.onCreate();
    HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
    thread.start();
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);
}
private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
        super(looper);
    }
    @Override
    public void handleMessage(Message msg) {
        onHandleIntent((Intent)msg.obj);
        stopSelf(msg.arg1);
    }
}

As you can see, in the onCreate method:

  • Create and start a HandlerThread type thread to obtain a Looper object corresponding to this thread;
  • Obtain the Looper object through the HandlerThread object and assign it to the mserviceloop member variable;
  • Use mServiceLooper to create a ServiceHandler object and assign it to the mServiceHandler member variable.

It should be noted that ServiceHandler is an internal class of IntentService, not a static internal class, because the stopSelf(int startId) method of Service object needs to be called in handleMessage. Internal classes hold references to external classes by default, while static internal classes do not hold references to external classes.

Callback onStartCommand() method of IntentService

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    onStart(intent, startId);
    return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

@Override
public void onStart(Intent intent, int startId) {
    // Send message in main thread
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent;
    mServiceHandler.sendMessage(msg);
}

private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
        super(looper);
    }
    @Override
    public void handleMessage(Message msg) {
        // Call in sub thread
        onHandleIntent((Intent)msg.obj);
        stopSelf(msg.arg1);
    }
}

You should know that every time you call the startService method, the onStartCommand method of the corresponding Service will be called once.

The onStart method is called internally for compatibility processing: onStart is an obsolete method and onStartCommand is a new method to replace it; By putting the main code into the onStart method, you can take into account the old and new versions.

It should be noted that the life cycle methods of Service are called in the main thread. Therefore, looking at the code inside the onStart method, you can use mServiceHandler in the main thread to send a message (encapsulating startId and intent). So where is the place to receive the message? In the handleMessage method of ServiceHandler, this method is called back in the child thread. In this way, the message (encapsulated with startId and intent) is switched from the main thread to the child thread.

In the handleMessage method, get obj through the Message object, that is, the Intent object, and get the startId. The content of the Intent object here is completely consistent with the content of the Intent object in startService(Intent service). Therefore, in the onHandleIntent method, the parameters passed when starting the Service, such as download link and task id, can be resolved through the Intent object to distinguish specific asynchronous tasks.

After the onHandleIntent method is executed, it will then execute the stopSelf(int startId) method, which is only used to try to stop the service, that is, when the conditions are met, the service will be really stopped, and when the conditions are not met, the service will not be stopped. So, what is the condition? When the number of times the service was recently started is equal to the startId, the service will be stopped immediately; If it is not equal, the service will not be stopped.

2.2.11 how does intentservice ensure the sequential execution of multiple asynchronous tasks?

When the startService method is called many times, the IntentService will execute different asynchronous tasks each time. The IntentService will send the tasks to the message queue held by the sub thread through the Handler, and the Looper object corresponding to the sub thread will process the messages in sequence, that is, after an asynchronous task is processed through the onHandleIntent method, The next message (encapsulating the parameters of the asynchronous task) will be taken from the message queue, and then the corresponding asynchronous task will be processed through the onHandleIntent method. This ensures that when multiple asynchronous tasks are sent to IntentService, the execution of these asynchronous tasks will be executed one by one in the order of sending, without crossing or parallel.

Here is a code example to illustrate:

public class MyIntentService extends IntentService {
    private static final String TAG = MyIntentService.class.getSimpleName();
    public static final String EXTRA_TASK = "extra_task";

    public MyIntentService() {
        super(TAG);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: ");
    }

    @Override
    public void onStart(Intent intent, int startId) {
        Log.d(TAG, "onStart: startId = " + startId);
        super.onStart(intent, startId);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand startId:" + startId);
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        String task = intent.getStringExtra(EXTRA_TASK);
        Log.d(TAG, "onHandleIntent: " + task + " start");
        long time;
        switch(task) {
            case "task 0":
                time = 4000; break;
            case "task 1":
                time = 2000; break;
            case "task 2":
                time = 5000; break;
            case "task 3":
                time = 1000; break;
            case "task 4":
                time = 3000; break;
            default:
                time = 0; break;
        }
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.d(TAG, "onHandleIntent: " + task + " finish");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy: ");
    }
}

In MainActivity, click the button to send the task:

public void intentservice(View view) {
    Intent service = new Intent(MainActivity.this, MyIntentService.class);
    for (int i = 0; i < 5; i++) {
        service.putExtra(MyIntentService.EXTRA_TASK, "task " + i);
        Log.d(TAG, "startService: task " + i);
        startService(service);
    }
}

Print the log as follows:

 14:17:51.013  D/MainActivity: startService: task 0
 14:17:51.017  D/MainActivity: startService: task 1
 14:17:51.020  D/MainActivity: startService: task 2
 14:17:51.025  D/MainActivity: startService: task 3
 14:17:51.027  D/MainActivity: startService: task 4
 14:17:51.037  D/MyIntentService: onCreate: 
 14:17:51.037  D/MyIntentService: onStartCommand startId:1
 14:17:51.037  D/MyIntentService: onStart: startId = 1
 14:17:51.038  D/MyIntentService: onHandleIntent: task 0 start
 14:17:51.041  D/MyIntentService: onStartCommand startId:2
 14:17:51.041  D/MyIntentService: onStart: startId = 2
 14:17:51.042  D/MyIntentService: onStartCommand startId:3
 14:17:51.042  D/MyIntentService: onStart: startId = 3
 14:17:51.042  D/MyIntentService: onStartCommand startId:4
 14:17:51.042  D/MyIntentService: onStart: startId = 4
 14:17:51.043  D/MyIntentService: onStartCommand startId:5
 14:17:51.043  D/MyIntentService: onStart: startId = 5
 14:17:55.041  D/MyIntentService: onHandleIntent: task 0 finish
 14:17:55.043  D/MyIntentService: onHandleIntent: task 1 start
 14:17:57.044  D/MyIntentService: onHandleIntent: task 1 finish
 14:17:57.046  D/MyIntentService: onHandleIntent: task 2 start
 14:18:02.049  D/MyIntentService: onHandleIntent: task 2 finish
 14:18:02.050  D/MyIntentService: onHandleIntent: task 3 start
 14:18:03.053  D/MyIntentService: onHandleIntent: task 3 finish
 14:18:03.055  D/MyIntentService: onHandleIntent: task 4 start
 14:18:06.058  D/MyIntentService: onHandleIntent: task 4 finish
 14:18:06.064  D/MyIntentService: onDestroy: 

You can see that the multi line logs at 14:17:51 are printed quickly, which is mainly related to the sending task.

The following log is the printing of tasks one by one.

Task nameThe time (in milliseconds) that the task needs to executeTask execution start timeTask execution end time
task 0400014:17:51.03814:17:55.041
task 1200014:17:55.04314:17:57.044
task 2500014:17:57.04614:18:02.049
task 3100014:18:02.05014:18:03.053
task 4300014:18:03.05514:18:06.058

2.2.12 find out the difference between stopSelf() and stopSelf(int startId) of Service from the source code?

public final void stopSelf() {
    stopSelf(-1);
}
public final void stopSelf(int startId) {
    if (mActivityManager == null) {
        return;
    }
    try {
        mActivityManager.stopServiceToken(
                new ComponentName(this, mClassName), mToken, startId);
    } catch (RemoteException ex) {
    }
}

You can see that the stopSelf() method is just an encapsulation of stopSelf(-1).

So what exactly does startId mean?

According to the log of 2.2.11:

 14:17:51.037  D/MyIntentService: onStartCommand startId:1
 14:17:51.041  D/MyIntentService: onStartCommand startId:2
 14:17:51.042  D/MyIntentService: onStartCommand startId:3
 14:17:51.042  D/MyIntentService: onStartCommand startId:4
 14:17:51.043  D/MyIntentService: onStartCommand startId:5

You can see that the value of startId increases from 1 to 5 after calling startService five times. Why?

Before continuing to read the following, I hope students will understand the startup process of Service. If you don't understand, you can refer to the author's article: Android foundation building - the starting process of Service is the same as that of process starting (based on api21).

Increment of lastStartId

In the startServiceLocked method of ActiveServices:

// After the ServiceRecord r object is obtained, an element is added to its pendingStarts.
r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),
                service, neededGrants));

View the makeNextStartId method of ServiceRecord

public int makeNextStartId() {
     lastStartId++;
     if (lastStartId < 1) {
        lastStartId = 1;
     }
     return lastStartId;
}

This method is used to increment lastStartId and return it. Specifically: start MyIntentService once, and the value of lastStartId is 1; Start MyIntentService twice, and the value of lastStartId is 2. Therefore, lastStartId means the number of times the service was recently started.

Receive startId

In the sendServiceArgsLocked method, lastStartId will be sent to the startId parameter of onStartCommand method through cross process call.

private final void sendServiceArgsLocked(ServiceRecord r, boolean execInFg,
        boolean oomAdjusted) {
    // Traverse the pendingStarts list of ServiceRecord
    while (r.pendingStarts.size() > 0) {
        try {
            ServiceRecord.StartItem si = r.pendingStarts.remove(0);
            r.app.thread.scheduleServiceArgs(r, si.taskRemoved, si.id, flags, si.intent);
        } catch (RemoteException e) {
        }
    }
}

Here, we know that the startId value of onStartCommand method is the lastStartId value of the current ServiceRecord.

stopServiceToken method of AMS

public boolean stopServiceToken(ComponentName className, IBinder token,
        int startId) {
    synchronized(this) {
        return mServices.stopServiceTokenLocked(className, token, startId);
    }
}

stopServiceTokenLocked method of ActiveServices

boolean stopServiceTokenLocked(ComponentName className, IBinder token,
        int startId) {
    ServiceRecord r = findServiceLocked(className, token, UserHandle.getCallingUserId());
    if (r != null) {
        if (startId >= 0) {
            // If the number of times the service was recently started and the startId are not equal, the method execution is exited.
            if (r.getLastStartId() != startId) {
                return false;
            }
            
        }
        // To stop the service
        bringDownServiceIfNeededLocked(r, false, false);
        Binder.restoreCallingIdentity(origId);
        return true;
    }
    return false;
}

When the value of startId is - 1, the condition of startId > = 0 will not be met, so it will directly call to stop the service;

When the value of startId is greater than or equal to 0, the service will be stopped only when the value of startId is equal to the number of times the service has been started recently.

2.3 thread pool in Android

2.3.1 what are the meanings of various parameters in the construction method of ThreadPoolExecutor?

public ThreadPoolExecutor(int corePoolSize,						// First parameter
                          int maximumPoolSize,					// Second parameter
                          long keepAliveTime,					// 3rd parameter
                          TimeUnit unit,						// 4th parameter
                          BlockingQueue<Runnable> workQueue,	// 5th parameter
                          ThreadFactory threadFactory,			// 6th parameter
                          RejectedExecutionHandler handler) {   // 7th parameter
Serial numberParameter nameParameter typeParameter meaningValue rangeinterpretative statement
1corePoolSizeintNumber of core threads>=0If it is equal to 0, the threads of the thread pool will be destroyed when no request enters after the task execution is completed; If it is greater than 0, the core thread will not be destroyed even if the local task is completed. Setting this value is very critical. Setting it too low will waste resources, and setting it too small will cause threads to be created or destroyed frequently.
2maximumPoolSizeintThe maximum number of threads that can be executed simultaneously in the thread pool>0 and > = corepoolsizeIf the number of tasks exceeds the upper limit of the task cache of the fifth parameter workQueue and the number of threads to be executed is less than maximumPoolSize, you need to cache them in the queue with the help of the fifth parameter. If maximumPoolSize is equal to corePoolSize, it is a fixed size thread pool. Maximum number of threads = number of core threads + number of non core threads.
3keepAliveTimelongIdle time of threads in thread pool>=0When the idle time reaches the keepAliveTime value, the non core threads will be destroyed until only corePoolSize threads are left, so as to avoid wasting memory and handle resources. By default, keepAliveTime works only when the number of threads in the thread pool is greater than corePoolSize. However, when the allowCoreThreadTimeOut variable of ThreadPoolExecutor is set to true, the core thread will also be recycled after timeout.
4unitTimeUnitTime unitThe time unit of keepAliveTime is usually timeunit SECONDS.
5workQueueBlockingQueue<Runnable>Task cache queueCannot be nullWhen the number of requested threads is greater than or equal to corePoolSize, the task will enter the blocking queue and wait for execution.
6threadFactoryThreadFactoryThread factoryCannot be nullThreads used to generate a set of the same tasks. The naming of thread pool is realized by adding group name prefix to the factory. When analyzing the virtual machine stack, you can know which thread factory produces the thread task.
7handlerRejectedExecutionHandlerObject that executes the reject policyCannot be nullWhen the number of threads to be executed is greater than or equal to maximumPoolSize, the request can be processed through this policy, which is a simple current limiting protection.

2.3.2 what are the rules for ThreadPoolExecutor to execute tasks?

  1. If the number of threads in the thread pool does not reach the number of core threads, a core thread will be directly started to execute the task;
  2. If the number of threads in the thread pool has reached or exceeded the number of core threads, the task will be inserted into the task queue and queued for execution;
  3. If the task cannot be inserted into the task queue in step 2 (often because the task queue is full), at this time, if the number of threads does not reach the maximum specified by the thread pool, a non core thread will be started to execute the task;
  4. If the number of threads in step 3 has reached the maximum specified by the thread pool, the task will be rejected.

The flow chart is as follows:

2.3.3 what is the UML class diagram of ThreadPoolExecutor?

  • The top-level interface Executor provides an idea: decouple task submission and task execution. Users do not need to pay attention to how to create threads and how to schedule threads to execute tasks. Users only need to provide Runnable objects to submit the running logic of tasks to the Executor, and the Executor framework completes the scheduling of threads and the execution of tasks.
  • ExecutorService interface inherits the Executor interface and adds some capabilities: (1) expand the ability to execute tasks and supplement the methods that can generate Future for one or a batch of asynchronous tasks; (2) It provides a method to control the thread pool, such as stopping the operation of the thread pool and judging whether the thread pool is running.
  • AbstractExecutorService is an upper level abstract class, which provides the default implementation of the ExecutorService interface (submit method and invokeAll method), adds the newTaskFor method to return a RunnableFuture, and does not implement the core method execute. Its function is to connect the processes of executing tasks in series to ensure that the implementation of the lower level only needs to pay attention to one method of executing tasks.
  • ThreadPoolExecutor inherits from AbstractExecutorService class and is a concrete implementation class. On the one hand, it maintains its own life cycle, on the other hand, it manages tasks and threads at the same time, so that they can be well combined to execute parallel tasks.
  • The ScheduledExecutorService interface inherits the ExecutorService interface and increases the ability to schedule operation or regular execution after a given delay.
  • ScheduledThreadPoolExecutor inherits from the ThreadPoolExecutor class and implements the ScheduledExecutorService interface. On the basis of ThreadPoolExecutor, it extends the ability to schedule running or regular execution after a given delay.

Which thread pools are there in executors 2.4? What are the characteristics of these thread pools?

Comparison itemnewCachedThreadPoolnewFixedThreadPool(int nThreads)newSingleThreadExecutornewScheduledThreadPool(int corePoolSize)
corePoolSize0nThreads1corePoolSize
maximumPoolSizeInteger.MAX_VALUEnThreads1Integer.MAX_VALUE
keepAliveTime60L0L0L0
unitTimeUnit.SECONDSTimeUnit.MILLISECONDSTimeUnit.MILLISECONDSTimeUnit.NANOSECONDS
workQueuenew SynchronousQueue<Runnable>()new LinkedBlockingQueue<Runnable>()new LinkedBlockingQueue<Runnable>()new DelayedWorkQueue()
Thread pool nameUnbounded thread pool for automatic thread recyclingFixed size thread poolSingle thread poolExecute scheduled tasks, repeat tasks, thread pool
characteristicThere are no core threads, only non core threads (the maximum is Integer.MAX_VALUE). Idle threads that exceed 60s will be recycled. SynchronousQueue will submit tasks directly to threads without keeping them, so the tasks will be executed immediately.There are only a fixed number of core threads, no non core threads, core threads will not be recycled, and there is no limit on the size of the task queue. When the thread is idle, it will not be recycled; When all threads are active, a new task arrives and is in a waiting state until a thread is idle.There is only one core thread, no non core thread, the core thread will not be recycled, and there is no limit on the size of the task queue. It can ensure that all tasks are executed in sequence in the same thread.The number of core threads is fixed, and the number of non core threads is unlimited. When non core threads are idle, they will be recycled immediately.

3. Finally

This article summarizes threads and thread pools in Android.

Limited to space, the summary of thread pool is not in-depth, such as how thread pool reuses threads? Thread pool source code analysis? How does the thread pool reflect the producer and consumer patterns? For these contents, students can check the articles in the reference section. These articles are worth studying.

reference resources

Keywords: thread thread pool

Added by prbrowne on Sun, 20 Feb 2022 06:40:05 +0200