- Android Handler Internals
- The Nuggets of Gold translation Project
- Translator: Jamweak
- Proofreader: Newt0n, coding monkey
If you want an Android application to be responsive, you must prevent its UI thread from being blocked. Similarly, moving these blocking or computationally intensive tasks to the worker thread improves the responsiveness of the program. However, the execution of these tasks usually results in updating the display of the UI component, which can only be done in the UI thread. There are several solutions to the blocking problem of UI threads, such as blocking queues, shared memory, and pipe techniques. Android provides a proprietary messaging mechanism called Handler to solve this problem. Handler is a basic component of the Android Framework. It implements a non-blocking message passing mechanism. In the process of message transformation, neither the producer nor the consumer of the message will block.
While Handler is used very often, it’s easy to overlook how it works. This article delves into the implementation of many of Handler’s internal components, which will show you how powerful Handler is, not just as a tool for worker threads to communicate with UI threads.
Image browsing examples
Let’s start with an example of how to use Handler in an application. Imagine an Activity that needs to fetch and display images from the web. There are several ways to do this. In the example below, we create a new worker thread to perform the network request to fetch the image.
public class ImageFetcherActivity extends AppCompactActivity { class WorkerThread extends Thread { void fetchImage(String url) { // network logic to create and execute request handler.post(new Runnable() { @Override public void run() { imageView.setImageBitmap(image); }}); } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { // prepare the view, maybe setContentView, etc new WorkerThread().fetchImage(imageUrl); }}Copy the code
Another option is to use Handler Messages instead of the Runnable class.
public class ImageFetcherAltActivity extends AppCompactActivity { class WorkerThread extends Thread { void fetchImage(String url) { handler.sendEmptyMessage(MSG_SHOW_LOADER); // network call to load image handler.obtainMessage(MSG_SHOW_IMAGE, imageBitmap).sendToTarget(); } } class UIHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SHOW_LOADER: { progressIndicator.setVisibility(View.VISIBLE); break; } case MSG_HIDE_LOADER: { progressIndicator.setVisibility(View.GONE); break; } case MSG_SHOW_IMAGE: { progressIndicator.setVisibility(View.GONE); imageView.setImageBitmap((Bitmap) msg.obj); break; } } } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { // prepare the view, maybe setContentView, etc new WorkerThread().fetchImage(imageUrl); }}Copy the code
In the second example, the worker thread retrieves an image from the network, and once the download is complete, we need to update the ImageView display with the downloaded Bitmap. We know you can’t update UI components in a non-UI thread, so we use handlers. The Handler acts as the middleman between the worker thread and the UI thread. Messages are queued by handlers in the worker thread and then processed by handlers in the UI thread.
Learn more about Handler
The Handler consists of the following parts:
- Handler
- Message
- Message Queue
- Looper
We’ll look at the components and their interactions next.
Handler
Handler[2] is a just-in-time interface for passing messages between threads. Producing and consuming threads invoke the following operations to use Handler:
- Creates, inserts, or removes messages from message queues
- The message is processed in the consuming thread
Android. OS. Handler component
Each Handler has a Looper and message queue associated with it. There are two ways to create a Handler:
- The default constructor uses the Looper associated with the current thread
- Explicitly specify the Looper to use
A Handler that does not specify Looper will not work because it cannot put messages into a message queue. Again, it can’t get the message to process.
public Handler(Callback callback, boolean async) { // code removed for simplicity mLooper = Looper.myLooper(); If (mLooper == null) {throw new RuntimeException(" Can't create handler inside thread that has not been called Stars. Prepare () "); } mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async; }Copy the code
The code snippet above shows the logic to create a new Handler. Handler checks for available Looper objects for the current thread at creation time, and throws a run-time exception if it doesn’t. If normal, the Handler will hold a reference to the message queue object in Looper.
Note: Multiple handlers in the same thread share the same message queue because they share the same Looper object.
The Callback argument is an optional argument that, if provided, will process the message distributed by Looper.
Message
Message[3] is a container for arbitrary data. The production thread sends a message to the Handler, which adds the message to the message queue. The message provides three additional types of information for use by the Handler and message queue when processing:
- what— An identifier that can be used by handlers to distinguish different messages and thus take different actions
- timeTell the message queue when to process the message
- target— Indicates which Handler should process the message
Android. OS. Message component
Messages are typically created using the following methods in the Handler:
public final Message obtainMessage() public final Message obtainMessage(int what) public final Message obtainMessage(int what, Object obj) public final Message obtainMessage(int what, int arg1, int arg2) public final Message obtainMessage(int what, int arg1, int arg2, Object obj)Copy the code
The message is retrieved from the message pool, and the parameters provided in the method are put into the corresponding fields in the message body. The Handler can also set the target of the message to itself, which allows us to make chained calls such as:
mHandler.obtainMessage(MSG_SHOW_IMAGE, mBitmap).sendToTarget();
Copy the code
A message pool is a collection of LinkedList of message body objects with a maximum length of 50. After the Handler has processed the message, the message queue returns the object to the message pool and resets all its fields.
When a Runnable is executed by calling the POST method with Handler, the Handler implicitly creates a new message and sets the callback parameter to store the Runnable.
Message m = Message.obtain();
m.callback = r;
Copy the code
An interaction in which a production thread sends a message to a Handler
In the figure above, we can see the interaction between the production thread and the Handler. The producer creates a message and sends it to the Handler, which queues the message and processes it in the consuming thread at some point in the future.
Message Queue
Message Queue[4] is an unbounded collection of LinkedList of Message body objects. It inserts messages into the queue in chronological order, and the smallest timestamp will be processed first.
Android. OS. MessageQueue components
Message queues also get the current time via systemclock. uptimeMillis and maintain a dispatch barrier. When the timestamp of a message body falls below this value, the message is distributed to Handler for processing.
Handler provides three ways to send messages:
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean sendMessageAtFrontOfQueue(Message msg)
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
Copy the code
To delay sending messages, set the time field of the message body to Systemclock. uptimeMillis() + delayMillis.
Delayed messages have their time fields set to Systemclock. uptimeMillis() + delayMillis. However, through sendMessageAtFrontOfQueue () method is inserted into the first team, the news will be the time field is set to 0, the message will be processed when the next polling. Use this approach with caution because it can affect message queues, cause ordering problems, or other unexpected side effects.
Handlers are often associated with UI components that typically hold references to activities. References to these components held by the Handler can lead to potential Activity leaks. Consider the following scenario:
public class MainActivity extends AppCompatActivity {
private static final String IMAGE_URL = "https://www.android.com/static/img/android.png";
private static final int MSG_SHOW_PROGRESS = 1;
private static final int MSG_SHOW_IMAGE = 2;
private ProgressBar progressIndicator;
private ImageView imageView;
private Handler handler;
class ImageFetcher implements Runnable {
final String imageUrl;
ImageFetcher(String imageUrl) {
this.imageUrl = imageUrl;
}
@Override
public void run() {
handler.obtainMessage(MSG_SHOW_PROGRESS).sendToTarget();
InputStream is = null;
try {
// Download image over the network
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.connect();
is = conn.getInputStream();
// Decode the byte payload into a bitmap
final Bitmap bitmap = BitmapFactory.decodeStream(is);
handler.obtainMessage(MSG_SHOW_IMAGE, bitmap).sendToTarget();
} catch (IOException ignore) {
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ignore) {
}
}
}
}
}
class UIHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SHOW_PROGRESS: {
imageView.setVisibility(View.GONE);
progressIndicator.setVisibility(View.VISIBLE);
break;
}
case MSG_SHOW_IMAGE: {
progressIndicator.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
imageView.setImageBitmap((Bitmap) msg.obj);
break;
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressIndicator = (ProgressBar) findViewById(R.id.progress);
imageView = (ImageView) findViewById(R.id.image);
handler = new UIHandler();
final Thread workerThread = new Thread(new ImageFetcher(IMAGE_URL));
workerThread.start();
}
}Copy the code
In this example, the Activity starts a new worker thread to download and display the image in the ImageView. The worker thread notifies the UI of updates through UIHandler, which holds references to views to update their state (switching visibility, setting images, etc.).
Let’s assume that the worker thread takes a long time to download the image due to poor network. Destroying the Activity before the worker thread completes the download will cause the Activity to leak. In this case, there are two strong reference relationships, one between the worker thread and UIHandler, and one between UIHandler and the View. This prevents the garbage collection mechanism from collecting references to the Activity.
Now, let’s look at another example:
public class MainActivity extends AppCompatActivity { private static final String TAG = "Ping"; private Handler handler; class PingHandler extends Handler { @Override public void handleMessage(Message msg) { Log.d(TAG, "Ping message received"); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); handler = new PingHandler(); final Message msg = handler.obtainMessage(); handler.sendEmptyMessageDelayed(0, TimeUnit.MINUTES.toMillis(1)); }}Copy the code
In this example, the following events occur in order:
- PingHandler is created
- The Activity sends a delayed message to the Handler, which is then added to the message queue
- The Activity is destroyed before the message arrives
- The message is distributed and processed by UIHandler, which prints a log
Although it may not seem obvious at first, the Activity in this example also has leaks.
After the Activity is destroyed, the Handler should be garbage collected, but when a message object is created, it also holds a reference to Handler:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
Copy the code
The Android code snippet above shows that all messages sent to Handler will eventually trigger the enqueueMessage method. Notice that the Handler reference is explicitly assigned to msg.target to tell the Looper object which Handler to select to process the message when it is enqueued from the message queue.
After the message is added to the message queue, the message queue gets the reference to the message. It also has a Looper associated with it. The life of a custom Looper object lasts until it is terminated, whereas the Looper object in the main thread persists for the life of the program. Therefore, the reference to the Handler held in the message persists until the message is reclaimed by the message queue. Once the message is reclaimed, its internal fields, including the reference to the target, are emptied.
Although the Handler can survive for a long time, it will not be emptied when an Activity leaks. To check for leaks, we must check that the Handler holds a reference to the Activity within the scope of the class. In this case, it does hold: the non-static inner class holds an implicit reference to its outer class. To be clear, PingHandler is not defined as a static class, so it holds an implicit reference to the Activity.
You can use a combination of weak references and static class modifiers to prevent Activity leaks caused by handlers. Weak references allow the garbage collector to reclaim the object you want to keep when an Activity is destroyed (typically an Activity). Adding static modifiers to Handler inner classes prevents external classes from holding implicit references.
Let’s fix this by modifying the UIHandler in the previous example:
static class UIHandler extends Handler { private final WeakReference mActivityRef; UIHandler(ImageFetcherActivity activity) { mActivityRef = new WeakReference(activity); } @Override public void handleMessage(Message msg) { final ImageFetcherActivity activity = mActivityRef.get(); if (activity == null) { return } switch (msg.what) { case MSG_SHOW_LOADER: { activity.progressIndicator.setVisibility(View.VISIBLE); break; } case MSG_HIDE_LOADER: { activity.progressIndicator.setVisibility(View.GONE); break; } case MSG_SHOW_IMAGE: { activity.progressIndicator.setVisibility(View.GONE); activity.imageView.setImageBitmap((Bitmap) msg.obj); break; }}}}Copy the code
Now, the Activity needs to be passed in to the UIHandler constructor, and this reference will be wrapped with a weak reference. This allows the garbage collector to reclaim the reference when the Activity is destroyed. When interacting with UI components in an Activity, we need to get a strong reference to the Activity from mActivityRef. Since we are using a weak reference, we must be careful to access the Activity. If we could only access the Activity through weak references, the garbage collector might have already reclaimed it, so we need to check to see if the collection happened. If it is, and the Handler is actually no longer associated with the Activity, the message should be discarded.
While this logic solves the memory leak problem, there is still a problem. The Activity has been destroyed, but the garbage collector hasn’t had time to reclaim the reference, and depending on how the operating system is running, this can cause your program to potentially crash. To solve this problem, we need to get the current state of the Activity.
Let’s update the UIHandler logic to solve the problem in the above scenario:
static class UIHandler extends Handler { private final WeakReference mActivityRef; UIHandler(ImageFetcherActivity activity) { mActivityRef = new WeakReference(activity); } @Override public void handleMessage(Message msg) { final ImageFetcherActivity activity = mActivityRef.get(); if (activity == null || activity.isFinishing() || activity.isDestroyed()) { removeCallbacksAndMessages(null); return } switch (msg.what) { case MSG_SHOW_LOADER: { activity.progressIndicator.setVisibility(View.VISIBLE); break; } case MSG_HIDE_LOADER: { activity.progressIndicator.setVisibility(View.GONE); break; } case MSG_SHOW_IMAGE: { activity.progressIndicator.setVisibility(View.GONE); activity.imageView.setImageBitmap((Bitmap) msg.obj); break; }}}}Copy the code
Now we can summarize the interaction between message queues, handlers, and production threads:
Interaction between message queues, handlers, and production threads
In the figure above, multiple production threads submit messages to different handlers. However, different handlers are associated with the same Looper object, so all messages join the same message queue. This is important because many of the different handlers created in Android are associated with the main thread Looper:
- The Choreographer:Handles vSYNC and frame updates
- The ViewRoot:Handles input and window events, configuration changes, and more
- The InputMethodManager:Handle keyboard touch events and more
Tip: Make sure that the production thread does not churn out messages, as this may inhibit the processing system from generating them.
A small example of the main thread Looper distributing messages
Debug help: You can debug/dump messages distributed by Looper by attaching a LogPrinter to Looper:
final Looper looper = getMainLooper();
looper.setMessageLogging(new LogPrinter(Log.DEBUG, "Looper"));
Copy the code
Similarly, you can debug/dump all messages waiting in the message queue by attaching a LogPrinter to the Handler associated with the message queue:
handler.dump(new LogPrinter(Log.DEBUG, "Handler"), "");
Copy the code
Looper
Looper[5] reads messages from the message queue and distributes them to the corresponding Handler for processing. Once the message passes the blocking threshold, Looper reads it in the next read. Looper blocks when no messages are distributed and continues polling when messages are available.
Only one Looper can be associated with each thread, and attaching another Looper to a thread can result in a runtime exception. Using ThreadLocal objects in the Looper class ensures that only one Looper object is associated with each thread.
Calling the looper.quit () method immediately terminates Looper and discards any messages in the message queue that have passed the blocking threshold. Calling the looper.quitsafely () method ensures that all messages to be dispensed are handled before the messages waiting in the queue are dropped.
The overall flow of handlers interacting directly with message queues and Looper
Looper should be initialized in the thread’s run method. Calling the static method looper.prepare () checks whether the thread is associated with an existing Looper. This is done by checking for the existence of a Looper object with a ThreadLocal object in the Looper class. If Looper does not exist, a new Looper object and a new message queue will be created. The following snippet in the Android code shows this process.
Note: The public prepare method calls prepare(true) by default.
private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() ! = null) {throw new RuntimeException(" Only one Looper may be created per thread "); } sThreadLocal.set(new Looper(quitAllowed)); }Copy the code
The Handler can now receive the message and queue it, executing the static method looper.loop () to begin dequeuing the message from the queue. Each polling iterator points to the next message, which is then distributed to the corresponding target Handler, and then reclaimed to the message pool. The looper.loop () method loops through the process until Looper terminates. The following snippet in the Android code shows this process:
public static void loop() { if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } msg.target.dispatchMessage(msg); msg.recycleUnchecked(); }}Copy the code
There is no need to create the thread associated with Looper yourself. Android provides a handy class to do this — HandlerThread. It inherits the Thread class and provides management of Looper creation. The following code describes its general usage:
private final Handler handler;
private final HandlerThread handlerThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate();
handlerThread = new HandlerThread("HandlerDemo");
handlerThread.start();
handler = new CustomHandler(handlerThread.getLooper());
}
@Override
protected void onDestroy() {
super.onDestroy();
handlerThread.quit();
}
Copy the code
The onCreate() method constructs a HandlerThread. When the HandlerThread is started, it prepares to create a Looper associated with its thread, which then starts processing the messages in the HandlerThread’s message queue.
Note: It is important to terminate the HandlerThread when the Activity is destroyed. This action also terminates the associated Looper.
conclusion
Handler in Android plays an indispensable role in the application life cycle. It forms the basis of the semi-synchronous/semi-asynchronous schema architecture. Many internal and external code relies on handlers to distribute events asynchronously, which can maintain thread-safety with minimal cost.
A deeper understanding of how components work can help solve problems. This also allows us to use the component’s API in the best way possible. We typically use handlers as a communication mechanism between worker threads and UI threads, but handlers are not limited to that. It appears in IntentService[6], Camera2[7] and many other apis. In these API calls, the Handler is more often used as a communication tool between arbitrary threads.
With a deeper understanding of how Handler works, we can use it to build more efficient, concise, and robust applications.