Looper and Handler mechanism

This article will understand the Looper and Handler mechanisms from a source and application perspective and give some basic uses.

Lead to

  • Basic understanding of operating systems and threads

  • Master the basic knowledge of Android

  • [the best] download the Android Source code Source control tools | | the Android Open Source Project Android Open Source Project

    Once downloaded, it is recommended to open it in VS Code without compiling it. (much faster than idea, and easy to view multiple languages: JAVA, C++…)

For omissions or do not understand the place, or need to read the source.

Introduction:

Before I begin, I would like to share my personal opinion about how to study. One of the most important ways to learn is to treat the knowledge that is not fully understood as a black box. A black box is an attempt to understand the functionality it provides without understanding the underlying implementation. Some may not need to master the knowledge of the first as a black box, rather than drilling has been drilling, forget the purpose of learning at the beginning, “drilling” to the final discovery of the fundamental “drilling” not over, leaving a “destroy IT I am tired”. Some of this knowledge is not necessary to master, some of the knowledge is not necessary to master now, when the time to master on the line.

To understand how a “Hello World” program works, for example, we don’t need to understand CPU design, instruction sets, semiconductors, even the mining and processing of silicon, or the theory of molecular and atomic interactions. Understanding the process of compiling, linking, and assembling is probably enough (depending on your goal).

Looper and Handler are the core mechanisms for running Android applications. They permeate every aspect of Android applications, and understanding them leads to many other concepts in Android development that I think every Android developer should grasp.

Here’s how to explain the mechanism: Why Looper and Handler? How does it work? How do we use it in development? (Why, What, How)

Why is this mechanism needed?

Because single-threaded UI development is proven to be the right model, multi-threaded UI development is cumbersome and error-prone.

Browsers are single-threaded. Windows UI development framework WPF’s UI can only be updated on the main thread. It seems that all UI development frameworks have a principle that the UI must be updated on the main thread. So the fundamental purpose of this mechanism is to ensure thread safety.

Is a multithreaded UI necessarily unsafe? Some references read, “People have tried many multithreaded UI schemes and given up.” Understandably, single-threaded UIs are an approach that has been proven correct by experience. It’s also easy to understand: suppose we have multiple threads (or more specifically: In the end, we will have to lock the UI ourselves, to implement thread synchronization, which is not only tedious, but also greatly increases the possibility of error, and eventually we will need to implement a set of such Looper/Handler mechanism, so now Android directly adopts this mechanism.

Now, looking ahead to the design of the Android system, if we were to design a single threaded UI, thread-safe mechanism, what would we do? We want it to satisfy:

  • Can process all kinds of “signals”. The so-called “signal”, more specifically, is the interaction between multiple threads, other threads (system layer, application layer) how to control the UI thread, communication with it; In addition to the requirements of our own application, system signals: screen actions, mouse, buttons, and so on need to interact with our application.
  • We do not need to do tedious synchronization, locking
  • Thread safety

It’s easy to think of the production-consumer model because it’s so widely used in browsers, message queues, various UI frameworks, and it works really well. This pattern is described as a “secure distribution idiom” in the book Concurrent Development for Android.

Basic production-consumer model:

  • Messages: A “message” may have the feel of being lightweight and just a “flag,” but in reality a “message” can be any data, such as a Runable interface, which can contain operations thrown directly to a message queue that the consumer performs when consuming it.

  • Producer: Produces the “message” and throws it to the queue.

  • Consumer: In fact, some people may be confused, what is a consumer? My understanding is that the data handler is the consumer. It doesn’t matter what the concept is. There is also a role, a loop, where the general practice is to have a management class that circulates the data and lets the consumer consume it, rather than having the consumer queue the data themselves. If we had to give the manager class of this loop a name, we would call it a Looper. ? 😨).

    So how do you “break” the cycle? You can send an exit message, and the consumer will exit the loop when it receives the message. In order for the program to be robust, the entire “production-consumption” process needs to be stopped after exiting the consumer loop, so that the producer cannot continue to produce data into queues that are no longer consuming.

  • Message queue: As the name suggests, a queue that passes messages between producers and consumers. Since producers and consumers may be on different threads, the queue must be thread-safe, and the simplest (and most common) way is to use a blocking queue. Or to put it another way, a message queue must be a blocking queue, because it blocks when there are no messages and multiple threads are reading or writing simultaneously.

A more specific production-consumer model:

How do you implement thread-safe UI in production-consumer mode?

  1. We initialize the message queue and consumer loop at the entry point of the program, which we call the master thread (i.e., the UI thread).
  2. Restrict all UI operations to be performed on the main thread.
  3. When we need operations on UI or any main thread, we can encapsulate the operations as messages (Message)Push to message queueThe message is then processed by the consumer loop.

