Bits and pieces of things are not always remembered for a long time, just learning other people’s articles is just the residue left by others after chewing. I accidentally found this daily interview question, and I thought that if I just thought about it simply, I would not only have little effect, but even I might be too lazy to think about the difficult questions and could not stick to them. So write down every thought, understanding, and other people’s insights. Not only deepen your understanding, but also motivate yourself to stick to it.
preface
The interview a few days ago left me totally confused. Now I have some spare time to discuss the problems at that time carefully. The most impressive problem is about HandlerThread and AsyncTask. Start by making a list of questions that come to mind during the discussion and questions that you ask during the interview.
- HandlerThread and AsyncTask implementation principle
- What happens when HandlerThread and AsyncTask complete time-consuming tasks
- Whether the processing of time-consuming events in HandlerThread and AsyncTask is asynchronous or synchronous, and whether it can be changed to another processing
- The possibility of HandlerThread and AsyncTask memory leaks
- HandlerThread and AsyncTask
- Practical application scenarios of HandlerThread and AsyncTask
- Given a large number of time-consuming tasks, time-consuming operations are not continuous and the length of time-consuming time varies. Which one should be used
The source code parsing
First, a brief talk about the source code of these two, are very simple, not so complicated. The following source code is from Android-28
HandlerThread
HandlerThread is actually a simple wrapper around Thread+Looper+Handler.
The HandlerThread class inherits the Thread class, so you need the start() method to start the Thread.
HandlerThread handlerThread = new HandlerThread("workThread");
handlerThread.start();
Handler threadHandler = new Handler(handlerThread.getLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// Process different time-consuming tasks according to different message types}};Copy the code
That’s the easy way to use it. The Handler is created by passing in the Looper object in the HandlerThread so that the handlerMessage method handles time-consuming tasks in the child thread. Take the task can be threadHandler. SendMessage () sends the message, and then the handlerMessage method for processing.
The most important thing in the source code is a rewrite of the run method.
@Override
public void run(a) {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();// Wake up thread can get Looper object
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
Copy the code
This method is executed after we start a thread by executing handlerThread.start(). Create a Looper object in this thread with looper.prepare (), and notify other threads that the Looper object is available, setting thread priority. OnLooperPrepared () is an empty method that we can override to do some preparation before Looper starts the loop.
Once everything is ready, it’s time to send the message through Handler and then do the time-consuming operation in handlerMessage(). To clarify, the thread in which the handlerMessage() method of the Handler class is executed is determined by the thread in which the Looper object in the Handler resides. This is because in the loop cycle by MSG. Target. DispatchMessage () – > handleMessage () indirectly invoked the handlerMessage method, and the stars. The loop is executed in the child thread. Android’s message Handler is a Handler that can be used to create a message Handler
AsyncTask
AsyncTask encapsulates a thread pool and Handler
Some basic use is not detailed, mainly to look at the source code. There are three constructors. No arguments, Handler, Looper, the first two call the third constructor.
public AsyncTask(@Nullable Looper callbackLooper) {
mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
? getMainHandler()
: new Handler(callbackLooper);
mWorker = new WorkerRunnable<Params, Result>() {
public Result call(a) 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 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); }}}; }Copy the code
It’s a little long, but it’s really just three steps.
- Initialize mHandler. MHandler is used to convert threads. A new Handler is created when the Looper object passed in is not empty and is not a Looper for the main thread, otherwise the Handler for the main thread is obtained. GetMainHandler () is a simple operation that creates a new Handler object and passes in the main thread’s Looper object.
- Initialize mWorker. MWorker is an object of a class that implements the Callable interface. The call method is overridden at initialization, and the time-consuming task doINbackGround is encapsulated within it.
- Initialize the mFuture. MFuture is a FutureTask object that is a subclass of Runnable and Future. Pass the mWorker into the mFuture object. The next step is to pass this object into the thread pool for scheduling.
It is usually used to start tasks with the execute method to see what is done in the source code.
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(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
Execute executes the executeOnExecutor method and passes in sDefaultExecutor with the parameters needed for the time-consuming task. MStatus is an enumeration variable that has three states, including PENDING, RUNNING, and FINSHED. All three states are unique. When the object is initialized, it is PENDING in the order of RUNNING FINISHED. Changes to RUNNING in executeOnExecutor and to FINSHED in Finish. So you can see that executor methods can only be executed once in an object, and multiple executions will throw an exception. Then update the state and call the onPreExecute method, which we can override to prepare for the time-consuming operation. The parameters are passed in, followed by the exec. Execute submission task, which is the FutureTask object wrapped in the constructor.
Exec is the member variable sDefaultExecutor, a static class defined internally by AsyncTask that implements the Executor interface.
private static class SerialExecutor implements Executor {
// A two-end queue that stores FutureTask objects on a first-in, first-out basis
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
// The incoming mFuture is wrapped once more to facilitate serial processing of tasks
mTasks.offer(new Runnable() {
public void run(a) {
try {
r.run();
} finally {
// Select the next task after completing the previous time-consuming taskscheduleNext(); }}});// Select a task
if (mActive == null) { scheduleNext(); }}protected synchronized void scheduleNext(a) {
if((mActive = mTasks.poll()) ! =null) {
// SerialExecutor is only responsible for the serial processing of tasks. The real time-consuming tasks are scheduled by the THREAD_POOL_EXECUTOR thread poolTHREAD_POOL_EXECUTOR.execute(mActive); }}}Copy the code
This is a static class, meaning that all time-consuming tasks are sequentially processed through this class. SerialExecutor exists in order for time-consuming tasks to be processed serially, and it is the THREAD_POOL_EXECUTOR thread pool that actually processes time-consuming tasks.
// Get the number of cpus without sleep
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// We want at least 2 threads and at most 4 threads in the 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 ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);//cas operation int variable
public Thread newThread(Runnable r) {
// Record the number of threads created
return new Thread(r, "AsyncTask #"+ mCount.getAndIncrement()); }};private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
// Setting the core thread pool is also affected by the set lifetime
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
Copy the code
This is to create a new thread pool and assign the value to the static variable THREAD_POOL_EXECUTOR. This is done in the static code block, meaning that it is only executed once when the class is loaded. All time-consuming tasks are executed in this single thread pool. A few words about the parameters in this thread pool.
- CORE_POOL_SIZE: the number of core threads in the pool. This definition is a bit complicated. The official explanation is that we always expect the number of core threads to be between 2 and 4, and prefer to have one less than the number of working cpus.
- MAXIMUM_POOL_SIZE: The maximum number of threads that exist, which is the sum of core and non-core threads. The default value is 2 times the number of working cpus +1.
- KEEP_ALIVE_SECONDS: specifies the time for non-core threads to remain alive after completing a task. The timeout will be destroyed. If the allowCoreThreadTimeOut(True) attribute is set, the core thread is also subject to this constraint. The default is 30
- Timeunit. SECONDS: The unit of the previous property, in this case SECONDS.
- SPoolWorkQueue: A queue that stores time-consuming tasks. Here we use LinkBlockingQueue, a block queue based on a linked list implementation, with the number of stores limited to 128.
- SThreadFactory: A thread factory that provides the creation of new threads for the thread pool. ThreadFactory is an interface that contains only one newThread method. The default is the DefaultThreadFactory class.
All that remains is for the thread pool to schedule the mFuture to perform time-consuming tasks, executing the Call method of the Callable interface in the mFuture. This is the mWorker initializer from the constructor above, which overwrites the call method.
mWorker = new WorkerRunnable<Params, Result>() {
public Result call(a) throws Exception {
// Indicates that the time-consuming task has been executed
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
// If an exception occurs, set the task status to cancel
mCancelled.set(true);
throw tr;
} finally {
// In any case, the result
postResult(result);
}
returnresult; }};Copy the code
As you can see, the postResult() method is used to wrap up after a time-consuming operation, either after processing or when an exception occurs.
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
A message is wrapped with Handler and sent out. Labeled MESSAGE_POST_RESULT, which means the result of a time-consuming task. This Handler, which is the same Handler object initialized in the constructor, is wrapped in a custom static internal Handler class through getMainHandler.
private static Handler getMainHandler(a) {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler(Looper.getMainLooper());
}
returnsHandler; }}private static class InternalHandler extends Handler {
public InternalHandler(Looper looper) {
super(looper);
}
@SuppressWarnings({"unchecked"."RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
// Static inner class of AsyncTask to pass the resulting data to the corresponding AsyncTask objectAsyncTaskResult<? > 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
This is where the sent message is processed. If MESSAGE_POST_RESULT, call finish; If it is MESSAGE_POST_PROGRESS, it is the onProgressUpdate(result.mdata) method, which we can use to override the update progress. The MESSAGE_POST_PROGRESS class message is only called when you call the publishProgress method.
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
Copy the code
The finish method will call onCancelled(result) or onPostExecute(result) depending on the status of the mCancel, meaning that only one of the two methods will be called. These two methods are also the ones we need to override when using AsyncTask. MCanael We can change the state by calling cancel().
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
// Break the thread in mFuture
return mFuture.cancel(mayInterruptIfRunning);
}
Copy the code
The whole internal process is not good enough. Time consuming tasks are wrapped into FutureTask, SerialExecutor wraps the FutureTask so that the time consuming task can be executed serially, and the THREAD_POOL_EXECUTOR thread pool performs the actual time consuming task scheduling.
FAQ about HandlerThread and AsyncTask
General implementation principle
- HandlerThread: Starts a child thread, creates a new Looper and starts the Looper loop, keeping the child thread alive until the loop exits. If you initialize the HandlerThread, new a Handler and pass it to Looper in the Handler, you can send a message through the Handler, receive the message in the handlerMessage and perform the corresponding time-consuming task.
- AsyncTask: Passing FutureTask objects that encapsulate time-consuming tasks in doInBackground into SerialExecutor via executor, SerialExecutor serially sends tasks to the THREAD_POOL_EXECUTOR thread pool for scheduling.
What happens after completing time-consuming tasks
- HandlerThread is a loop loop +Handler message processing mechanism, that is, as long as the loop loop does not exit, the thread will not stop, and the Handler only needs to send the corresponding type of message to process the time-consuming task.
- The time consuming tasks of AsyncTask are scheduled by the thread pool. The survival of the thread after the time consuming tasks are completed depends on the characteristics of the thread pool. However, the execute method of AsyncTask cannot be invoked after it is executed once. The internal state changes in the order of PENDING, RUNNING, and FINISHED and cannot be reversed. In the executeOnExecutor method the status is checked and an exception is thrown.
Whether the processing of time-consuming events is asynchronous or synchronous, and whether it can be changed to another processing
HandlerThread
HandlerThread is a time consuming operation in the loop of the child thread. The next message processing can be obtained only after the current time consuming operation is completed, so it is serial. As for becoming parallel, no. So let’s just verify that
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.US);
Handler threadHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
HandlerThread handlerThread = new HandlerThread("workThread");
handlerThread.start();
threadHandler = new Handler(handlerThread.getLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("HandlerThread"."message" + msg.what + ":" + df.format(newDate())); }}; }public void onLoginClick(View v){
int i = 0;
threadHandler.sendEmptyMessage(++i);
threadHandler.sendEmptyMessage(++i);
threadHandler.sendEmptyMessage(++i);
threadHandler.sendEmptyMessage(++i);
threadHandler.sendEmptyMessage(++i);
}
Copy the code
AsyncTask
Customize a simple AsyncTask
static class MyAsyncTask extends AsyncTask<Void.Void.String>{
String name = "AsyncTask";
private MyAsyncTask(String name){
super(a);this.name = name;
}
@Override
protected String doInBackground(Void... voids) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return name;
}
@Override
protected void onPostExecute(String string) {
super.onPostExecute(string);
Log.e("AsyncTask", string + ":" + df.format(newDate())); }}Copy the code
When we do a series of time-consuming tasks, we do them sequentially
new MyAsyncTask("AsyncTask#1").execute();
new MyAsyncTask("AsyncTask#2").execute();
new MyAsyncTask("AsyncTask#3").execute();
new MyAsyncTask("AsyncTask#4").execute();
new MyAsyncTask("AsyncTask#5").execute();
new MyAsyncTask("AsyncTask#6").execute();
new MyAsyncTask("AsyncTask#7").execute();
new MyAsyncTask("AsyncTask#8").execute();
Copy the code
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);
new MyAsyncTask("AsyncTask#5").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new MyAsyncTask("AsyncTask#6").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new MyAsyncTask("AsyncTask#7").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new MyAsyncTask("AsyncTask#8").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
Copy the code
So why do you default to serial processing of tasks when you lose the biggest advantage of thread pools?
This is because the default thread pool actually does not hold many threads. For example, the maximum number of threads is 17 and the blocking queue capacity is 128. The total maximum number of threads is 17+128=145, which can easily fill up under high concurrency. And the THREAD_POOL_EXECUTOR object is unique throughout the application. So the default is serial processing, and if you really have high concurrency, you can customize the thread pool for concurrent processing as required.
The possibility of memory leaks
- The memory leak of the HandlerThread is that you must remember to manually exit the thread when the Activity is destroyed. Because the loop loop is an infinite loop, it will persist until you exit manually.
- The memory leak of AsyncTask is that it must be in the form of a static inner class that holds a reference to an Activity by default, and the lifetime of an Activity and AsyncTask does not guarantee that an Activity is longer.
Precautions in use
- As for HandlerThread, I haven’t used it very much. I checked online information, and there is only one statement. By giving handlerThreads different priorities, the CPU optimizes all threads for different priorities.
- AsyncTask objects are created in the main thread. This is not necessary because the Handler of the internal conversion thread does not depend on the Looper object of the current thread. Instead, the main thread Looper is retrieved via looper.getMainLooper (). Of course, the onPreExecute method is affected by the thread on which the AsyncTask object is created, since this method is not executed through Handler messaging.
- Cancel does not actually terminate the program immediately. It simply changes the variable marked with state. Of course, mFuture.cancel(mayInterruptIfRunning) in the Cancel method, Passing true will also terminate the thread at the FutureTask layer, but for non-stopable operations, onCancelled or onPostExecute will be called depending on the status of the marked variable until the task completes. Therefore, we should constantly check the state in the doInBackground method as much as possible, and exit the thread as soon as it needs to return.
Application scenarios
- HandlerThread is actually a combination of Thread and Looper, so it is suitable for single-thread + multiple time-consuming tasks, such as network requests, file reads and writes. However, I personally don’t think HandlerThread is suitable for multiple tasks with long time due to serial execution.
- AsyncTask is mainly used to interact with the UI thread after a time-consuming task is completed, but it is serial by default. This is probably the official instructions for AsyncTask to perform operations that take only a few seconds, but can be changed to a parallel task directly through executeOnExecutor.
Given a large number of time-consuming tasks, time-consuming operations are not continuous and the length of time-consuming time varies. Which one should be used
As for the question raised in the interview, I would like to briefly explain my understanding. Because my actual development experience is little pitiful, so what say may have mistake or very one-sided, include the answer of a few questions above. Now I really can’t believe the information on the Internet, and some of them even contradict themselves. I suddenly understand the meaning of the interviewer asking me who I usually read and who I know who is more professional in Android.
A lot of time-consuming operations, if they are not connected to each other, are not good in my opinion if they are sequential because they block subsequent tasks that do not need to be executed in a sequential order. So it’s better to do parallel processing in AsyncTask. If the tasks are associated, they need to be executed in serial. At this time, it depends on whether the execution logic of these time-consuming tasks is consistent. If not, multiple AsyncTasks need to be customized, which is also very troublesome. In my opinion AsyncTask is more about interaction with the UI thread.
It’s hard to tell what the differences are between HandlerThread, IntentService, AsyncTask, and ThreadPoolExecutor. (It’s a dish anyway (funny))