Android source code analysis: Based on Handler and Looper to intercept global crash, monitor ANR, etc.

Keywords: Android Java

I believe that many people will have a question about why we need to read the source code, but we can't use it at work. This is a great problem. We start from the use, and then analyze the implementation principle of these uses, so as to reflect the significance of reading the source code.

  1. Block global crash (main thread) based on Handler and Looper to avoid APP exit.
  2. The implementation of ANR monitoring based on Handler and Looper.
  3. Implement single thread pool based on Handler.

Implementation code

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        var startWorkTimeMillis = 0L
        Looper.getMainLooper().setMessageLogging {
            if (it.startsWith(">>>>> Dispatching to Handler")) {
                startWorkTimeMillis = System.currentTimeMillis()
            } else if (it.startsWith("<<<<< Finished to Handler")) {
                val duration = System.currentTimeMillis() - startWorkTimeMillis
                if (duration > 100) {
                    Log.e("Main thread takes too long to execute","$duration Millisecond, $it")
                }
            }
        }
        val handler = Handler(Looper.getMainLooper())
        handler.post {
            while (true) {
                try {
                    Looper.loop()
                } catch (e: Throwable) {
                    // TODO main thread crashes and reports the crash information by itself
                    if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
                        android.os.Process.killProcess(android.os.Process.myPid())
                        break
                    }
                    e.printStackTrace()
                }
            }
        }
        Thread.setDefaultUncaughtExceptionHandler { thread, e ->
            e.printStackTrace()
            // TODO asynchronous thread crashes and reports the crash information by itself
        }
    }
}

Through the above code, you can realize the interception of UI thread crash, time-consuming performance monitoring. However, it is not able to intercept all exceptions. If the onCreate of an Activity crashes, causing the creation of an Activity to fail, a black screen will be displayed.

ANR get stack information< Android: implement ANR monitoring and get stack based on Handler and Looper>

Source code analysis

Through the above simple code, we can realize the interception and monitoring of crash and ANR, but we may not know why, including we know that there is ANR, but we need to further analyze where and how to solve it. Today's questions are:

  1. How to intercept global crash and avoid APP exit.
  2. How to realize ANR monitoring.
  3. Using Handler to realize the function of single thread pool.
  4. Why does the life cycle of an Activity use Handler to send execution?
  5. How to implement the delay operation of Handler.

Source code involved

/java/android/os/Handler.java
/java/android/os/MessageQueue.java
/java/android/os/Looper.java
/java/android.app/ActivityThread.java

We start from the start of app. The start method of app is to create the looper of main thread in ActivityThread, that is, the current process creation. And at the end of the main method, Looper.loop() is called. In this method, the main thread's task scheduling is handled. Once executed, this means that APP is withdrawn. If we want to avoid APP being withdrawn, APP must continue to execute Looper.loop().

package android.app;
public final class ActivityThread extends ClientTransactionHandler {
    ...
    public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
        ...
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}

Looper.loop()

Let's further analyze the Looper.loop() method, write a loop in this method, and exit only when queue.next() == null. We may have a question in mind here. If there is no main thread task, is the Looper.loop() method quit? In fact, queue.next() is actually a blocking method. If there is no task or no active exit, it will block all the time and wait for the main thread task to be added.

When there is a task in the queue, the message Dispatching to... Will be printed, and then msg.target.dispatchMessage(msg) will be called; when the task is executed, the message Finished to... Will be printed. We can analyze the ANR through the printed information. Once the task is executed for more than 5 seconds, the system will prompt the ANR, but we must be more strict with our APP. We can give We set a goal to report the statistics after the specified time to help us optimize.

public final class Looper {
    final MessageQueue mQueue;
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            // 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);
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {}
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
            msg.recycleUnchecked();
        }
    }
    public void quit() {
        mQueue.quit(false);
    }
}

If there is an exception in the main thread, it will exit the loop, which means that APP crashes, so we need to try catch to avoid APP exit. We can start another loop. Loop () in the main thread to execute the main thread task, and then try catch the loop. Loop () method will not exit.

Implementation of single thread thread pool based on Handler

From the above loop. Loop(), we can use the Handler to implement the single thread pool function, and this thread pool has the same powerful functions as the main thread, such as immediate post(), delayed post(), timed post at time().

// Wrong usage
var handler: Handler? = null
Thread({
    handler = Handler()
}).start()

Can't create Handler inside thread [thread-2,5, main] that has not called Looper. Prepare(). This is because the Handler's work depends on the Looper, which must be created for the thread to function normally. The correct usage is as follows:

// Correct usage
var handler: Handler? = null
Thread({
    Looper.prepare()
    handler = Handler()
    Looper.loop()
}).start()

Test:

button.setOnClickListener {
    handler?.post {
        println(Thread.currentThread())
    }
    handler?.post {
        println(Thread.currentThread())
    }
}

Output results:

System.out: Thread[Thread-2,5,main]
System.out: Thread[Thread-2,5,main]
HandlerThread

HandlerThread is Android's encapsulation of Thread. With the addition of Handler support, the implementation implements the functions of the previous example.

val handlerThread = HandlerThread("test")
handlerThread.start()
handler = Handler(handlerThread.looper)

Source code analysis of MessageQueue

We all know that the Handler has a lot of functions, including immediate post(), delayed post(), timed post at time (), etc. Next, from the source analysis is how to achieve.

public final class MessageQueue {
    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();
            }

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

MessageQueue.next() is a method with blocking. Only exit or tasks can return. The implementation of starting blocking is to use the nativePollOnce() function of the Native layer. If there is no message in the message queue with nativePollOnce, it will not return. It has been in the waiting state of the Native layer. Until quit() is called or enqueueMessage(Message msg, long when) is called, a new task comes in and calls the nativeWake() function of the Native layer, it will not wake up again. android_os_MessageQueue.cpp

nativePollOnce(long ptr, int timeoutMillis)

nativePollOnce is a Native function with two parameters. The first parameter is the ID of the current task queue. The second parameter is the waiting time. If - 1, it means there is no message and it will enter the waiting state. If 0, it will find the message waiting again. If it is greater than 0, wait until the specified time and then return.

nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

In this line of code, delay is assigned to realize the functions of postDelayed and postatime.

enqueueMessage()

See here we may have a question, since it is the queue, first in, first out principle, then what is the output result of the following code?

Handler?. postdelayed ({println ("task 1")}, 5000)
Handler?. post {println ("task 2")}
Handler?. postdelayed ({println ("task 3")}, 3000)
//Output results
 Task 2
 Task 3
 Task 1

The reason is that when enqueueMessage(Message msg, long when) adds tasks, it has been sorted according to the execution time.

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            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;
    }

summary

After the above analysis, I think it's very meaningful to understand Handler and Looper MessageQueue, which can help us better handle crashes, ANR, Handler use, etc.

Well, that's the end of the article. If you think the articles are useful, you can recommend them to your friends.

Posted by gkelley091565 on Fri, 25 Oct 2019 23:21:35 -0700