As you can see, since each message is processed on the main thread after it has been queued, the next operation is processed after it has been queued, thus ensuring atomicity of each operation, which is of course thread-safe.

So Looper/Handler is nothing fancy, and the core mechanics of Looper/Handler are what we all use in development. Since this is the basic concurrency mechanism of Android applications, the production and processing of messages is very frequent, so let’s combine the source code to explain the details of the implementation.

How does it work?

The general process is shown in the figure above, and the specific details are analyzed in combination with the source code. Here’s the idea:

  • Consumer: Let’s start with the entry to the programLooperHow the loop initializes and starts the loop, how it gets messages, and how it processes them.
  • The producer part: We take the example of how we normally produce messages, and then analyze how messages are pushed into queues.
  • Message queue:MessageQueueMessage queues are not divided into separate sections in order, because their logic is mixed together inProduction-consumptionIn the process of. So once the above part is understood, the message queue part is understood. Here we can add some concepts:
    • Message queues are implemented using linked lists
    • It contains data, correspondingHandlerExpect it to be processed

OK, the point here is that reading with a “top down” approach to comprehension will help you grasp it better. So read the following section with the picture above.

Consumer-process

Start the consumer loop at the entry point of the program: looper.loop (), where we start at the entry point of the program and analyze the call level and details from the top down.

Program entrance

In the Android source code ->ActivityThread->main(), this is an Android app entry.

  • prepareMainLooper()

  • loop()

loop()The details of the

Before we understand loop(), let’s think about what a Message needs to contain in the loop:

  • data
  • The expected time for the message to be processed

Data is nothing to talk about, and with time we can plan tasks in an orderly way, so time is also very important.

Take a look at some of the structure:

The WHEN field is the time it is expected to be processed.

Other things like: target is the Handler object that processes the message, arg, obj, what, etc. are some information carried, pool is used by reuse mechanism, next of course is used by linked list structure… Don’t sweat the details, just think of them as data.

So the goal is: let’s loop in the message that is ready (time is up) and process it.

  • quene.next()

    Here we post some general source code, this method is very important so there will be a detailed analysis, probably do the following things:

    • Returns the next message that is ready, and blocks if not. [This is the most important thing]

      Of course, in addition to the most important part, of course did a few other things, as follows:

    • Exit mechanism

    • Hopefully it will do something extra when there are no messages currently available: like GC collection

  • msg.target.diapatchMessage()

    Process the message

    Handler specifies the priority for processing information:

    • Callback: callback has the highest priority. This is a Runable that is used when we call POST (Runable r).

    • 2. MCallback: the second priority, which we can pass in when we initialize this Handler:

      It is a Callback interface with only one method: handleMessage(@nonnull Message MSG)

      If (McAllback.handlemessage (MSG)) {return; }, so handler.handleMessage () is not expected to be processed when the interface returns true.

    • 3. HandleMessage () : Lowest priority, we override this method to process messages when we inherit this Handler. Handler does nothing by default:

  • msg.recycleUnchecked()

    Message collection. Message has a reuse mechanism, which resets the variables of the Message and adds them to the head of the reuse pool:

MessageQueue.next()The details of the

  1. Exit mechanism

  2. nativePollOnce(long ptr, int timeoutMillis)

    This call calls the native method that provides our core mechanism for blocking and waking up.

    Its invocation hierarchy is as follows:

    As you can see, the core wait function is: epoll_wait().

    The details of the epoll mechanism won’t be explained here, but if this article can’t explain the nature of epoll, come and strangle me! (1) – Zhihu.com; But it goes something like this:

    1. Listen for some handles ->2. It can be woken up when these handles write data. Of course, it also supports setting TimeOut, which is our underlying mechanism for timeouts.

    In server design, a basic usage is:

    int s = socket(AF_INET, SOCK_STREAM, 0);   
    bind(s, ...)
    listen(s, ...)
    
    int epfd = epoll_create(...). ;epoll_ctl(epfd, ...) ;// Add all sockets that need to be listened to to epFD
    
    while(1) {int n = epoll_wait(...).for(Socket that receives data){/ / processing}}Copy the code

    There are two core functions: epoll_ctl and epoll_wait.

    In the constructor of Looper(), the pending handles (here: mWakeEventFd) are added to the epoll listener list

    Many times you need to wake up, such as when you call nativeWake in the Jave layer, and write a value to mWakeEventFd:

  3. When woken up, we find if any messages are available and return:

  4. Do something extra when idle: idle Handler

    This is done only once during a next() call.

    At the first iteration, the mIdleHandlers values are retrieved and then iterated through, calling the.Queueidle () method one by one.

    The IdleHandler interface looks like this:

    When we need an IdleHandler like this, we implement this interface, put the operations in it and add them to the mIdleHandlers (through addIdleHandler(@nonnull IdleHandler Handler) method).

