preface

Of interception, we all know that can Thread. SetDefaultUncaughtExceptionHandler to intercept exceptions that occur in the App, and then for processing.

So, I had a half-baked idea…

Keep my APP from crashing

Since we can intercept crashes, we can simply block all exceptions in the APP without killing the program. The user experience of such a non-crash APP is not a bar?

  • Some people shook their heads in disapproval, and this light came to ask me:

“Old tie, when a crash occurs, you solve it, not cover it up!!”

  • I took a fan to fan a few times, a little cold but pretending to be calm said:

“Brother, you can upload the exception to your own server to handle ah, you can get your crash cause, users will not cause the APP crash because of the exception, isn’t that good?”

  • Light a little angry said:

“There’s something wrong with that. It sounds crazy. I’ll give it a try.”

Experiments with little light

So light according to a small online blogger – blocks article, write the following code to catch exceptions:

/ / define CrashHandler
class CrashHandler private constructor(): Thread.UncaughtExceptionHandler {
    private var context: Context? = null
    fun init(context: Context?). {
        this.context = context
        Thread.setDefaultUncaughtExceptionHandler(this)}override fun uncaughtException(t: Thread, e: Throwable) {}

    companion object {
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler() }
    }
}

// the Application is initialized
class MyApplication : Application() {override fun onCreate(a) {
        super.onCreate()
        CrashHandler.instance.init(this)}}// An exception is raised in the Activity
class ExceptionActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exception)
        
        btn.setOnClickListener {
            throw RuntimeException("Main thread exception")
        }
        btn2.setOnClickListener {
            Thread {
                throw NumberFormatException("Child thread exception")
            }.start()
        }
    }
}
Copy the code

In order to verify its conjecture, I wrote two kinds of triggering exceptions: child thread crash and main thread crash.

  • Run, click button 2, trigger child thread exception crash:

“Gee, there’s really no impact, the program continues to work.”

  • Then click button 1 to trigger the main thread exception crash:

“Hey hey, stuck, click a few more, direct ANR”

“Sure enough, there was a problem, but why did the main thread have a problem? I have to figure it out before I confront him.”

Light thinking (abnormal source code analysis)

First, we will introduce exceptions in Java, including runtime and non-runtime exceptions:

  • Runtime exception. Is RuntimeException class and its subclasses abnormalities, abnormal are tested, such as system exception or a program logic, we often encounter a NullPointerException, IndexOutOfBoundsException, etc. Upon such an exception, the Java Runtime will stop the thread, print an exception, and the program will stop running, often referred to as a crash.

  • Non-runtime exception. Exceptions are exceptions that belong to the Exception class and its subclasses. Exceptions that are checked are exceptions other than RuntimeException. Exceptions such as NoSuchFieldException and IllegalAccessException must be handled in the program, without which the program will not compile properly.

Ok, which means that when we throw a RuntimeException, the thread will stop. If this exception is thrown in the main thread, the main thread will be stopped, so the APP will be stuck and unable to operate properly, and will be ANR after a long time. The child thread crashes without affecting the main thread, the UI thread, so the user can still use it.

That kind of makes sense.

And so on, why meet setDefaultUncaughtExceptionHandler won’t collapse?

We also need to start with the source of the exception:

The JAVA virtual machine calls uncaughtException() whenever an uncaughtException occurs on a single thread in the same thread group.

// ThreadGroup.java
  private final ThreadGroup parent;

    public void uncaughtException(Thread t, Throwable e) {
        if(parent ! =null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if(ueh ! =null) {
                ueh.uncaughtException(t, e);
            } else if(! (einstanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" "); e.printStackTrace(System.err); }}}Copy the code

Parent represents the parent thread group of the current thread group, so this method is eventually called. Then look at the back of the code, through getDefaultUncaughtExceptionHandler access to the system default exception handler, and then call the uncaughtException method. So let’s look for the original system exception handler – UncaughtExceptionHandler.

