Audio and video development: Android message mechanism (Looper Handler MessageQueue Message)

catalogue

  1. Android message mechanism process

  2. Handler

  3. Message

  4. MessageQueue

  5. Looper

  6. HandleThread

Foreign language

In addition to the "audio and video development journey series", take time to learn and strengthen your weak foundation of Java & Android, which is more in line with your inner pursuit and self expectation. Start another learning journey in parallel, start from the Handler message mechanism, and conduct learning and analysis in combination with the process and source code of the message mechanism.

1, Android message mechanism process

Let's first have an understanding of the Android message mechanism process and the relationship between key classes through the following two diagrams, and then analyze them one by one in combination with the source code.

Flow of message mechanism

Relationship among Handler, Message, MessageQueue and Looper

2, Handler

Handler has two main purposes:

  1. The scheduling message is executed at a certain point in time;

  2. Communication between different threads

2.1 global variables

 final Looper mLooper;     //There is a reference to Looper
 final MessageQueue mQueue;//There is a reference to MessageQueue
 final Callback mCallback;
 final boolean mAsynchronous;
 IMessenger mMessenger;

2.2 construction method

    public Handler() {
        this(null, false);
    }
    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;
    }

2.3 get Message

//Get a Message from the Message reuse pool   
 public final Message obtainMessage()
    {
        return Message.obtain(this);
    }

//It is basically the same as the above method. The difference is that after getting the Message from the reuse pool, assign a value to what
    public final Message obtainMessage(int what)
    {
        return Message.obtain(this, what);
    }
//... Other obtainmessages are similar

2.4 sending messages

Let's pick a few sending methods

sendMessage: Send a Message, when is the current time
MessageQueue matches the insertion position according to when

    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

   public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        ......
        return enqueueMessage(queue, msg, uptimeMillis);
    }

post: get the Message from the Message reuse pool and set the Callback of the Message

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

postAtFrontOfQueue(): inserts a message into the queue header

By calling sendMessageAtFrontOfQueue, a message with a when of 0 is added to the queue, that is, it is inserted into the head of the queue. It should be noted that the insertion of MessageQueue#enqueueMessage into the linked list is based on the comparison of when (when < p.when). If there are multiple messages with a when equal to 0 in the queue, the new message will be added to the front and the back of when.

    public final boolean postAtFrontOfQueue(Runnable r)
    {
        return sendMessageAtFrontOfQueue(getPostMessage(r));
    }

    public final boolean sendMessageAtFrontOfQueue(Message msg) {
        MessageQueue queue = mQueue;
        ......
        //The third parameter is 0, that is, when of Message is 0, which is inserted into the head of the queue. Note that the insertion of MessageQueue#enqueueMessage into the linked list is based on when comparison (when < p.when). If there are multiple messages with when equal to 0 in the queue before, this new Message will be added to the front and after when is also 0.
        return enqueueMessage(queue, msg, 0);
    }

2.5 dispatch message

Priority is as follows:
Callback method of Message callback run() >
Callback method of Handler: mcallback handleMessage(msg) > default method of Handler handleMessage(msg)

public void dispatchMessage(@NonNull Message msg) {
      //The callback method of Message has the highest priority  
    if (msg.callback != null) {
            handleCallback(msg);
        } else {
            //The Handler's mCallBack takes precedence
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //Handler's handleMessage method has the lowest priority (most of them implement Message processing in this method)
            handleMessage(msg);
        }
    }

Follow me and get the latest and complete learning and improvement materials in 2022, including (C/C + +, Linux, FFmpeg, webRTC, rtmp, hls, rtsp, ffplay, srs)

 

3, Message

global variable

//Some important variables

    public int arg1;
    public int arg2;
    public Object obj;
    public long when;
    Bundle data;
    Handler target;  //There is a Handler reference in the Message
    Runnable callback;
    
    //Message has a next pointer, which can form a one-way linked list
    Message next;


    public static final Object sPoolSync = new Object();
    
    //The first Message in the reuse pool
    private static Message sPool;
    
    //The maximum size of the reuse pool is 50 by default (what happens if there are more than the maximum number of messages in the reuse pool in a short time, and re create new)
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 50;