Producer-process

All the producer part has to do is generate the message -> push it to the correct position in the message queue.

Of course we don’t push messages directly from the message queue. Instead, we do this through a Handler. A simple example is as follows:

Specific call hierarchy we omitted here, anyway, in the end is a function called: MessageQueue. EnqueueMessage (MSG, uptimeMillis); What it does is simple: it inserts the Message into the appropriate place chronologically and decides if it needs to wake up the blocked wait (nativePollOnce)

  • Inserted in the header: The current time is smaller than the time of all other messages

    Since we inserted more recent messages, we need to wake up if we are currently blocked.

  • Inserted in the middle: Inserted in chronological order

    The awakening judgment here looks like this:

    1. If our message is not asynchronous and is inserted in the middle of a message queue, this means that the message must have been executed longer than the previous message, so there is no need to wake up.

    2. If our message is an asynchronous message, what does an asynchronous message mean? When there is a barrier (target null), we do not consider synchronized messages:

      This logic can be found in message.next ().

      So if it is an asynchronous message and there is a barrier, then we have to consider whether to wake up even if we are in the middle. What time does it wake up? We need to wake up when the asynchronous message we are inserting is inserted in the header of the asynchronous message.

use

Now that we understand how it works, how do we use it in general?

  • UI updates are performed on the main thread

    You can get the main loop’s Looper: looper.getMainLooper () on any thread and send the message via Handler (which can be new, put in a global variable, or even put in an Utils) :

    • sendMessage()
    • post()

    You can also specify postDelayed(), etc.

  • Use Looper, Handler mechanism in our own thread

    What’s the good of that? The advantage is that you can use them to provide a good API and save yourself a lot of trouble. It’s not too much trouble to write your own production-consumer model (but scheduling tasks is!). .

    For example, if we want to write a worker thread to perform some time-consuming tasks, we can initialize Looper and Handler in one thread, and then whatever task we need to do is put into the queue by Handler, and Looper will automatically fetch the message and execute it. Like this:

    There are two steps to start a cycle:

    1. Initialize theLooper:Looper.prepare()
    2. Start the cycle:Looper.loop()
            new Thread(()->{
                Looper.prepare();
                mWorkLooper = Looper.myLooper();
                Looper.loop();
            }).start();
    Copy the code

    Let’s write a test program to see:

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            new Thread(()->{
                Log.d("ZHUTAG"."Threads in WorkLooper:"+Thread.currentThread());
                Looper.prepare();
                mWorkLooper = Looper.myLooper();
                Looper.loop();
            }).start();
    
            Log.d("ZHUTAG"."Main thread:"+Thread.currentThread());
            try {
                Thread.sleep(1000); // This is in case Looper is not initialized yet
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            workHandler = new Handler(mWorkLooper);
            workHandler.postDelayed(()->{
                Log.d("ZHUTAG"."Got the message!! Current thread:"+Thread.currentThread());
            }, 5000);
        }
    Copy the code

    The results are as follows:

    Of course, it can also be encapsulated as Utils, and then any tasks that need to be executed will be executed using the Looper mechanism of this background thread.

  • It’s helpful to understand some of the other mechanisms of Android.

    For example, the HandlerThread encapsulates the Looper. For example, the IntentService uses the HandlerThread inside:

    Anyway, once you understand how this mechanism works, it’s easy to understand when you encounter components that use it!

conclusion

In fact, this article exists for several purposes:

  • Help me sort out and summarize myself, and I can quickly recall when I forget later
  • Help the reader quickly understand the mechanism and hopefully understand the starting point of the mechanism design and the implementation of some of the theoretical details
  • Can help readers quickly locate the source code, better understand (because in fact WHEN I look at the source code is also referred to a lot of articles, a lot of time to directly see the source code encountered a point stuck on the card for a long time, without problems and design ideas. You can save a lot of time if you have someone else’s work as a basis for learning from their experience.

Hopefully, once you understand the mechanism, in addition to being able to use and understand some Android libraries on your own; You can also gain some insight into this thread interaction mechanism, the production-consumer pattern, for example, if we want to make a queue with timed messages in the future, we will also consider using Epoll. If the service is not very complex, is it ok to use the timeout time of the semaphore to replace epoll?

There is also a, in the understanding of the mechanism, will find that Android source code is actually a lot of time and there is no lofty things, we need to master the mechanism, components can see their own source code!

Well, this article ends here, if you have some small harvest I will be satisfied! Thank you for reading.