The zygoteInit method is called when a new process is started. The zygoteInit method performs some application initialization:

    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
        if (RuntimeInit.DEBUG) {
            Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        // Log redirection
        RuntimeInit.redirectLogStreams();
        // General configuration initialization
        RuntimeInit.commonInit();
        // Zygote initialization
        ZygoteInit.nativeZygoteInit();
        // Apply related initializations
        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
    }
Copy the code

The exception handler is in this general configuration initialization method:

    protected static final void commonInit(a) {
        if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

       // Set the exception handler
        LoggingHandler loggingHandler = new LoggingHandler();
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

        // Set the time zone
        TimezoneGetter.setInstance(new TimezoneGetter() {
            @Override
            public String getId(a) {
                return SystemProperties.get("persist.sys.timezone"); }}); TimeZone.setDefault(null);

        / / the log configuration
        LogManager.getLogManager().reset();
        / / * * *

        initialized = true;
    }
Copy the code

Here you have the default exception handler — KillApplicationHandler — applied.

private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
        private final LoggingHandler mLoggingHandler;

        
        public KillApplicationHandler(LoggingHandler loggingHandler) {
            this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
        }

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
                ensureLogging(t, e);
                / /...
                // Bring up crash dialog, wait for it to be dismissed
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } catch (Throwable t2) {
                if (t2 instanceof DeadObjectException) {
                    // System process is dead; ignore
                } else {
                    try {
                        Clog_e(TAG, "Error reporting crash", t2);
                    } catch (Throwable t3) {
                        // Even Clog_e() fails! Oh well.}}}finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());
                System.exit(10); }}private void ensureLogging(Thread t, Throwable e) {
            if(! mLoggingHandler.mTriggered) {try {
                    mLoggingHandler.uncaughtException(t, e);
                } catch (Throwable loggingThrowable) {
                    // Ignored.}}}Copy the code

See here, small light pleased a smile, I caught it. In the uncaughtException callback, a handleApplicationCrash method is executed for exception handling and finally for process destruction. Try everything to make sure this process goes away. So the program crashed.

The crash popup we usually see on mobile phones is in this handleApplicationCrash method. Not only Java crashes, but also native_crash, ANR and other exceptions that we encounter will end up in the handleApplicationCrash method for crash handling.

LoggingHandler (uncaughtException) {LoggingHandler (uncaughtException) {LoggingHandler (uncaughtException); Is the LoggingHandler the same crash log we see when we encounter crash problems? Take a look inside:

private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
        public volatile boolean mTriggered = false;

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            mTriggered = true;
            if (mCrashing) return;

            if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
                Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
            } else {
                StringBuilder message = new StringBuilder();
                message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
                final String processName = ActivityThread.currentProcessName();
                if(processName ! =null) {
                    message.append("Process: ").append(processName).append(",");
                }
                message.append("PID: ").append(Process.myPid()); Clog_e(TAG, message.toString(), e); }}}private static int Clog_e(String tag, String msg, Throwable tr) {
        return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
    }
Copy the code

Isn’t that what it is? Some information about the crash – such as thread, process, process ID, crash reason, etc. – is printed by Log. Here’s a crash log to show you:

Ok, get back on track, so we through setDefaultUncaughtExceptionHandler method set up our own collapse processor, set the application before the collapse of the processor to top off, and then we didn’t do any processing, natural processes will not collapse, to summarize the chart.

Light came to confront me again

  • The light that figured it all out came to me again:

“Old iron, you look, this is the Demo I wrote and summary of the data, your set does not work, the main thread crashed GG, I said there is a problem.”

  • I continued to play it cool:

“Brother, I forgot to mention last time, only add this UncaughtExceptionHandler can not, also need to add a code, send you, go back to try.”

    Handler(Looper.getMainLooper()).post {
        while (true) {
            try {
                Looper.loop()
            } catch (e: Throwable) {
            }
        }
    }
Copy the code

“Does… does it work?”

Light again experiment

Add the above code to the Application (Application — onCreate) and run it again:

I really have no problem. After clicking the main thread crashes, you can still operate the APP normally. What is the principle of this?

Rethinking of light (blocking the main thread crash scheme idea)

As we all know, a Handler mechanism is maintained in the main thread, Looper is created and initialized at application startup, and the loop method is called to start the loop processing of messages. Applications in use process, click on the main thread of all operations such as events, a list of sliding, and so on are complete processing in this cycle, its essence is to add a message to MessageQueue queue, then cycle to remove the message from the queue and processing, if there is no message processing, hang wait for awakening will rely on epoll mechanism. Here’s my condensed loop code:

    public static void loop(a) {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for(;;) { Message msg = queue.next(); msg.target.dispatchMessage(msg); }}Copy the code

An endless loop that keeps retrieving and processing messages. Go back to the code you just added:

    Handler(Looper.getMainLooper()).post {
        while (true) {
            // The main thread is blocked
            try {
                Looper.loop()
            } catch (e: Throwable) {
            }
        }
    }
Copy the code

We send a runnable task to the main thread via Handler, and then add an infinite loop to the runnable that executes looper.loop () for the message loop read. This will cause all subsequent main thread messages to be processed in our loop method, which will catch exceptions if the main thread crashes. Also, since we are writing a while loop, the new looper.loop () method will be executed once the exception is caught. This way the main thread’s Looper can keep reading messages, and the main thread can keep running.

Words can’t say clearly pictures to help us:

In addition, the CrashHandler logic ensures that the child thread is not affected by the crash.

But the light is not convinced, he thought of a collapse…

Light again and again experiment

class Test2Activity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exception)

        throw RuntimeException("Main thread exception")}}Copy the code