Construction method
Check whether there is a message that can be reused. If so, reduce the number of reusable messages in the reuse pool by one and return the message; If there is no new one. Note that the default maximum number of reuse pools is 50.

   public static Message obtain() {
       synchronized (sPoolSync) {
           //Check whether there are message s that can be reused
           if (sPool != null) {
               //Take out the first Message
               Message m = sPool;
               sPool = m.next;
               m.next = null;
               m.flags = 0; // clear in-use flag
               //The number of reusable messages in the reuse pool is reduced by one
               sPoolSize--;
               return m;
           }
       }
       //If there is no Message in the reuse pool, restart new
       return new Message();
   }

recycleUnchecked
//When marking a Message, it is an asynchronous Message. Normally, it is a synchronous Message. When encountering the synchronization barrier, the first asynchronous Message will be executed first. We will introduce the synchronization barrier in MessageQueue in combination with next and other methods.

public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }


void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        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) {
            //There are 50 messages that can be reused. If more than 50 messages are reused, they will not be reused
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

//toString and dumpDebug can Dump message messages and help analyze some problems
android.os.Message#toString(long)

android.os.Message#dumpDebug

4, MessageQueue

MessageQueue is a single linked list priority queue
Message cannot be directly added to MessageQueue, but should be added through Handler and corresponding Looper.

variable

//The first Message in the MessageQueue linked list
Message mMessages;

Next: fetch the next message to be executed from the message queue

If it is a synchronous barrier message, find the first asynchronous message in the first queue
If the execution time of the first Message is later than the current time, record how long it will take to start execution; Otherwise, find the next Message to execute.
The loop method of the latter Looper will pass through the queue Next call this method to get the next Message to be executed. The blocked native method, nativePollOnce, will be called. This method is used to "wait" until the next Message is available If the time spent during this call is very long, it indicates that the corresponding thread has no actual work to do, so ANR will not appear. ANR has nothing to do with this.

The key codes are as follows:

Message next() {

        //Pointer to MessageQueue in native layer
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

     ......
        for (;;) {
           
            //Blocking operation, when waiting for nextPollTimeoutMillis for a long time, or the message queue is awakened
            //nativePollOnce is used to "wait" until the next message is available If the time spent during this call is very long, it indicates that the corresponding thread has no actual work to do, so ANR will not appear. ANR has nothing to do with this.
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                
                //Create a new Message to point to the header of the current Message queue
                Message msg = mMessages;
                
                //If it is a synchronous barrier message, find the first asynchronous message in the first queue
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                
                if (msg != null) {
                    //If the execution time of the first Message is later than the current time, how long will it take to record the execution
                    if (now < msg.when) {                    
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //Otherwise, take the current Message from the linked list and point to the next Message in the linked list
                        
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        //Get the value of the current Message and leave next blank
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } 
                .....

                //android. os. Mquiting is true when messagequeue #quit
                //If you need to exit, execute immediately and return a null message, Android os. Looper. Loop exits the loop loop after receiving a null message
                if (mQuitting) {
                    dispose();
                    return null;
                }
        ......
            if (pendingIdleHandlerCount <= 0) {
                                // Note here that if there is no message to be executed, mBlocked is marked as true, and enqueueMessage will judge whether to call nativeWake wake-up according to this mark
                                mBlocked = true;
                                continue;
                            }
        ......
    }
    ......
}

enqueueMessage: inserts a Message into the Message queue

If the Message linked list is empty or the inserted Message is executed earlier than the first Message in the Message linked list, it is directly inserted into the header
Otherwise, find a suitable place to insert in the linked list. Generally, it is not necessary to wake up the event queue, except for the following two cases:

  1. There is only one Message just inserted in the Message chain list, and mBlocked is true, that is, it is blocking and wakes up after receiving a Message

  2. The header of the linked list is a synchronization barrier, and this message is the first asynchronous message

Wake up who? MessageQueue. Blocked nativePollOnce in next

The specific implementation is as follows:,

About how to find the right place? This involves the insertion algorithm of the linked list: introduce a prev variable, which points to P also message (if it is the first internal execution of the for loop), then move p to the next and compare it with the message to be inserted when

The key codes are as follows:

boolean enqueueMessage(Message msg, long when) {
        ......

        synchronized (this) {
          
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            
            //If the Message linked list is empty or the inserted Message is executed earlier than the first Message in the Message linked list, it is directly inserted into the header
            if (p == null || when == 0 || when < p.when) {
                
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
               
                //Otherwise, find a suitable place to insert in the linked list
                //Generally, there is no need to wake up the event queue unless the head of the linked list is a synchronization barrier and the message is the first asynchronous message
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                //The specific implementation is as follows, which is illustrated by this picture
                //The linked list introduces a prev variable, which points to p message (if it is the first internal execution of the for loop), then moves p to the next and compares it with the message to be inserted when
                Message prev;
                
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; 
                prev.next = msg;
            }

            //If an asynchronous message is inserted and the first message in the message chain is a synchronous message.
    //Or there is only one Message just inserted in the Message chain list, and mBlocked is true, that is, it is blocking and wakes up after receiving a Message
 Wake up who? MessageQueue.next Blocked in nativePollOnce
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

Take a brief look at the epoll of native (this has not been analyzed in depth, and it will be added in the following chapters)

nativePollOnce and nativeWake use epoll system call, which can monitor IO events in file descriptors nativePollOnce calls epoll on a file descriptor_ Wait, while nativeWake writes an IO operation to the descriptor
Epoll belongs to IO multiplexing mode call, and epoll is called_ Wait Then the kernel takes the epoll waiting thread from the waiting state, and the thread continues to process new messages

removeMessages: removes the corresponding message from the message chain list
It should be noted that the implementation of this function is divided into the removal of header meg and the removal of non header msg.
Remove the same msg as the header in the message linked list
eg: msg1.what=0;msg2.what=0;msg3.what=0; msg4.what=1; You need to remove the MSG whose what is 0, that is, remove the first three

Remove the corresponding non header messages in the message chain list, eg: MSG1 what=1; msg2. what=0; msg3. what=0; It is necessary to remove the message with what 0, that is, to remove the subsequent messages, which reflects the query and removal algorithm of the linked list everywhere

The key codes are as follows:

void removeMessages(Handler h, int what, Object object) {
  ......
        synchronized (this) {
            Message p = mMessages;

           
            //Remove the same msg eg: MSG1 from the header in the message link list What=0; msg2. what=0; msg3. what=0;  msg4. what=1;  You need to remove the MSG whose what is 0, that is, remove the first three
            while (p != null && p.target == h && p.what == what
                   && (object == null || p.obj == object)) {
                Message n = p.next;
                mMessages = n;
                p.recycleUnchecked();
                p = n;
            }

         
            //Remove the corresponding non header messages in the message chain list, eg: MSG1 what=1; Msg2 what=0; msg3. what=0;  It is necessary to remove the message with what 0, that is, to remove the subsequent messages, which reflects the query and removal algorithm of the linked list everywhere
            while (p != null) {
                Message n = p.next;
                if (n != null) {
                    if (n.target == h && n.what == what
                        && (object == null || n.obj == object)) {
                        Message nn = n.next;
                        n.recycleUnchecked();
                        p.next = nn;
                        continue;
                    }
                }
                p = n;
            }
        }
    }

postSyncBarrier: send synchronization barrier message

The synchronization barrier is also a message, but the target of this message is null Send synchronization barrier message via ViewRootImpl#scheduleTraversals()
The insertion position of synchronization barrier messages is not always the header of the message linked list, but depends on when and other information: if when is not 0 and the message linked list is not empty, find the position where the synchronization barrier is to be inserted in the message linked list; If prev is empty, the synchronization message is inserted into the head of the queue.

The key codes are as follows:

 /**
     * android.view.ViewRootImpl#scheduleTraversals()Send synchronization barrier message
     * @param when
     * @return
     */
    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;

            //The synchronization barrier is also a message, but the target of this message is null

            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                //If when is not 0, the message linked list is not empty. Find the location where the synchronization barrier should be inserted in the message linked list
                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 {
                //If prev is empty, the synchronization message is inserted into the head of the queue
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

dump: MessageQueue information
Sometimes we need to dump the Message information of the current looper to analyze some problems. If there are many messages in the Queue, it will affect the execution of the messages behind the Queue, which may cause slow logical processing and even ANR. The default reuse pool of MessageQueue is 50. If there are too many queued messages, it will also affect the performance. dump Message information can help analyze. mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);

 void dump(Printer pw, String prefix, Handler h) {
        synchronized (this) {
            long now = SystemClock.uptimeMillis();
            int n = 0;
            for (Message msg = mMessages; msg != null; msg = msg.next) {
                if (h == null || h == msg.target) {
                    pw.println(prefix + "Message " + n + ": " + msg.toString(now));
                }
                n++;
            }
            pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
                    + ", quitting=" + mQuitting + ")");
        }
    }

5, Looper

Looper mainly involves several important methods of construction, prepare and loop. In the design of ensuring that a thread has and only one looper, ThreadLocal and code logic control are adopted.

variable

//Some important variables  
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    final MessageQueue mQueue;

    final Thread mThread;

Construction method
When constructing a Looper, create a MessageQueue corresponding to the Looper one by one

    private Looper(boolean quitAllowed) {
        //When constructing Looper, new corresponds to MessageQueue one by one
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

prepare
Here we can see how the message mechanism ensures that a thread has only one Looper.

//The quitAllowed parameter allows quits. The Looper of the UI thread is not allowed to exit, and others are allowed to exit
private static void prepare(boolean quitAllowed) {
    //Ensure that a thread can only have one Looper, and sThreadLocal here
    if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

loop
We have analyzed nativePollOnce in the next method of MessageQueue. This method may block until we get the message.
If next returns a null Message, exit the Looper loop, otherwise send msg.
After the extracted msg is executed, it will be added to the recycling pool for reuse. Recycled unchecked we have also analyzed it in Message. If you don't know, you can look back.

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
    ......
    for (;;) {
            //The next method is a method that will block. We have analyzed nativePollOnce in the next method of MessageQueue. This method may block until we get the message.
            Message msg = queue.next(); 
            //When an empty msg is received, the Loop loop exits. So when will I receive an empty msg? quit
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            //Distribution of msg, msg Target is the Handler, that is, calling the dispatcher's dispatchMessage to dispatch the message
            msg.target.dispatchMessage(msg);

            ......
            //msg recycling
            msg.recycleUnchecked();
        }

Follow me and get the latest and complete learning and improvement materials in 2022, including (C/C + +, Linux, FFmpeg, webRTC, rtmp, hls, rtsp, ffplay, srs)

 

6, HandleThread

HandlerThread is a Thread with Looper.

global variable

public class HandlerThread extends Thread {
    int mPriority;//thread priority 
    int mTid = -1;//Thread id
    Looper mLooper;
    private Handler mHandler;
    ......
}

Construction method

    public HandlerThread(String name) {
        super(name);
        //Used to set the priority of threads when run ning process setThreadPriority(mPriority);

        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }

    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }

run method
Call loop prepare and loop, and configure the loop environment

    @Override
    public void run() {
        //Thread id
        mTid = Process.myTid();
        //Call the prepare method of Looper to add the unique Looper associated with the current thread to sThreadLocal
        Looper.prepare();
        
        synchronized (this) {
            //Get Looper from sThreadLocal
            mLooper = Looper.myLooper();
            
            notifyAll();
        }
        //Set the priority of the thread. The default is THREAD_PRIORITY_DEFAULT. If it is a background business, it can be configured as thread_ PRIORITY_ Backgroup, which is set according to the specific scene
        Process.setThreadPriority(mPriority);
        //You can do some preset operations
        onLooperPrepared();
        //Start looper loop
        Looper.loop();
        mTid = -1;
    }

The general process of using HandlerThread is as follows

// Step 1: create and start the HandlerThread thread, which contains Looper
HandlerThread handlerThread = new HandlerThread("xxx");
handlerThread.start();

// Step 2: create Handler
Handler handler = new Handler(handlerThread.getLooper());

handler.sendMessage(msg);

One disadvantage of this is that new HandlerThread is required every time the Handler is used, and the Thread takes up more memory,
Can we reduce the creation or reuse of threads
And realize that the Message can be executed in time without being blocked by the Message in front of the queue;
This is really a very interesting and challenging thing.

Tags: webrtc

Posted by CroNiX on Fri, 15 Apr 2022 18:24:45 +0930