Android:Android messaging mechanism collation

Posted by Pino on Mon, 07 Feb 2022 18:28:35 +0100

Preface

Organize Android messaging mechanisms to help you comb the content of Android messaging mechanisms

I. Composition of Android Message Mechanism

The Android messaging mechanism is actually the operating mechanism of Handler(). We often use Handler() to interact with other tasks during the development process, such as opening a sub-thread to finish pulling data from a database, and then notifying the main thread to make UI updates. Handler() is used to deliver messages from the sub-thread to the main thread for processing. Handler() is actually the interface between Android messaging mechanism and the upper layer. To understand Android messaging mechanism, we need to understand the underlying implementation of Handler()

The underlying mechanism of Handler()

  • MessageQueue message queues, which store a set of messages, then provide insert and delete operations to the outside as queues, which, in other words, store messages to be processed, but the underlying implementation of MessageQueue is not a queue, but a single-chain table
  • Looper, acting as an operator, MessageQueue is just a data structure that stores messages to be processed. Looper loops wirelessly to find out if there are new messages to process, processes them, and waits for them
  • ThreadLocal, a concept of ThreadLocal that is part of Looper, is not a thread. ThreadLocal can provide data among multiple threads without interruption, and ThreadLocal can easily get each thread's Looper. Also note that the default child thread does not have a Looper. If you want to use a Handler in a child thread, you must create a Looper for the child thread. The main thread (ActivityThread) ->UI thread initializes the Looper when it is initialized, which is why you can use the Handler in the main thread by default

So the Android messaging mechanism is actually how Handler works and how MessageQueue and Looper work with Handler

2. Why only the main thread is allowed to update the UI

Android UI controls are not thread-safe, and concurrent access in multiple threads can easily result in an unexpected state of the UI controls, so the UI is updated using a single-threaded model (the main thread)

3. Specific Analysis of Message Mechanism

ThreadLocal Principle Analysis

ThreadLocal is a data storage class within a thread through which data can be stored to a specified thread. After the data is stored, only the specified thread can get the data, and the same ThreadLocal object can make copies of the data in different threads, that is, you pass the same ThreadLocal object into different threads and the results returned by the get() method are different.

Different threads have different Looper objects. For Handler, you need to get the Looper of the current thread, and you can easily get the Looper object of the current thread through a ThreadLocal class (different threads have different Looper objects)

To understand why the same ThreadLocal object has duplicates in different threads, let's look at the get() and set() method source code for the ThreadLocal object
Note: In different threads, call the set() method of the same ThreadLocal object to set different values; The ThreadLocal get() method is then called on different threads, and the return values are different

  • First look at the set() method
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

	/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

	static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

Looking at this set process, a ThreadLocal call to the set method goes through roughly the following process:

  • Get the current thread
  • Get threadLocals storage inside the current thread through the current thread, threadLocals is a static ThreadLocalMap class, here is a class defined internally that inherits from WeekReference, which is understood as a special Map class as a whole, where the stored key-value pair key is the thread, and value is the value corresponding to a thread
  • If the map is not empty, call the map's set() method to place the thread-value key-value pair in the ThreadLocalMap class; Empty, create a map, put the value in

Let's look again at the get() method

	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

	private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

Having read the set() method, the get() method is much simpler

  • Get Current Thread
  • Get the ThreadLocalMap stored by the current thread (it's actually a static class, it's the same everywhere)
  • Pass in the current thread as key and get value
  • return value;
  • If the map is empty, initialization occurs and the initialized value is returned, depending on what the ThreadLocal generic implements.

Now that each Thread can come up with a corresponding unique value as a key, imagine if each thread can throw in a unique Looper class?

MessageQueue

MessageQueue mainly corresponds to two operations, insert and delete:

  • enqueueMessage Inserts a message into the queue
  • next takes a message from the queue and deletes it from the queue
	boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }

            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    
@UnsupportedAppUsage
    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }//block

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

Take a look at the enqueueMessage method

  • Verify Legality Input
  • Verify that the current MessageQueue is exiting
  • Synchronize operations to keep threads safe: one to lock MessageQueue and one to mark InUse()
  • If not empty, perform head interpolation of single-chain lists
  • If empty, create the insert and wake up the pinch

