Android asynchronous message mechanism from entry to mastery

Table of contents

1. What is the Android asynchronous message mechanism?

2. Introduction to asynchronous message mechanism

3. Source code analysis

1.Message

1.1 Message content

1.2. Processing messages

1.3. Caching mechanism

1.4. Summary

2.Handler and Looper

2.1. Looper in Handler

2.2.Handler sends messages

2.3.Looper takes out the message

2.4.Handler processing messages

2.5. Summary

Fourth, solve the problem

Why does the Hander send a message in the child process, and finally the process where the handler is located will process the message?

Looper.loop() is an infinite loop in the main process, won't it cause blocking?

V. Summary


1. What is the Android asynchronous message mechanism?

Imagine such a demand scenario, your App now needs to obtain data from network requests or databases, and then display the data on the screen. Because network requests and database queries are time-consuming operations, if they are performed in the main process, it will cause the App to freeze and the user experience will be extremely poor.

Therefore, at this time, you need to start a sub-thread to perform these time-consuming operations, and then update the UI interface after the time-consuming operations are completed.

At this time, you are excited to write code,

button.setOnClickListener {
    thread {
        ....          //耗时操作
        textView.text = date     //更新UI
    }
}

Execute the program, click the button, and find that the program crashes directly. Then this sentence appeared in the log

Only the original thread that created a view hierarchy can touch its views.

In other words, only the thread that created this Activity or Fragment can modify its view.

This is the same as many GUI libraries. UI operations are not thread-safe, so it is forbidden to modify UI content in sub-threads, and can only be modified in the thread that created it.

At this time, you are in trouble, so what should you do? At this time, you need to use the asynchronous message mechanism.

The role of the asynchronous message mechanism lies in the communication between different threads. That is to say, we can perform time-consuming operations in the child thread, wait for the operation to complete, and then use the asynchronous message mechanism to tell the main thread that I have got the data, and then the main thread will modify the UI. can solve our needs.

Then Android provides us with such a mechanism, which consists of the following parts:

  • Message

    messages passed between threads

  • Handler

    Message handlers for sending and receiving messages.

  • MessageQueue

    Message queue, messages sent by Handler will be stored in this queue. A thread has only one message queue.

  • Looper

    As the name suggests, Looper is a loop. He is the manager of MessageQueue in each thread, responsible for taking out messages and sending them to handle. A thread has only one Looper

The whole workflow is as follows:

2. Introduction to asynchronous message mechanism

We have mentioned the main members of the asynchronous message mechanism before, and here we will briefly apply it to see how the asynchronous message mechanism is used.

First, we add a TextView user display text in the layout of the Activity, which is also the UI component we want to modify, and then write a Button to start a thread.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ThreadUpdateUiActivity">
    <Button
        android:id="@+id/btn_changeUI"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="修改内容"/>
    <TextView
        android:id="@+id/tv_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="this is a TextView"
        android:gravity="center_horizontal"
        android:textSize="20sp"
        android:textColor="@color/black"/>
</LinearLayout>

Then go to the code of Activity to add an asynchronous message mechanism.

class HandlerUpdateUiActivity : AppCompatActivity() {
    val updateText = 1
    val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            when(msg.what) {
                updateText -> {
                    tv_content.text = "我收到了消息"
                }
            }
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_thread_update_ui)
        btn_changeUI.setOnClickListener {
            thread {
                val msg = Message()
                msg.what = updateText
                handler.sendMessage(msg)
            }
        }
    }
}

First, we define a member variable handler, and rewrite the handleMessage() method in it. In this method, the intent of the message is determined according to the received parameter msg.what, and then the corresponding operation is performed.

    val updateText = 1
    val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            when(msg.what) {
                updateText -> {
                    tv_content.text = "我收到了消息"
                }
            }
        }
    }

Then in the click event of the button, we create a Message object and set the variable msg.what to the udateText defined at the beginning. Finally call handler.sendMessage() to send the event.

The final running effect is that after clicking the button, the content of the TextView changes.

