What can App do to never crash

How do you handle program crashes in your project?

  • try, of course

How to collect exception logs?

  • Generally, you will write a tool class by hand, and then record the log in a special way where there may be problems, and then upload it at an appropriate time

This classmate, did you not wake up? What I asked is the exception log, which is the exception of your unknown state. Do you want to try the whole project?

  • Well, you can write a crashhandler: thread. Uncaughtexceptionhandler, which is registered in the Application.

Then collect the log information in the rewritten uncaughtException(t: Thread, e: Throwable).

Why does the program stop when an exception occurs?

  • It should be that the system ended the whole program process

If there is an exception, will the program stop running?

  • Well... It should

In the case of unknown exceptions, is there a way to keep the program from crashing?

  • Well... It should be ok

All right, go back and wait for the notice

The above is an interview scene that has been dramatized. It examines the understanding of exception handling and Handler correspondence principle. Next, we analyze the problems one by one.

Will try catch affect program performance?

First, try catch should be used to narrow the scope as much as possible. When no exception is thrown in the scope of try catch, the performance impact is not great, but as long as an exception is thrown, the performance impact is doubled. Specifically, I conducted a simple test for the following three situations.

  • No try catch
  • There is a try catch, but there is no exception
  • There are both try catch and exception.
   fun test() {
        val start = System.currentTimeMillis()
        var a = 0
        for (i in 0..1000) {
            a++
        }
        Log.d("timeStatistics", "noTry: " + (System.currentTimeMillis() - start))
    }

    fun test2() {
        val start = System.currentTimeMillis()
        var a = 0
        for (i in 0..1000) {
            try {
                a++
            } catch (e: java.lang.Exception) {
                e.printStackTrace()
            }
        }
        Log.d("timeStatistics", "tryNoCrash: " + (System.currentTimeMillis() - start))
    }

    fun test3() {
        val start = System.currentTimeMillis()
        var a = 0
        for (i in 0..1000) {
            try {
                a++
                throw java.lang.Exception()
            } catch (e: java.lang.Exception) {
                e.printStackTrace()
            }
        }
        Log.d("timeStatistics", "tryCrash: " + (System.currentTimeMillis() - start))
    }

     2021-02-04 17:10:27.823 22307-22307/com.ted.nocrash D/timeStatistics: noTry: 0
     2021-02-04 17:10:27.823 22307-22307/com.ted.nocrash D/timeStatistics: tryNoCrash: 0
     2021-02-04 17:10:28.112 22307-22307/com.ted.nocrash D/timeStatistics: tryCrash: 289

Two conclusions can be clearly drawn from the log

    1. When there is no exception, try and no try have little effect, both 0 Ms.
    1. When there are exceptions, the performance decreases by 289 times

Of course, the above tests are extreme cases. The purpose is to enlarge the problem and face the problem directly, so in the future, try catch should narrow the scope as much as possible.

How to collect exception logs?

The answer to this question has been given at the beginning of this article. Log collection can be realized by inheriting Thread.UncaughtExceptionHandler and overriding uncaughtException(). Note: it needs to be initialized in the Application call

class MyCrashHandler : Thread.UncaughtExceptionHandler {
    override fun uncaughtException(t: Thread, e: Throwable) {
        Log.e("e", "Exception:" + e.message);
    }

    fun init() {
        Thread.setDefaultUncaughtExceptionHandler(this)
    }
}

At this time, you can collect and upload logs in the uncaughtException() method.

Why does the program stop when an exception occurs?

For this problem, we need to understand the exception handling mechanism of Android. Before we set Thread.UncaughtExceptionHandler, the system will set one by default. For details, please refer to ZygoteInit.zygoteInit()

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

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        RuntimeInit.redirectLogStreams();

        RuntimeInit.commonInit();
        ZygoteInit.nativeZygoteInit();
        return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
                classLoader);
    }

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

        /*
         * set handlers; these apply to all threads in the VM. Apps can replace
         * the default handler, but not the pre handler.
         */
        LoggingHandler loggingHandler = new LoggingHandler();
        RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
        ...
}

You can see that setDefaultUncaughtExceptionHandler() has been set in ZygoteInit.zygoteInit(), and ZygoteInit is the process of process initialization. Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