I’m going to throw you an exception in onCreate and run it:

It’s dark. Yes, it’s black.

The last conversation (Cockroach Thought)

  • Seeing this scene, I took the initiative to find the light:

“This kind of circumstance is more troublesome, if an exception is thrown, directly in the Activity life cycle leads to interface, it cannot be drawn, the Activity could not be started, right will be white or black screen this seriously affect the user experience or advice directly kill the APP, because is likely to affect the function of other modules. Or if some activities aren’t important, you can just finish the Activity.”

  • Little Light asked thoughtfully:

“So how do you tell when a crash occurs during a lifetime?”

“It’s all about reflection. To take an idea from the Cockroach open source library, since the Activity’s life cycle is handled by the main thread’s Handler, you can replace the Callback in the main thread’s Handler with reflection. Namely ActivityThread. MH. MCallback, and then the corresponding messages for each life cycle trycatch catch exceptions, then can be finishActivity or kill the process operation.”

Main code:

		Field mhField = activityThreadClass.getDeclaredField("mH");
        mhField.setAccessible(true);
        final Handler mhHandler = (Handler) mhField.get(activityThread);
        Field callbackField = Handler.class.getDeclaredField("mCallback");
        callbackField.setAccessible(true);
        callbackField.set(mhHandler, new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (Build.VERSION.SDK_INT >= 28) {
                // Life cycle processing after Android 28
                    final int EXECUTE_TRANSACTION = 159;
                    if (msg.what == EXECUTE_TRANSACTION) {
                        try {
                            mhHandler.handleMessage(msg);
                        } catch (Throwable throwable) {
                            // Kill the process or the Activity
                        }
                        return true;
                    }
                    return false;
                }
                
                // Life cycle processing before Android 28
                switch (msg.what) {
                    case RESUME_ACTIVITY:
                    //onRestart onStart onResume callback here
                        try {
                            mhHandler.handleMessage(msg);
                        } catch (Throwable throwable) {
                            sActivityKiller.finishResumeActivity(msg);
                            notifyException(throwable);
                        }
                        return true;
Copy the code

This is done by replacing the Callback of the main thread Handler with a declaration cycle.

The next step is to do the post-capture processing, either by killing the process or the Activity.

  • Kill processes, this should all be familiar
  Process.killProcess(Process.myPid())
  exitProcess(10)
Copy the code
  • Finish off the Activity

The finish process of the Activity is described in android29.

    private void finish(int finishTask) {
        if (mParent == null) {
            
            if (false) Log.v(TAG, "Finishing self: token=" + mToken);
            try {
                if(resultData ! =null) {
                    resultData.prepareToLeaveProcess(this);
                }
                if (ActivityTaskManager.getService()
                        .finishActivity(mToken, resultCode, resultData, finishTask)) {
                    mFinished = true; }}}}@Override
    public final boolean finishActivity(IBinder token, int resultCode, Intent resultData,
            int finishTask) {
        return mActivityTaskManager.finishActivity(token, resultCode, resultData, finishTask);
    }    
Copy the code

From the finish of the Activity source, the final is a call to ActivityTaskManagerService finishActivity method, this method has four parameters, one is used to identify the parameters of the Activity is the most important parameters, the token. So go to the source code to find token~

Since we captured it in the handleMessage callback method, there is only one argument Message that can be used, so you should start with that. Let’s go back to the source code where we handled the message and see if we can find any clues:

 class H extends Handler {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EXECUTE_TRANSACTION: 
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    mTransactionExecutor.execute(transaction);
                    break; }}}public void execute(ClientTransaction transaction) {
        final IBinder token = transaction.getActivityToken();
        executeCallbacks(transaction);
        executeLifecycleState(transaction);
        mPendingActions.clear();
        log("End resolving transaction");
    }    