After clicking ->

That's it for the basic introductory usage. Maybe you still have questions, why don't you see Looper and MessageQueue, and how they work. Then it's time to go to the source code to analyze it.

3. Source code analysis

1.Message

Asynchronous message mechanism, let's first look at what the message is made of.

1.1 Message content

public final class Message implements Parcelable {
    public int what;
    public int arg1;
    public int arg2;
    public Object obj;
    
    ...
    
    Bundle data;
    ...   
}

Let's take a look at five important member variables.

  • what

    This variable we used in the previous example, its role is to let the receiver know what your message is about.

  • arg1、arg2

    If your message only needs to transmit one or two int variables during transmission, then you can directly use these two variables to assign and obtain values.

  • obj

    If you need to transfer an object, you can use this variable to store it. Note, however, that this object needs to be serialized through Parcelable.

  • data

    This is a Bundle object. Using Bundle, you can store all the data you want to transfer in it, and then transfer it.

Using these five variables, you can pass data in Message.

The method of setting data is to call setDate()

    public void setData(Bundle data) {
        this.data = data;
    }

There are two methods for obtaining, one is getData() and the other is peekData().

    public Bundle getData() {
        if (data == null) {
            data = new Bundle();
        }
        return data;
    }
    public Bundle peekData() {
        return data;
    }

It can be clearly seen from the source code that getData() must be able to obtain a Bundle object, and peekData() may obtain null.

1.2. Processing messages

There is also a Handler variable and a Runnable variable inside the Message

    @UnsupportedAppUsage
    Handler target;
    @UnsupportedAppUsage
    Runnable callback;

The target is easy to understand, which handler this message will eventually be sent to.

The callback will be discussed together with the handler's processing of the message. Now only the callback that knows the Message can process the message by itself.

1.3. Caching mechanism

Imagine that there must be many places in our App that use the asynchronous message mechanism to deliver messages. If we go to a new Message every time, it will cause memory waste. As for the message object, in fact, as long as it is passed to it, the handler performs the corresponding operation. Then these Messages can be cached for later use.