When the program has an agenda, it will call back to KillApplicationHandler.uncaughtException(Thread t, Throwable e)

   @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
                ensureLogging(t, e);

                // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
                if (mCrashing) return;
                mCrashing = true;

                // Try to end profiling. If a profiler is running at this point, and we kill the
                // process (below), the in-memory buffer will be lost. So try to stop, which will
                // flush the buffer. (This makes method trace profiling useful to debug crashes.)
                if (ActivityThread.currentActivityThread() != null) {
                    ActivityThread.currentActivityThread().stopProfiling();
                }

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

In direct observation of finally, Process.killProcess (Process.myPid ()) is called. System.exit(10);, Trigger the process end logic, which causes the program to stop running.

If an exception occurs, will the program stop running?

  • First, we need to define the concept of shutdown. Generally, there are two main situations.
    1. Program process exit (flash back often used in benchmarking)
    1. The program process continues, but click no response user event (benchmarking ANR)

The first problem is well understood, that is, the process exit of the above process. We mainly study the second case. The process continues but cannot respond to user events.

Here I want to popularize a little knowledge. Why can the Android system respond to various (man-made / non-man-made) events?

  • The concept of Handler will be involved here. In fact, the operation of the whole operating system depends on the mechanism of Handler Message Looper. All behaviors will be assembled into Message messages. Then Looper opens a for loop (dead loop) to take out one Message to the Handler for processing, and the Handler responds after processing, Our behavior is answered, and the faster the impact, the smoother the system will be.

Here is no more description of the Handler mechanism. If necessary, you can take a look at my blog that has been authorized to Hongyang. It's really rude. You can make sure you understand the whole process in a moment.

5 minutes to understand the Handler mechanism and the wrong use scenario of Handler

OK, let's go back and talk about why the process continues, but can't respond to user events? In fact, it was already mentioned when we just described the Handler. If an exception occurs, the Looper of the main thread has exited the loop. How can I respond to you if they all exit the loop.

After analyzing the above two situations clearly, let's focus on how to solve these two problems, and complete the first one first.

If an exception occurs, how to prevent the process from exiting? As mentioned above, the process exits. In fact, the default KillApplicationHandler.uncaughtException() calls Process.killProcess(Process.myPid()); System.exit(10). Just prevent the exit from calling KillApplicationHandler.uncaughtException()?

As described at the beginning of this article, we only need to implement a Thread.UncaughtExceptionHandler class and initialize it in the Application

class MyCrashHandler : Thread.UncaughtExceptionHandler {
    override fun uncaughtException(t: Thread, e: Throwable) {
       Log.e("e", "Exception:" + e.message);
    }

    fun init() {
        Thread.setDefaultUncaughtExceptionHandler(this)
    }
}

The above logic sets the default UncaughtExceptionHandler for Thread, so when a crash occurs again, we will call ThreadGroup.uncaughtException(), and then handle the exception to our own MyCrashHandler, so we won't exit the process.

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 (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

The above logic triggers the second stop at the same time, that is, although the process does not exit, the user clicks no response. Since the user's non response is caused by Looper exiting the loop, we can start the loop. We just need to call Application onCreate() in the following way

   Handler(mainLooper).post {
            while (true) {
                try {
                    Looper.loop()
                } catch (e: Throwable) {
                }
            }
        }

What's the meaning of this? We post a Message to the Message queue through the Handler. This Message is an endless loop. Every time loop() has an exception, it will restart loop(), which solves the problem of no response. However, the exception handling logic must be controlled here. Although the loop() is restarted indefinitely, it is not a long-term solution if it has been abnormal all the time. This try is equivalent to trying to live the running logic of the whole App.

At the beginning, we also explained that the scope of try should be as small as possible. Isn't this approach to maximize the scope of try??? In fact, our main efforts are to improve the code quality and reduce the probability of exceptions. This practice is just a remedy, and we use efficiency in exchange for the user experience.

To sum up, in fact, the essence of exception handling is to investigate the logical relationship between Handler, Looper mechanism, Application startup time and so on. As long as you know the corresponding relationship, you will completely master the methods of exception handling. It is recommended that you look at the Android source code.

Only through continuous learning and progress can we not be eliminated by the times. Pay attention to me and share knowledge every day!

Tags: Android Back-end

Posted on Tue, 30 Nov 2021 03:26:15 -0500 by landonmkelsey