Android Handler interview summary

xiangzhihong
中文

In the Android interview, the interview about Handler is an inseparable topic. Let's make a summary of the interview about Handler.

1, the relationship between Handler, Looper, MessageQueue, and threads

  • A thread can only have one Looper object, so there is a one-to-one correspondence between threads and Loopers.
  • The MessageQueue object is created when the new Looper is created, so Looper and MessageQueue correspond one-to-one.
  • The role of the Handler is only to add the message to the MessageQueue, and then after the message is taken out, it is distributed to the original handler according to the target field of the message, so the Handler can be many-to-one for the Looper, that is, multiple Hanlder objects can use the same One thread, the same Looper, the same MessageQueue.

To sum up, Looper, MessageQueue, and thread are one-to-one correspondence, and they and Handler can be one-to-many.

2. Why does the main thread not initialize Looper?

Because the application has initialized a main thread Looper during the startup process. Every java application has a main method entry, and Android is a Java-based program is no exception. The entry of the Android program is in the main method of ActivityThread. The code is as follows:

// 初始化主线程Looper
 Looper.prepareMainLooper();
 ...
 // 新建一个ActivityThread对象
 ActivityThread thread = new ActivityThread();
 thread.attach(false, startSeq);
 // 获取ActivityThread的Handler,也是他的内部类H
 if (sMainThreadHandler == null) {
 sMainThreadHandler = thread.getHandler();
 }
 ...
 Looper.loop();
 // 如果loop方法结束则抛出异常,程序结束
 throw new RuntimeException("Main thread loop unexpectedly exited");
} 

It can be seen that in the main method, the main thread Looper will be initialized first, a new ActivityThread object will be created, and then the Looper will be started, so that the Looper of the main thread will run when the program starts. And, we usually think that ActivityThread is the main thread, in fact it is not a thread, but the manager of the main thread operation.

3. Why is the Looper of the main thread an infinite loop, but it will not ANR

Because when Looper processes all messages, it will enter the blocking state, and when a new Message comes in, it will break the blocking and continue to execute.

First, let's take a look at what is ANR, ANR, the full name Application Not Responding. When I send a UI drawing message to the main thread Handler, and it is not executed after a certain period of time, an ANR exception is thrown. Let's answer again, why is the Looper of the main thread an infinite loop, but not ANR? The infinite loop of Looper is to execute various transactions cyclically, including UI drawing transactions. The Looper infinite loop indicates that the thread is not dead. If the Looper stops the loop, the thread ends and exits. The Looper's infinite loop itself is one of the reasons to ensure that the UI drawing task can be executed.

On this issue, we can also draw some conclusions as follows:

  • The real stuck operation is that the operation time is too long when a message is processed, resulting in dropped frames and ANR, not the loop method itself.
  • In addition to the main thread, there will be other threads to process and accept events from other processes, such as the Binder thread (ApplicationThread), which will accept events sent by AMS
  • After receiving the cross-process message, it will be handed over to the Handler of the main thread for message distribution. Therefore, the life cycle of Activity depends on the Looper.loop of the main thread. When receiving different Messages, corresponding measures are taken. For example, when msg=H.LAUNCH_ACTIVITY is received, the ActivityThread.handleLaunchActivity() method is called, and finally the onCreate method is executed.
  • When there is no message, it will be blocked in the nativePollOnce() method in the loop's queue.next() . At this time, the main thread will release the CPU resources and enter the sleep state until the next message arrives or a transaction occurs, so the infinite loop will not be particularly consumed. CPU resources.

4. How does the Message find the Handler to which it belongs and distribute it?

In the loop method, to find the Message to be processed, you need to call the following piece of code to process the message:

msg.target.dispatchMessage(msg);

So the message is handed over to msg.target for processing, so what is this target? Usually, you can find out by looking at the source of the target:

private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {
        msg.target = this;

        return queue.enqueueMessage(msg, uptimeMillis);
    }

When using Hanlder to send a message, msg.target = this will be set, so the target is the Handler that added the message to the message queue.

5. How does Handler switch threads

Messages are processed using Loopers from different threads. We know that the execution thread of the code is not determined by the code itself, but in which thread the logic of executing this code is called, or the logic of which thread is called. Each Looper runs on the corresponding thread, so the dispatchMessage method called by different Looper runs on the thread where it is located.

6. What is the difference between post(Runnable) and sendMessage

We know that there are two types of messages sent in Hanlder: post (Runnable) and sendMessage. First, let's take a look at the source code:

public final boolean post(@NonNull Runnable r) {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

 private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
   
public final boolean sendMessage(@NonNull Message msg) {
     return sendMessageDelayed(msg, 0);
   }

As you can see, the difference between post and sendMessage is that the post method sets a callback for Message. So, what is the use of this callback then? Let's go to the message processing method dispatchMessage to see:

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

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

It can be seen that if msg.callback is not empty, that is, when sending a message through the post method, the message will be handed over to this msg.callback for processing; if msg.callback is empty, that is, when sending a message through sendMessage, It will judge whether the current mCallback of Handler is empty, and if it is not empty, it will be handed over to Handler.Callback.handleMessage for processing.

So the difference between post(Runnable) and sendMessage lies in the processing method of subsequent messages, whether it is handed over to msg.callback or Handler.Callback or Handler.handleMessage.

7. How does Handler ensure that the concurrent access of MessageQueue is safe

Cyclic locking, with blocking wake-up mechanism. We found that MessageQueue is actually a [producer-consumer] model, Handler keeps putting in messages, and Looper keeps taking them out, which involves deadlock problems. If Looper gets the lock, but there is no message in the queue, it will keep waiting, and the Handler needs to put the message in, but the lock is held by the Looper and cannot enter the queue, which causes a deadlock. The solution to the Handler mechanism is to add a loop. Lock, the code is in the next method of MessageQueue:

Message next() {
 ...
 for (;;) {
 ...
 nativePollOnce(ptr, nextPollTimeoutMillis);
 synchronized (this) {
 ...
 }
 }
} 

We can see that his waiting is outside the lock. When there is no message in the queue, he will release the lock first, and then wait until it is woken up. This will not cause deadlock problems.

8. How is the blocking wake-up mechanism of Handler implemented?

The blocking wake-up mechanism of Handler is based on the blocking wake-up mechanism of Linux. This mechanism is also a pattern similar to the handler mechanism. Create a file descriptor locally, and then the party that needs to wait listens to the file descriptor. The party that wakes up only needs to modify the file, and the party waiting will receive the file to break the wake-up.

Reference: The blocking wake-up mechanism for Linux

9. What is the synchronization barrier of Handler

The so-called synchronization barrier is actually a Message, but it is inserted into the linked list header of the MessageQueue, and its target==null. The Message expedited message is implemented using a synchronization barrier. The synchronization barrier uses the postSyncBarrier() method.

public int postSyncBarrier() {
 return postSyncBarrier(SystemClock.uptimeMillis());
}
 private int postSyncBarrier(long when) {
 synchronized (this) {
 final int token = mNextBarrierToken++;
 final Message msg = Message.obtain();
 msg.markInUse();
 msg.when = when;
 msg.arg1 = token;
 Message prev = null;
 Message p = mMessages;
 // 把当前需要执行的Message全部执行
 if (when != 0) {
 while (p != null && p.when <= when) {
 prev = p;
 p = p.next;
 }
 }
 // 插入同步屏障
 if (prev != null) { // invariant: p == prev.next
 msg.next = p;
 prev.next = msg;
 } else {
 msg.next = p;
 mMessages = msg;
 }
 return token;
 }
} 

It can be seen that the synchronization barrier is a special target, that is, target==null. We can see that he does not assign a value to the target attribute, so what is the use of this target?

Message next() {
 ...
 // 阻塞时间
 int nextPollTimeoutMillis = 0;
 for (;;) {
 ...
 // 阻塞对应时间 
 nativePollOnce(ptr, nextPollTimeoutMillis);
 // 对MessageQueue进行加锁,保证线程安全
 synchronized (this) {
 final long now = SystemClock.uptimeMillis();
 Message prevMsg = null;
 Message msg = mMessages;
 /**
 *  1
 */
 if (msg != null && msg.target == null) {
 // 同步屏障,找到下一个异步消息
 do {
 prevMsg = msg;
 msg = msg.next;
 } while (msg != null && !msg.isAsynchronous());
 }
 if (msg != null) {
 if (now < msg.when) {
 // 下一个消息还没开始,等待两者的时间差
 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
 } else {
 // 获得消息且现在要执行,标记MessageQueue为非阻塞
 mBlocked = false;
 /**
 *  2
 */
 // 一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取
 if (prevMsg != null) {
 prevMsg.next = msg.next;
 } else {
 mMessages = msg.next;
 }
 msg.next = null;
 msg.markInUse();
 return msg;
 }
 } else {
 // 没有消息,进入阻塞状态
 nextPollTimeoutMillis = -1;
 }
 // 当调用Looper.quitSafely()时候执行完所有的消息后就会退出
 if (mQuitting) {
 dispose();
 return null;
 }
 ...
 }
 ...
 }
} 

Let's focus on the part of the code about synchronization barriers.

if (msg != null && msg.target == null) {
 // 同步屏障,找到下一个异步消息
 do {
 prevMsg = msg;
 msg = msg.next;
 } while (msg != null && !msg.isAsynchronous());
} 

If a synchronization barrier is encountered, the entire linked list will be traversed to find the Message marked as an asynchronous message, that is, isAsynchronous returns true, and other messages will be ignored directly, so the asynchronous message will be executed in advance. At the same time, the synchronization barrier will not be removed automatically. It needs to be removed manually after use, otherwise the synchronization message will not be processed.

10. Usage scenarios of IdleHandler

As mentioned earlier, when there is no message in MessageQueue, it will block in the next method. In fact, before blocking, MessageQueue will also do one thing, which is to check whether there is an IdleHandler. If there is, it will execute its queueIdle method.

IdleHandler looks like a Handler, but it is actually just an interface with a single method, also known as a functional interface.

public static interface IdleHandler {
 boolean queueIdle();
} 

In fact, there is a List in the MessageQueue that stores the IdleHandler object. When the MessageQueue has no Messages that need to be executed, it will traverse and call back all the IdleHandlers. So IdleHandler is mainly used to handle some lightweight work when the message queue is idle.

Therefore, IdleHandler can be used for startup optimization, such as putting some events (such as drawing and assignment of interface view) into the onCreate method or the onResume method. However, these two methods are actually called before the interface drawing, which means that the time-consuming of these two methods will affect the startup time to a certain extent, so we can put some operations into the IdleHandler, that is, the interface drawing is completed. It is called only after that, which can reduce the startup time.

11. HandlerThread usage scenarios

First, let's take a look at the source code of HandlerThread:

public class HandlerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
    }

As you can see, HandlerThread is a Thread class that encapsulates Looper, just to make it easier for us to use Handler in child threads. The locking here is to ensure thread safety, obtain the Looper object of the current thread, and then wake up other threads through the notifyAll method after the acquisition is successful. Where is the wait method called? The answer is the getLooper method.

public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }

        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
阅读 1.1k

著有《React Native移动开发实战》1,2、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台...

4.8k 声望
15k 粉丝
0 条评论

著有《React Native移动开发实战》1,2、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台...

4.8k 声望
15k 粉丝
文章目录
宣传栏