Officially, there is such a sentence before the Message class:

  • ​While the constructor of Message is public, the best way to get
  • one of these is to call {@link #obtain Message.obtain()} or one of the

  • {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull

  • them from a pool of recycled objects.</p>

The translation is that although the constructor of Message is public, the best way to obtain a Message is to obtain it through Message.obtain() or handler.obtainMessage(), which will be obtained from the buffer pool.

Then let's take a look at the source code of Message.obtain(). Let's take a look at the related member variables

    
    @UnsupportedAppUsage
    //  Message可以被组成一个链表,而缓存池就是一个链表结构
    /*package*/ Message next;
    /** @hide */
    public static final Object sPoolSync = new Object();
    // 缓存池
    private static Message sPool;
    // 缓存池的大小
    private static int sPoolSize = 0;
    // 缓存池的最大长度
    private static final int MAX_POOL_SIZE = 50;
    // 该版本系统是否支持回收标志位
    private static boolean gCheckRecycle = true;

Then you can look at the source code of obtain().

    public static Message obtain() {
        // 同步锁 保证线程安全
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

The source code is also easier to understand. First of all, the buffer pool is actually a linked list structure, and the variable sPool is the head node of the linked list.

If the cache pool is not empty, then take out the head node and return; if the cache pool is empty, then return a new object.

So now the question is, how does Message join the cache pool? Continue reading the source code and find the recycle() method

     public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

First he will enter isInUse() to determine whether the message is in use

    boolean isInUse() {
        return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
    }

Then an exception is thrown if the message is in use, and the message cannot be recycled because it is in use.

If it is not in use, it will enter recycleUnchecked().

   void recycleUnchecked() {
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

You can see that in this function, all the member variables that can be empty are assigned to null. Then enter the synchronization lock, if the size of the buffer pool does not exceed the maximum value, then put this message at the head node of the buffer pool.

The whole caching mechanism and process is relatively easy to understand.

For ease of use, Message also provides a series of obtain() methods with parameters to meet various needs and assign values ​​to some member variables.

    // 基本上都是你传入什么参数,就帮你给相应的成员变量赋值
    public static Message obtain(Message orig)
    public static Message obtain(Handler h)
    public static Message obtain(Handler h, Runnable callback)
    public static Message obtain(Handler h, int what)
    public static Message obtain(Handler h, int what, Object obj)
    public static Message obtain(Handler h, int what, int arg1, int arg2)
    public static Message obtain(Handler h, int what, int arg1, int arg2, Object obj)

1.4. Summary

This part analyzes the composition of Message, how to carry data transmission, and the cache mechanism of Message, using linked list as cache pool.

Questions that have not been resolved in this part: When will the Message be recycled? What is the use of the member variable callback.

2.Handler and Looper

Because these two are closely related, I plan to analyze them together.

2.1. Looper in Handler

Let's first look at the constructor of Handler


    public Handler(@Nullable Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

It is worth noting that

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }

Here the handler needs to obtain a Looper of its own. If it cannot obtain it, an exception will be thrown.

Then enter Looper.myLooper() to take a look

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

The variable sThreadLocal is of type ThreadLocal.

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

ThreadLocal allows us to create variables, each thread accesses its own variables.

Looper creates variables in prepare

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));
}

This method ensures that there is only one Looper in each thread.

So this means that before we create a Handler, we should call Looper.prepare() to create a Looper for this thread, otherwise an exception will be thrown! This is why we must call Looper.prepare() when creating a handler in a child thread.

Some people say that it is not called in the main thread. This is because the system has already called it for us when the main program starts, so there is no need to call it in the main thread.

public static void main(String[] args) {
    SamplingProfilerIntegration.start();
    CloseGuard.setEnabled(false);
    Environment.initForCurrentUser();
    EventLogger.setReporter(new EventLoggingReporter());
    Process.setArgV0("<pre-initialized>");
    Looper.prepareMainLooper();             //这一行!!!
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    AsyncTask.init();
    if (false) {
        Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}
public static final void prepareMainLooper() {
    prepare();
    setMainLooper(myLooper());
    if (Process.supportsProcesses()) {
        myLooper().mQueue.mQuitAllowed = false;
    }
}

Finally, prepare() will be called here.

However, the no-argument constructor of Handler is not recommended.

/**
     * Default constructor associates this handler with the {@link Looper} for the
     * current thread.
     *
     * If this thread does not have a looper, this handler won't be able to receive messages
     * so an exception is thrown.
     *
     * @deprecated Implicitly choosing a Looper during Handler construction can lead to bugs
     *   where operations are silently lost (if the Handler is not expecting new tasks and quits),
     *   crashes (if a handler is sometimes created on a thread without a Looper active), or race
     *   conditions, where the thread a handler is associated with is not what the author
     *   anticipated. Instead, use an {@link java.util.concurrent.Executor} or specify the Looper
     *   explicitly, using {@link Looper#getMainLooper}, {link android.view.View#getHandler}, or
     *   similar. If the implicit thread local behavior is required for compatibility, use
     *   {@code new Handler(Looper.myLooper())} to make it clear to readers.
     *
     */
    @Deprecated
    public Handler() {
        this(null, false);
    }

Because the implicit selection of Looper may result in lost operations, crashes, or races. Therefore, it is recommended to specify the Looper method to create the Handler.

    public Handler(@NonNull Looper looper) {
        this(looper, null, false);
    } 

    public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

So in the previous example, the way to create a Handler is

val handler = object : Handler(Looper.getMainLooper())

2.2.Handler sends messages

As mentioned in the previous introductory example, the handler sends messages through the sendMessage() method. In fact, there are many ways to send messages, such as

sendMessageDelayed(@NonNull Message msg, long delayMillis)   //在指定毫秒后发送消息

But these methods will eventually go to the sendMessageAtTime() method.

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

First of all, the member variable mQueue is obtained here, which is the message queue. If it is empty, an exception will be thrown; if it is not empty, it will go to enqueueMessage()

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

The parameters of the function are well understood. The first is the message queue, the second msg is the message to be delivered, and the third is the delayed sending time.

This message will enter the queue through queue.enqueueMessage() . Here we will not analyze the source code of MessageQueue for the time being, we only need to know that this message has now entered the queue.

2.3.Looper takes out the message

As mentioned earlier, Looper is the steward of MessageQueue, and he naturally goes to the message queue to take out messages.

And the method of taking out the message is in loop()

    public static void loop() {
        final Looper me = myLooper();
        ...    
        final MessageQueue queue = me.mQueue;
        ...    
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                return;
            }
            ...   
            try {
                msg.target.dispatchMessage(msg);
                ...
            } catch (Exception exception) {
                ...
                throw exception;
            } finally {
                ...
            }
           ...
            msg.recycleUnchecked();
        }
    }