Copy the code

In the source code, you can see how the Handler handles the EXECUTE_TRANSACTION message, retrieves the msG. obj object, which is an instance of the ClientTransaction class, and calls the execute method. In the execute method… Yi yi yi, isn’t that a token?

The source code of this part is not the focus of today, so I will skip it.

If we find the token, we can use reflection to destroy the Activity:

    private void finishMyCatchActivity(Message message) throws Throwable {
        ClientTransaction clientTransaction = (ClientTransaction) message.obj;
        IBinder binder = clientTransaction.getActivityToken();
       
       Method getServiceMethod = ActivityManager.class.getDeclaredMethod("getService");
        Object activityManager = getServiceMethod.invoke(null);

        Method finishActivityMethod = activityManager.getClass().getDeclaredMethod("finishActivity", IBinder.class, int.class, Intent.class, int.class);
        finishActivityMethod.setAccessible(true);
        finishActivityMethod.invoke(activityManager, binder, Activity.RESULT_CANCELED, null.0);
    }
Copy the code

Ah, finally done, but the light is still a puzzled look at me:

“I’ll go look at the Cockroach vault’s source code.”

“I’ll go.”

conclusion

Today’s focus is on one thing: how to catch exceptions in the application and not crash the APP, so as to give users the best experience. The main methods are as follows:

  • Catch the exception of the main thread by sending a message in the main thread and continue the call after the exception occursLooper.loopMethod that causes the host thread to continue processing the message.
  • For child thread exceptions, can passThread.setDefaultUncaughtExceptionHandlerTo intercept, and the child thread stops without being perceived by the user.
  • Exceptions that occur during the life cycle can be replacedActivityThread.mH.mCallbackMethod to capture, and throughtokenTo end the Activity or simply kill the process. However, this method needs to be adapted to the source code of different SDK versions, so be careful with it. If you need it, look at the source code of the Venomcockroach library.

Some of you may ask, why not let the program crash? Where do we need to do this?

In fact, there are a lot of times, there are exceptions that we can’t expect or the user is almost unaware of the exception, such as:

  • Some bugs in the system
  • Some bugs in third-party libraries
  • Some bugs from different phones

In such cases, we can make APP sacrifice this part of the function through such operations to maintain the stability of the system.

reference

Cockroach is an evolutionary Cockroach. The Cockroach process is called The wanAndroid

Bye bye

Well, it’s time to say goodbye.

Recommend a drama to everybody finally – chess soul, hey hey, small light is the leading role inside.

These excellent open source libraries are the light that guides our progress

Thank you for your reading. If you study with me, you can pay attention to my public account — code on blocks ❤️❤️.