preface
As for intercepting exceptions, you must know that you can use thread Setdefaultuncaughtexceptionhandler to intercept exceptions in the App and then handle them.
So I had an immature idea...
Let my APP never collapse
Since we can intercept crashes, we can directly intercept all exceptions in the APP without killing the program. Such an APP user experience that will not collapse is not a lever?
- Someone shook his head and said he didn't agree. He came to me and asked me:
"Old fellow iron, the collapse is to solve it, not to cover it up!!"
- I fanned the fan a few times. It was a little cold, but I pretended to be calm and said:
"Brother, you can upload the exception to your own server for processing. You can get the cause of your crash, and users won't crash the APP because of the exception. Isn't that good?"
- Xiaoguang said angrily:
"There must be a problem. It doesn't sound reliable. Hum, I'll try."
Small light experiment
So Xiaoguang wrote the following code to catch exceptions according to the article of a small blogger building block on the Internet:
//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() } } } //Initialization in Application class MyApplication : Application(){ override fun onCreate() { super.onCreate() CrashHandler.instance.init(this) } } //Exception triggered in 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 RuntimeException("Child thread exception") } } } }
In order to verify its conjecture, Xiaoguang wrote two cases of triggering exceptions: sub thread crash and main thread crash.
- Run and click button 2 to trigger the abnormal crash of the sub thread:
"Well, it really doesn't matter. The program can continue to run normally."
- Then click button 1 to trigger the abnormal crash of the main thread:
"Hey, hey, it's stuck. Click a few more times and go straight to ANR"
<figcaption style="margin: 5px 0px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;"> Main thread crash < / figcaption >
"Yes, but why is there any problem with the main thread? I have to understand it first and then go to the old fellow iron company."
Xiaoguang's thinking (abnormal source code analysis)
First, check the exceptions in java, including runtime exceptions and non runtime exceptions:
- Runtime exception. It is the exception of RuntimeException class and its subclasses, and it is non checked exception, such as system exception or program logic exception. We often encounter NullPointerException, IndexOutOfBoundsException, etc. In case of this exception, the Java Runtime will stop the thread, print the exception, and stop the program, that is, the program crashes.
- Non runtime Exception. It belongs to the Exception class and its subclasses. It is the checked Exception, except RuntimeException. Such exceptions must be handled in the program. If they are not handled, the program cannot be compiled normally, such as NoSuchFieldException and IllegalAccessException.
ok, that is, after we throw a RuntimeException, the thread will be stopped. If this exception is thrown in the main thread, the main thread will be stopped, so the APP will get stuck and cannot operate normally. After a long time, the ANR will be. The crash of the child thread will not affect the operation of the main thread, that is, the UI thread, so the user can still use it normally.
It seems to make sense.
Wait, why doesn't the setDefaultUncaughtExceptionHandler crash when it comes to setDefaultUncaughtExceptionHandler?
We have to start with the source code of the exception:
Generally, the threads used in an application are in the same thread group. In this thread group, as long as there is an uncaught exception in one thread, the JAVA virtual machine will call the uncaughtException() method in the thread group where the current thread is located.
// 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 (!(e instanceof ThreadDeath)) { System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } }
Parent represents the parent thread group of the current thread group, so it will be called in this method finally. Then look at the code behind, get the default processor of the system through getDefaultUncaughtExceptionHandler, then call the uncaughtException method. Then let's find the exception handler in the original system - UncaughtExceptionHandler.
This starts with the APP startup process. As mentioned earlier, all Android processes are fork ed from the zygote process. When a new process is started, the zygoteInit method will be called. This method will initialize some applications:
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(); //Application related initialization return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader); }
As for exception handlers, in this general configuration initialization method:
protected static final void commonInit() { if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!"); //Set exception handler LoggingHandler loggingHandler = new LoggingHandler(); Thread.setUncaughtExceptionPreHandler(loggingHandler); Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler)); //Set time zone TimezoneGetter.setInstance(new TimezoneGetter() { @Override public String getId() { return SystemProperties.get("persist.sys.timezone"); } }); TimeZone.setDefault(null); //log configuration LogManager.getLogManager().reset(); //*** initialized = true; }
Find it. Here is the default exception handler for application - KillApplicationHandler.
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. } } }
Seeing this, Xiaoguang smiled with relief and was caught by me. In the uncaughtException callback method, a handleApplicationCrash method will be executed to handle exceptions, and finally go to finally to destroy the process. Try everything to make sure this process goes away. So the program crashed.
The crash prompt pop-up window we usually see on the mobile phone is popped out in this handleApplicationCrash method. Not only the java crash, but also the native we usually encounter_ Crash, ANR and other exceptions will finally go to the handleApplicationCrash method for crash processing.
In addition, some friends may find that a LoggingHandler is passed in the construction method, and the uncaughtException method of the LoggingHandler is also called in the uncaughtException callback method. Is this LoggingHandler the crash log we see when we encounter a crash problem? Go in and have a look:
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); }
Isn't that it? Some crash information such as thread, process, process id, crash reason, etc. are printed out through Log. Let's show you a crash Log:
OK, back on track, so we set our own crash processor through the setDefaultUncaughtExceptionHandler method, and knocked out the crash processor set by the previous application. Then we didn't do any processing, and the natural program won't crash. Let's draw a summary diagram.
Xiaoguang came to me again
- Xiaoguang came to me again to find out all this:
"Old fellow, you see, this is the information I wrote about Demo and summary. Your root is not workable. If the main thread collapses, it will be GG. I will say there is a problem."
- I continued to pretend to be calm:
"Brother, I forgot to say last time. Just adding this UncaughtExceptionHandler is not enough. I have to add a piece of code and send it to you. Go back and try."
Handler(Looper.getMainLooper()).post { while (true) { try { Looper.loop() } catch (e: Throwable) { } } }
"Is that okay?"
Xiaoguang's experiment again
Xiaoguang adds the above code to the program (Application onCreate) and runs it again:
I'll go. It's really no problem. After clicking the main thread crashes, you can still operate the app normally. What's the principle?
Rethinking of Xiaoguang (scheme idea of intercepting main thread crash)
As we all know, a set of Handler mechanism is maintained in the main thread. When the application starts, the loop is created and initialized, and the loop method is called to start the circular processing of messages. During the use of the application, all operations of the main thread, such as event clicking, list sliding, etc., are processed in this loop. Its essence is to add messages to the MessageQueue queue, and then loop to take messages from the queue and process them. If there is no message processing, it will hang up and wait for wake-up by relying on the epoll mechanism. Post my condensed loop code:
public static void loop() { final Looper me = myLooper(); final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); msg.target.dispatchMessage(msg); } }
An endless loop that continuously fetches and processes messages. Look back at the code just added:
Handler(Looper.getMainLooper()).post { while (true) { //Main thread exception interception try { Looper.loop() } catch (e: Throwable) { } } }
We send a runnable task to the main thread through the Handler, and then add an endless loop to the runnable, in which Looper.exe is executed Loop() to read the message loop. This will cause all subsequent main thread messages to go to our loop method for processing, that is, once the main thread crashes, you can catch exceptions here. At the same time, because we write a while loop, we will start a new Looper after catching the exception The loop () method executes. In this way, the Looper of the main thread can always read messages normally, and the main thread can always run normally.
Pictures with unclear words help us:
At the same time, the previous CrashHandler logic can ensure that the child thread will not be affected by the crash, so the two pieces of code are added and live together.
But Xiaoguang was not convinced, and he thought of a collapse...
Xiaoguang experimented again
class Test2Activity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_exception) throw RuntimeException("Main thread exception") } }
Ah, I'll throw you an exception directly in onCreate. Run to see:
It's dark ~ Yes, the screen is black.
Final dialogue (coach library idea)
- Seeing this scene, I took the initiative to find Xiaoguang:
"This situation is really troublesome. If an exception is thrown directly in the Activity life cycle, the interface drawing will not be completed, and the Activity will not be started correctly, and the screen will be white or black. In this case, it is recommended to kill the APP directly, because it is likely to affect other function modules. Or if some activities do not start correctly It is very important. You can only finish this Activity. "
- Xiaoguang asked thoughtfully, "so how do you distinguish the collapse in this life cycle?"
"This is through reflection. Borrowing the idea of the coach open source library, since the life cycle of an Activity is handled by the Handler of the main thread, we can replace the Callback callback in the Handler of the main thread, that is, ActivityThread.mH.mCallback, through reflection, and then try to catch exceptions for the messages corresponding to each life cycle. However After that, you can finish the Activity or kill the process. "
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 process or 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;
Part of the code is posted, but everyone should understand the principle. It is to catch exceptions in the declaration cycle by replacing the Callback of the main thread Handler.
The next step is to process after capture, either kill the process or kill the Activity.
- The killing process should be familiar to everyone
Process.killProcess(Process.myPid()) exitProcess(10)
- finish Activity
Here we will analyze the finish process of Activity. To put it simply, take the source code of Android 29 as an example.
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); }
From the Activity's finish source code, we can know that the final call is to call the finishActivity method of ActivityTaskManagerService. This method has four parameters, one of which is used to identify the Activity, that is, the most important parameter - token. So go to the source code to find the token~
Since the place we captured is in the handleMessage callback method, only one parameter Message can be used. Let's start from this aspect. Go back to the source code of the Message we just processed to 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"); }
You can see how the Handler handles execute in the source code_ Of the transaction message, get MSG The obj object, that is, the ClientTransaction class instance, then calls the execute method. In the execute method... Eh, eh, isn't this a token?
(it's too fast to find it. The main reason is that the activity starts to destroy. The explanation of the source code of this part is not the focus of today, so I've covered it all.)
If we find the token, we can destroy the Activity through reflection:
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); }
Ah, it's finally done, but Xiaoguang still looks at me with a puzzled face:
"I'd better see the source code of the coach Library ~"
"I'll go."
summary
Today, I mainly talked about one thing: how to catch exceptions in the program and prevent the APP from crashing, so as to bring the best experience to users. The main methods are as follows:
- By sending a message in the main thread, catch the exception of the main thread, and continue to call looper Loop method so that the main thread continues to process messages.
- For exceptions of child threads, you can use thread Setdefaultuncaughtexceptionhandler to intercept, and the stop of the child thread will not bring perception to the user.
- For exceptions that occur within the lifecycle, you can replace the activitythread mH. The method of mcallback is used to capture and end the Activity or kill the process directly through token.
Some friends may ask, why not let the program crash? What situations do we need to do this?
In fact, there are still a lot of exceptions that we can't predict or bring almost imperceptible exceptions to users, such as:
- Some bug s in the system
- Some bug s in third-party libraries
- Some bug s brought by mobile phones from different manufacturers
In these cases, we can make the APP sacrifice the functions of this part to maintain the stability of the system.
For my inventory, please click My GitHub Free collection