It can be seen that an infinite loop is opened in this method, and messages are continuously taken out from the message queue.

If the fetched message is empty, or if the fetched message does not know who to send it to, do nothing.

Otherwise, it will go to msg.target.dispatchMessage(msg); This is how the handler handles the message.

At the end of the method, we can also see msg.recycleUnchecked(); that is, after processing, here, msg is recycled. It corresponds to the Message analyzed earlier.

When analyzing here, I have a problem . Looper.loop() is an infinite loop, and the main thread will create its own Looper object, indicating that Looper.loop() is an infinite loop in the main thread. Will such a design not cause blocking? The answer is given at the end of the article.

2.4.Handler processing messages

After the previous Looper fetches the message, it will call the dispatchMessage() method of the handler.

    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

A member variable of Runnable callback was mentioned in Message earlier. You can see his role here. If you set a callback in the Message, you will end up here.

    private static void handleCallback(Message message) {
        message.callback.run();
    }

In other words, Message can open a sub-thread to handle some things by itself, not necessarily a handler.

If Message doesn't handle it by itself, then Handler handles it by itself.

First, there is an interface inside the Handler

    public interface Callback {
        boolean handleMessage(@NonNull Message msg);
    }

This interface only needs to implement a handleMessage method, which is to process messages. But this requires a boolean return value. Through the dispatchMessage() source code, it can be seen that if the handleMessage() of the callback returns false, then the handler itself can continue to process the message; if it returns true, the handler itself cannot process it.

Is this mechanism very familiar? There is also such a mechanism in event distribution.

2.5. Summary

This part explains why the child thread needs to call Looper.prepare() first to create a handler, and how the Handler and Looper cooperate to make the message pass and be processed.

Now look at this picture again, will it be clearer?

The whole principle of the asynchronous message mechanism is relatively clear so far, and the source code of MessageQueue is not analyzed here due to space reasons.

Fourth, solve the problem

Why does the Hander send a message in the child process, and finally the process where the handler is located will process the message?

First of all, because there is only one Looper and MessageQueue in one process, in which process the Handler is created, the process should have its own Looper and MessageQueue. When the Handler sends a message, it puts the message into the MessageQueue in the thread it created, and finally the Looper of the thread takes it out and sends it to the Handler.

So no matter which thread the handler sends the message on, it will process the message in the thread that created it.

Looper.loop() is an infinite loop in the main process, won't it cause blocking?

First of all, why do you need an infinite loop. Our App keeps running while it is running. If there is no such infinite loop, the program will end and exit. So an infinite loop is necessary. Android itself is event-driven, and every click event or Activity life cycle is under the control of Looper. So in fact, our code is executed based on this infinite loop. If it stops, the application will also stop.

So on this basis, there will be no blockage.

V. Summary

This article analyzes the mechanism of Android asynchronous messages from entry to source code analysis.

Message structure and caching mechanism, Handler and Looper collaboration.

I hope it will be of some help to readers' understanding.

If wrong, kindly point out.

Guess you like

Origin blog.csdn.net/qq_43478882/article/details/123443082