前言
关于拦截异常,想必你们都知道能够经过Thread.setDefaultUncaughtExceptionHandler
来拦截App中发生的异常,而后再进行处理。java
因而,我有了一个不成熟的想法。。。android
让个人APP永不崩溃
既然咱们能够拦截崩溃,那咱们直接把APP中全部的异常拦截了,不杀死程序。这样一个不会崩溃的APP用户体验不是杠杠的?git
- 有人听了摇摇头表示不赞同,这不小光跑来问我了:
“老铁,出现崩溃是要你解决它不是掩盖它!!”github
- 我拿把扇子扇了几下,有点冷可是故做镇定的说:
“这位老哥,你能够把异常上传到本身的服务器处理啊,你能拿到你的崩溃缘由,用户也不会由于异常致使APP崩溃,这不挺好?”面试
- 小光有点生气的说:
“这样确定有问题,听着就不靠谱,哼,我去试试看”服务器
小光的实验
因而小光按照网上一个小博主—积木
的文章,写出了如下捕获异常的代码:app
//定义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() } } } //Application中初始化 class MyApplication : Application(){ override fun onCreate() { super.onCreate() CrashHandler.instance.init(this) } } //Activity中触发异常 class ExceptionActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_exception) btn.setOnClickListener { throw RuntimeException("主线程异常") } btn2.setOnClickListener { thread { throw RuntimeException("子线程异常") } } } }
小光一顿操做,写下了整套代码,为了验证它的猜测,写了两种触发异常的状况:子线程崩溃和主线程崩溃。ide
- 运行,点击按钮2,触发子线程异常崩溃:
“咦,还真没啥影响,程序能继续正常运行”oop
- 而后点击按钮1,触发主线程异常崩溃:
“嘿嘿,卡住了,再点几下,直接ANR了”源码分析
“果真有问题,可是为啥主线程会出问题呢?我得先搞懂再去找老铁对峙。”
小光的思考(异常源码分析)
首先科普下java中的异常,包括运行时异常
和非运行时异常
:
-
运行时异常。是
RuntimeException
类及其子类的异常,是非受检异常,好比系统异常或者是程序逻辑异常,咱们常遇到的有NullPointerException、IndexOutOfBoundsException
等。遇到这种异常,Java Runtime
会中止线程,打印异常,而且会中止程序运行,也就是咱们常说的程序崩溃。 -
非运行时异常。是属于
Exception
类及其子类,是受检异常,RuntimeException
之外的异常。这类异常在程序中必须进行处理,若是不处理程序都没法正常编译,好比NoSuchFieldException,IllegalAccessException
这种。
ok,也就是说咱们抛出一个RuntimeException
异常以后,所在的线程会被中止。若是主线程中抛出这个异常,那么主线程就会被中止,因此APP就会卡住没法正常操做,时间久了就会ANR
。而子线程崩溃了并不会影响主线程也就是UI线程的操做,因此用户还能正常使用。
这样好像就说的通了。
等等,那为何遇到setDefaultUncaughtExceptionHandler
就不会崩溃了呢?
咱们还得从异常的源码开始提及:
通常状况下,一个应用中所使用的线程都是在同一个线程组,而在这个线程组里只要有一个线程出现未被捕获异常的时候,JAVA 虚拟机就会调用当前线程所在线程组中的 uncaughtException()
方法。
// 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
表示当前线程组的父级线程组,因此最后仍是会调用到这个方法中。接着看后面的代码,经过getDefaultUncaughtExceptionHandler
获取到了系统默认的异常处理器,而后调用了uncaughtException
方法。那么咱们就去找找原本系统中的这个异常处理器——UncaughtExceptionHandler
。
这就要从APP的启动流程提及了,以前也说过,全部的Android进程
都是由zygote进程fork
而来的,在一个新进程被启动的时候就会调用zygoteInit
方法,这个方法里会进行一些应用的初始化工做:
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"); //日志重定向 RuntimeInit.redirectLogStreams(); //通用的配置初始化 RuntimeInit.commonInit(); // zygote初始化 ZygoteInit.nativeZygoteInit(); //应用相关初始化 return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader); }
而关于异常处理器,就在这个通用的配置初始化方法当中:
protected static final void commonInit() { if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!"); //设置异常处理器 LoggingHandler loggingHandler = new LoggingHandler(); Thread.setUncaughtExceptionPreHandler(loggingHandler); Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler)); //设置时区 TimezoneGetter.setInstance(new TimezoneGetter() { @Override public String getId() { return SystemProperties.get("persist.sys.timezone"); } }); TimeZone.setDefault(null); //log配置 LogManager.getLogManager().reset(); //*** initialized = true; }
找到了吧,这里就设置了应用默认的异常处理器——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. } } }
看到这里,小光欣慰一笑,被我逮到了吧。在uncaughtException
回调方法中,会执行一个handleApplicationCrash
方法进行异常处理,而且最后都会走到finally
中进行进程销毁,Try everything to make sure this process goes away
。因此程序就崩溃了。
关于咱们平时在手机上看到的崩溃提示弹窗,就是在这个handleApplicationCrash
方法中弹出来的。不只仅是java崩溃,还有咱们平时遇到的native_crash、ANR
等异常都会最后走到handleApplicationCrash
方法中进行崩溃处理。
另外有的朋友可能发现了构造方法中,传入了一个LoggingHandler
,而且在uncaughtException
回调方法中还调用了这个LoggingHandler
的uncaughtException
方法,难道这个LoggingHandler
就是咱们平时遇到崩溃问题,所看到的崩溃日志?进去瞅瞅:
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); }
这可不就是吗?将崩溃的一些信息——好比线程,进程,进程id,崩溃缘由等等经过Log打印出来了。来张崩溃日志图给你们对对看:
好了,回到正轨,因此咱们经过setDefaultUncaughtExceptionHandler
方法设置了咱们本身的崩溃处理器,就把以前应用设置的这个崩溃处理器给顶掉了,而后咱们又没有作任何处理,天然程序就不会崩溃了,来张总结图。
小光又来找我对峙了
- 搞清楚这一切的小光又来找我了:
“老铁,你瞅瞅,这是我写的Demo
和总结的资料,你那套根本行不通,主线程崩溃就GG了,我就说有问题吧”
- 我继续故做镇定:
“老哥,我上次忘记说了,只加这个UncaughtExceptionHandler
可不行,还得加一段代码,发给你,回去试试吧”
Handler(Looper.getMainLooper()).post { while (true) { try { Looper.loop() } catch (e: Throwable) { } } }
“这,,能行吗”
小光再次的实验
小光把上述代码加到了程序里面(Application—onCreate),再次运行:
我去,真的没问题了
,点击主线程崩溃后,仍是能够正常操做app,这又是什么原理呢?
小光的再次思考(拦截主线程崩溃的方案思想)
咱们都知道,在主线程中维护着Handler
的一套机制,在应用启动时就作好了Looper
的建立和初始化,而且调用了loop
方法开始了消息的循环处理。应用在使用过程当中,主线程的全部操做好比事件点击,列表滑动等等都是在这个循环中完成处理的,其本质就是将消息加入MessageQueue
队列,而后循环从这个队列中取出消息并处理,若是没有消息处理的时候,就会依靠epoll机制挂起等待唤醒。贴一下我浓缩的loop
代码:
public static void loop() { final Looper me = myLooper(); final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); msg.target.dispatchMessage(msg); } }
一个死循环,不断取消息处理消息。再回头看看刚才加的代码:
Handler(Looper.getMainLooper()).post { while (true) { //主线程异常拦截 try { Looper.loop() } catch (e: Throwable) { } } }
咱们经过Handler
往主线程发送了一个runnable
任务,而后在这个runnable
中加了一个死循环,死循环中执行了Looper.loop()
进行消息循环读取。这样就会致使后续全部的主线程消息都会走到咱们这个loop
方法中进行处理,也就是一旦发生了主线程崩溃,那么这里就能够进行异常捕获。同时由于咱们写的是while死循环,那么捕获异常后,又会开始新的Looper.loop()
方法执行。这样主线程的Looper就能够一直正常读取消息,主线程就能够一直正常运行了。
文字说不清楚的图片来帮咱们:
同时以前CrashHandler
的逻辑能够保证子线程也是不受崩溃影响,因此两段代码都加上,齐活了。
可是小光还不服气,他又想到了一种崩溃状况。。。
小光又又又一次实验
class Test2Activity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_exception) throw RuntimeException("主线程异常") } }
诶,我直接在onCreate
里面给你抛出个异常,运行看看:
黑漆漆的一片~没错,黑屏了。
最后的对话(Cockroach库思想)
- 看到这一幕,我主动找到了小光:
“这种状况确实比较麻烦了,若是直接在Activity
生命周期内抛出异常,会致使界面绘制没法完成,Activity
没法被正确启动,就会白屏或者黑屏了 这种严重影响到用户体验的状况仍是建议直接杀死APP
,由于颇有可能会对其余的功能模块形成影响。或者若是某些Activity不是很重要,也能够只finish
这个Activity
。”
- 小光思索地问: “那么怎么分辨出这种生命周期内发生崩溃的状况呢?”
“这就要经过反射了,借用Cockroach
开源库中的思想,因为Activity
的生命周期都是经过主线程的Handler
进行消息处理,因此咱们能够经过反射替换掉主线程的Handler中的Callback
回调,也就是ActivityThread.mH.mCallback
,而后针对每一个生命周期对应的消息进行trycatch捕获异常,而后就能够进行finishActivity
或者杀死进程操做了。”
主要代码:
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) { //android 28以后的生命周期处理 final int EXECUTE_TRANSACTION = 159; if (msg.what == EXECUTE_TRANSACTION) { try { mhHandler.handleMessage(msg); } catch (Throwable throwable) { //杀死进程或者杀死Activity } return true; } return false; } //android 28以前的生命周期处理 switch (msg.what) { case RESUME_ACTIVITY: //onRestart onStart onResume回调这里 try { mhHandler.handleMessage(msg); } catch (Throwable throwable) { sActivityKiller.finishResumeActivity(msg); notifyException(throwable); } return true;
代码贴了一部分,可是原理你们应该都懂了吧,就是经过替换主线程Handler
的Callback
,进行声明周期的异常捕获。
接下来就是进行捕获后的处理工做了,要不杀死进程,要么杀死Activity。
- 杀死进程,这个应该你们都熟悉
Process.killProcess(Process.myPid()) exitProcess(10)
- finish掉Activity
这里又要分析下Activity的finish
流程了,简单说下,以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); }
从Activity的finish源码
能够得知,最终是调用到ActivityTaskManagerService
的finishActivity
方法,这个方法有四个参数,其中有个用来标识Activity
的参数也就是最重要的参数——token
。因此去源码里面找找token~
因为咱们捕获的地方是在handleMessage
回调方法中,因此只有一个参数Message
能够用,那我么你就从这方面入手。回到刚才咱们处理消息的源码中,看看能不能找到什么线索:
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"); }
能够看到在源码中,Handler是怎么处理EXECUTE_TRANSACTION
消息的,获取到msg.obj
对象,也就是ClientTransaction
类实例,而后调用了execute
方法。而在execute
方法中。。。咦咦咦,这不就是token吗?
(找到的过于快速了哈,主要是activity
启动销毁这部分的源码解说并非今天的重点,因此就一笔带过了)
找到token
,那咱们就经过反射进行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); }
啊,终于搞定了,可是小光仍是一脸疑惑的看着我:
“我仍是去看Cockroach
库的源码吧~”
“我去,,”
总结
今天主要就说了一件事:如何捕获程序中的异常不让APP崩溃,从而给用户带来最好的体验。主要有如下作法:
- 经过在主线程里面发送一个消息,捕获主线程的异常,并在异常发生后继续调用
Looper.loop
方法,使得主线程继续处理消息。 - 对于子线程的异常,能够经过
Thread.setDefaultUncaughtExceptionHandler
来拦截,而且子线程的中止不会给用户带来感知。 - 对于在生命周期内发生的异常,能够经过替换
ActivityThread.mH.mCallback
的方法来捕获,而且经过token
来结束Activity或者直接杀死进程。
可能有的朋友会问,为何要让程序不崩溃呢?会有哪些状况须要咱们进行这样操做呢?
其实仍是有不少时候,有些异常咱们没法预料
或者给用户带来几乎是无感知
的异常,好比:
- 系统的一些bug
- 第三方库的一些bug
- 不一样厂商的手机带来的一些bug
等等这些状况,咱们就能够经过这样的操做来让APP
牺牲掉这部分的功能来维护系统的稳定性。
参考
Cockroach 一文读懂 Handler 机制全家桶 zyogte进程(Java篇) wanAndroid
拜拜
好了,到了说再见的时候了。
最后给你们推荐一个剧—棋魂,嘿嘿,小光就是里面的主角。
这些优秀的开源库又未尝不是指引咱们前行进步的光呢~
感谢你们的阅读,有一块儿学习的小伙伴能够关注下个人公众号——码上积木❤️❤️
每日三问知识点/面试题,聚沙成塔。