next method

  • Using a for causes an infinite loop, return messages jump out of the loop when there is a message in the queue and move the message out (prev.next=msg.next), msg.target is the Handler object that sends this message; If no message exists, the method is called to block

Looper

Looper plays the role of a message loop, constantly viewing messages from the MessageQueue and executing them as soon as new ones are available, otherwise they will be blocked there
The following shows some of the important sources

	private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }



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

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


	/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    @SuppressWarnings("AndroidFrameworkBinderIdentity")
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        if (me.mInLoop) {
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }

        me.mInLoop = true;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        me.mSlowDeliveryDetected = false;

        for (;;) {
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

	 private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return false;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what);
        }
        // Make sure the observer won't change while processing a transaction.
        final Observer observer = sObserver;

        final long traceTag = me.mTraceTag;
        long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
        if (thresholdOverride > 0) {
            slowDispatchThresholdMs = thresholdOverride;
            slowDeliveryThresholdMs = thresholdOverride;
        }
        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

        final boolean needStartTime = logSlowDelivery || logSlowDispatch;
        final boolean needEndTime = logSlowDispatch;

        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }

        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        Object token = null;
        if (observer != null) {
            token = observer.messageDispatchStarting();
        }
        long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
        try {
            msg.target.dispatchMessage(msg);//Distributed
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (logSlowDelivery) {
            if (me.mSlowDeliveryDetected) {
                if ((dispatchStart - msg.when) <= 10) {
                    Slog.w(TAG, "Drained");
                    me.mSlowDeliveryDetected = false;
                }
            } else {
                if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                        msg)) {
                    // Once we write a slow delivery log, suppress until the queue drains.
                    me.mSlowDeliveryDetected = true;
                }
            }
        }
        if (logSlowDispatch) {
            showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }

        msg.recycleUnchecked();

        return true;
    }

Let's analyze it below

  • The message queue is created in the constructor and the current thread is obtained
  • A new Looper was created in the prepare() method, and the sThreadLocal variable was thrown in. We then verified in the source that this variable is ThreadLocal, which means that what we have previously validated, using the current thread as the key and the Looper object as the value, works perfectly well, as described earlier.
  • The loop() method is also essentially an infinite loop, passing into Looper itself, a long value, an int value. Call the loopOne method and exit when the return value is false
  • The loopOnce() method simply takes out a message from the message queue and decides if it is empty, returns false when it is empty, ends the external loop method, and lets MSG when it is not empty. Target (Handler object) for distribution; But the next() method is actually a blocking method. When the message queue is empty, it does not return null, it only blocks. When does the MSG returned by the message queue become empty, Looper calls the quit() method to force the msg to be null and exit

Handler

Here's how Handler works
Let's start with sendMessage()

/**
     * Pushes a message onto the end of the message queue after all pending messages
     * before the current time. It will be received in {@link #handleMessage},
     * in the thread attached to this handler.
     *  
     * @return Returns true if the message was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.
     */
    public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }
	
	public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    
    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);
    }
	
	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);
    }

It's just a few steps

  • See if delay action is required
  • Create a message and set the target of the message to itself
  • Insert into Message Queue

Then the next() method of MessageQueue starts working, hands in a message to Looper, then Looper starts processing, and calls the dispatchMessage() method

/**
     * Handle system messages here.
     */
    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();
    }
    
	/**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(@NonNull Message msg) {
    }

handleCallback() actually calls msg's callback object, which is actually the Runnable object that we added during the process of using Handler. If this step is completed, it return s. If not, we call handleMessage, which is actually a call back we implemented. That's how we rewrite that handleMessage from our new Handler(callback)

Message Loop for Main Thread

The main thread has its own MessageQueue through Looper.prepareMainLooper() creates Looper and MesageQueue, and the looper() method opens the main thread message loop, and the internal Handler is ActivityThread.H, responsible for communication with ApplicationThread and AMS

summary

This is the preliminary sorting out of the current Android message mechanism, the author puts forward his own understanding of the source code step by step, hope to help you

Topics: Android Design Pattern UI