经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
能否让APP永不崩溃—小光与我的对决
来源:cnblogs  作者:jimuzz  时间:2020/12/14 17:02:42  对本文有异议

前言

关于拦截异常,想必大家都知道可以通过Thread.setDefaultUncaughtExceptionHandler来拦截App中发生的异常,然后再进行处理。

于是,我有了一个不成熟的想法。。。

让我的APP永不崩溃

既然我们可以拦截崩溃,那我们直接把APP中所有的异常拦截了,不杀死程序。这样一个不会崩溃的APP用户体验不是杠杠的?

  • 有人听了摇摇头表示不赞同,这不小光跑来问我了:

“老铁,出现崩溃是要你解决它不是掩盖它!!”

  • 我拿把扇子扇了几下,有点冷但是故作镇定的说:

“这位老哥,你可以把异常上传到自己的服务器处理啊,你能拿到你的崩溃原因,用户也不会因为异常导致APP崩溃,这不挺好?”

  • 小光有点生气的说:

“这样肯定有问题,听着就不靠谱,哼,我去试试看”

小光的实验

于是小光按照网上一个小博主—积木的文章,写出了以下捕获异常的代码:

  1. //定义CrashHandler
  2. class CrashHandler private constructor(): Thread.UncaughtExceptionHandler {
  3. private var context: Context? = null
  4. fun init(context: Context?) {
  5. this.context = context
  6. Thread.setDefaultUncaughtExceptionHandler(this)
  7. }
  8. override fun uncaughtException(t: Thread, e: Throwable) {}
  9. companion object {
  10. val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
  11. CrashHandler() }
  12. }
  13. }
  14. //Application中初始化
  15. class MyApplication : Application(){
  16. override fun onCreate() {
  17. super.onCreate()
  18. CrashHandler.instance.init(this)
  19. }
  20. }
  21. //Activity中触发异常
  22. class ExceptionActivity : AppCompatActivity() {
  23. override fun onCreate(savedInstanceState: Bundle?) {
  24. super.onCreate(savedInstanceState)
  25. setContentView(R.layout.activity_exception)
  26. btn.setOnClickListener {
  27. throw RuntimeException("主线程异常")
  28. }
  29. btn2.setOnClickListener {
  30. thread {
  31. throw RuntimeException("子线程异常")
  32. }
  33. }
  34. }
  35. }

小光一顿操作,写下了整套代码,为了验证它的猜想,写了两种触发异常的情况:子线程崩溃和主线程崩溃

  • 运行,点击按钮2,触发子线程异常崩溃:

“咦,还真没啥影响,程序能继续正常运行”

  • 然后点击按钮1,触发主线程异常崩溃:

“嘿嘿,卡住了,再点几下,直接ANR了”

主线程崩溃

“果然有问题,但是为啥主线程会出问题呢?我得先搞懂再去找老铁对峙。”

小光的思考(异常源码分析)

首先科普下java中的异常,包括运行时异常非运行时异常

  • 运行时异常。是RuntimeException类及其子类的异常,是非受检异常,比如系统异常或者是程序逻辑异常,我们常遇到的有NullPointerException、IndexOutOfBoundsException等。遇到这种异常,Java Runtime会停止线程,打印异常,并且会停止程序运行,也就是我们常说的程序崩溃。

  • 非运行时异常。是属于Exception类及其子类,是受检异常,RuntimeException以外的异常。这类异常在程序中必须进行处理,如果不处理程序都无法正常编译,比如NoSuchFieldException,IllegalAccessException这种。

ok,也就是说我们抛出一个RuntimeException异常之后,所在的线程会被停止。如果主线程中抛出这个异常,那么主线程就会被停止,所以APP就会卡住无法正常操作,时间久了就会ANR。而子线程崩溃了并不会影响主线程也就是UI线程的操作,所以用户还能正常使用。

这样好像就说的通了。

等等,那为什么遇到setDefaultUncaughtExceptionHandler就不会崩溃了呢?

我们还得从异常的源码开始说起:

一般情况下,一个应用中所使用的线程都是在同一个线程组,而在这个线程组里只要有一个线程出现未被捕获异常的时候,JAVA 虚拟机就会调用当前线程所在线程组中的 uncaughtException()方法。

  1. // ThreadGroup.java
  2. private final ThreadGroup parent;
  3. public void uncaughtException(Thread t, Throwable e) {
  4. if (parent != null) {
  5. parent.uncaughtException(t, e);
  6. } else {
  7. Thread.UncaughtExceptionHandler ueh =
  8. Thread.getDefaultUncaughtExceptionHandler();
  9. if (ueh != null) {
  10. ueh.uncaughtException(t, e);
  11. } else if (!(e instanceof ThreadDeath)) {
  12. System.err.print("Exception in thread \""
  13. + t.getName() + "\" ");
  14. e.printStackTrace(System.err);
  15. }
  16. }
  17. }

parent表示当前线程组的父级线程组,所以最后还是会调用到这个方法中。接着看后面的代码,通过getDefaultUncaughtExceptionHandler获取到了系统默认的异常处理器,然后调用了uncaughtException方法。那么我们就去找找本来系统中的这个异常处理器——UncaughtExceptionHandler

这就要从APP的启动流程说起了,之前也说过,所有的Android进程都是由zygote进程fork而来的,在一个新进程被启动的时候就会调用zygoteInit方法,这个方法里会进行一些应用的初始化工作:

  1. public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
  2. if (RuntimeInit.DEBUG) {
  3. Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
  4. }
  5. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
  6. //日志重定向
  7. RuntimeInit.redirectLogStreams();
  8. //通用的配置初始化
  9. RuntimeInit.commonInit();
  10. // zygote初始化
  11. ZygoteInit.nativeZygoteInit();
  12. //应用相关初始化
  13. return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
  14. }

而关于异常处理器,就在这个通用的配置初始化方法当中:

  1. protected static final void commonInit() {
  2. if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
  3. //设置异常处理器
  4. LoggingHandler loggingHandler = new LoggingHandler();
  5. Thread.setUncaughtExceptionPreHandler(loggingHandler);
  6. Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
  7. //设置时区
  8. TimezoneGetter.setInstance(new TimezoneGetter() {
  9. @Override
  10. public String getId() {
  11. return SystemProperties.get("persist.sys.timezone");
  12. }
  13. });
  14. TimeZone.setDefault(null);
  15. //log配置
  16. LogManager.getLogManager().reset();
  17. //***
  18. initialized = true;
  19. }

找到了吧,这里就设置了应用默认的异常处理器——KillApplicationHandler

  1. private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
  2. private final LoggingHandler mLoggingHandler;
  3. public KillApplicationHandler(LoggingHandler loggingHandler) {
  4. this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
  5. }
  6. @Override
  7. public void uncaughtException(Thread t, Throwable e) {
  8. try {
  9. ensureLogging(t, e);
  10. //...
  11. // Bring up crash dialog, wait for it to be dismissed
  12. ActivityManager.getService().handleApplicationCrash(
  13. mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
  14. } catch (Throwable t2) {
  15. if (t2 instanceof DeadObjectException) {
  16. // System process is dead; ignore
  17. } else {
  18. try {
  19. Clog_e(TAG, "Error reporting crash", t2);
  20. } catch (Throwable t3) {
  21. // Even Clog_e() fails! Oh well.
  22. }
  23. }
  24. } finally {
  25. // Try everything to make sure this process goes away.
  26. Process.killProcess(Process.myPid());
  27. System.exit(10);
  28. }
  29. }
  30. private void ensureLogging(Thread t, Throwable e) {
  31. if (!mLoggingHandler.mTriggered) {
  32. try {
  33. mLoggingHandler.uncaughtException(t, e);
  34. } catch (Throwable loggingThrowable) {
  35. // Ignored.
  36. }
  37. }
  38. }

看到这里,小光欣慰一笑,被我逮到了吧。在uncaughtException回调方法中,会执行一个handleApplicationCrash方法进行异常处理,并且最后都会走到finally中进行进程销毁,Try everything to make sure this process goes away。所以程序就崩溃了。

关于我们平时在手机上看到的崩溃提示弹窗,就是在这个handleApplicationCrash方法中弹出来的。不仅仅是java崩溃,还有我们平时遇到的native_crash、ANR等异常都会最后走到handleApplicationCrash方法中进行崩溃处理。

另外有的朋友可能发现了构造方法中,传入了一个LoggingHandler,并且在uncaughtException回调方法中还调用了这个LoggingHandleruncaughtException方法,难道这个LoggingHandler就是我们平时遇到崩溃问题,所看到的崩溃日志?进去瞅瞅:

  1. private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
  2. public volatile boolean mTriggered = false;
  3. @Override
  4. public void uncaughtException(Thread t, Throwable e) {
  5. mTriggered = true;
  6. if (mCrashing) return;
  7. if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
  8. Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
  9. } else {
  10. StringBuilder message = new StringBuilder();
  11. message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
  12. final String processName = ActivityThread.currentProcessName();
  13. if (processName != null) {
  14. message.append("Process: ").append(processName).append(", ");
  15. }
  16. message.append("PID: ").append(Process.myPid());
  17. Clog_e(TAG, message.toString(), e);
  18. }
  19. }
  20. }
  21. private static int Clog_e(String tag, String msg, Throwable tr) {
  22. return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
  23. }

这可不就是吗?将崩溃的一些信息——比如线程,进程,进程id,崩溃原因等等通过Log打印出来了。来张崩溃日志图给大家对对看:

崩溃日志图

好了,回到正轨,所以我们通过setDefaultUncaughtExceptionHandler方法设置了我们自己的崩溃处理器,就把之前应用设置的这个崩溃处理器给顶掉了,然后我们又没有做任何处理,自然程序就不会崩溃了,来张总结图。

崩溃调用图

小光又来找我对峙了

  • 搞清楚这一切的小光又来找我了:

“老铁,你瞅瞅,这是我写的Demo和总结的资料,你那套根本行不通,主线程崩溃就GG了,我就说有问题吧”

  • 我继续故作镇定

“老哥,我上次忘记说了,只加这个UncaughtExceptionHandler可不行,还得加一段代码,发给你,回去试试吧”

  1. Handler(Looper.getMainLooper()).post {
  2. while (true) {
  3. try {
  4. Looper.loop()
  5. } catch (e: Throwable) {
  6. }
  7. }
  8. }

“这,,能行吗”

小光再次的实验

小光把上述代码加到了程序里面(Application—onCreate),再次运行:

我去,真的没问题了,点击主线程崩溃后,还是可以正常操作app,这又是什么原理呢?

小光的再次思考(拦截主线程崩溃的方案思想)

我们都知道,在主线程中维护着Handler的一套机制,在应用启动时就做好了Looper的创建和初始化,并且调用了loop方法开始了消息的循环处理。应用在使用过程中,主线程的所有操作比如事件点击,列表滑动等等都是在这个循环中完成处理的,其本质就是将消息加入MessageQueue队列,然后循环从这个队列中取出消息并处理,如果没有消息处理的时候,就会依靠epoll机制挂起等待唤醒。贴一下我浓缩的loop代码:

  1. public static void loop() {
  2. final Looper me = myLooper();
  3. final MessageQueue queue = me.mQueue;
  4. for (;;) {
  5. Message msg = queue.next();
  6. msg.target.dispatchMessage(msg);
  7. }
  8. }

一个死循环,不断取消息处理消息。再回头看看刚才加的代码:

  1. Handler(Looper.getMainLooper()).post {
  2. while (true) {
  3. //主线程异常拦截
  4. try {
  5. Looper.loop()
  6. } catch (e: Throwable) {
  7. }
  8. }
  9. }

我们通过Handler往主线程发送了一个runnable任务,然后在这个runnable中加了一个死循环,死循环中执行了Looper.loop()进行消息循环读取。这样就会导致后续所有的主线程消息都会走到我们这个loop方法中进行处理,也就是一旦发生了主线程崩溃,那么这里就可以进行异常捕获。同时因为我们写的是while死循环,那么捕获异常后,又会开始新的Looper.loop()方法执行。这样主线程的Looper就可以一直正常读取消息,主线程就可以一直正常运行了。

文字说不清楚的图片来帮我们:

同时之前CrashHandler的逻辑可以保证子线程也是不受崩溃影响,所以两段代码都加上,齐活了。

但是小光还不服气,他又想到了一种崩溃情况。。。

小光又又又一次实验

  1. class Test2Activity : AppCompatActivity() {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContentView(R.layout.activity_exception)
  5. throw RuntimeException("主线程异常")
  6. }
  7. }

诶,我直接在onCreate里面给你抛出个异常,运行看看:

黑漆漆的一片~没错,黑屏了

最后的对话(Cockroach库思想)

  • 看到这一幕,我主动找到了小光:

“这种情况确实比较麻烦了,如果直接在Activity生命周期内抛出异常,会导致界面绘制无法完成,Activity无法被正确启动,就会白屏或者黑屏了
这种严重影响到用户体验的情况还是建议直接杀死APP,因为很有可能会对其他的功能模块造成影响。或者如果某些Activity不是很重要,也可以只finish这个Activity。”

  • 小光思索地问:
    “那么怎么分辨出这种生命周期内发生崩溃的情况呢?”

“这就要通过反射了,借用Cockroach开源库中的思想,由于Activity的生命周期都是通过主线程的Handler进行消息处理,所以我们可以通过反射替换掉主线程的Handler中的Callback回调,也就是ActivityThread.mH.mCallback,然后针对每个生命周期对应的消息进行trycatch捕获异常,然后就可以进行finishActivity或者杀死进程操作了。”

主要代码:

  1. Field mhField = activityThreadClass.getDeclaredField("mH");
  2. mhField.setAccessible(true);
  3. final Handler mhHandler = (Handler) mhField.get(activityThread);
  4. Field callbackField = Handler.class.getDeclaredField("mCallback");
  5. callbackField.setAccessible(true);
  6. callbackField.set(mhHandler, new Handler.Callback() {
  7. @Override
  8. public boolean handleMessage(Message msg) {
  9. if (Build.VERSION.SDK_INT >= 28) {
  10. //android 28之后的生命周期处理
  11. final int EXECUTE_TRANSACTION = 159;
  12. if (msg.what == EXECUTE_TRANSACTION) {
  13. try {
  14. mhHandler.handleMessage(msg);
  15. } catch (Throwable throwable) {
  16. //杀死进程或者杀死Activity
  17. }
  18. return true;
  19. }
  20. return false;
  21. }
  22. //android 28之前的生命周期处理
  23. switch (msg.what) {
  24. case RESUME_ACTIVITY:
  25. //onRestart onStart onResume回调这里
  26. try {
  27. mhHandler.handleMessage(msg);
  28. } catch (Throwable throwable) {
  29. sActivityKiller.finishResumeActivity(msg);
  30. notifyException(throwable);
  31. }
  32. return true;

代码贴了一部分,但是原理大家应该都懂了吧,就是通过替换主线程HandlerCallback,进行声明周期的异常捕获。

接下来就是进行捕获后的处理工作了,要不杀死进程,要么杀死Activity。

  • 杀死进程,这个应该大家都熟悉
  1. Process.killProcess(Process.myPid())
  2. exitProcess(10)
  • finish掉Activity

这里又要分析下Activity的finish流程了,简单说下,以android29的源码为例。

  1. private void finish(int finishTask) {
  2. if (mParent == null) {
  3. if (false) Log.v(TAG, "Finishing self: token=" + mToken);
  4. try {
  5. if (resultData != null) {
  6. resultData.prepareToLeaveProcess(this);
  7. }
  8. if (ActivityTaskManager.getService()
  9. .finishActivity(mToken, resultCode, resultData, finishTask)) {
  10. mFinished = true;
  11. }
  12. }
  13. }
  14. }
  15. @Override
  16. public final boolean finishActivity(IBinder token, int resultCode, Intent resultData,
  17. int finishTask) {
  18. return mActivityTaskManager.finishActivity(token, resultCode, resultData, finishTask);
  19. }

从Activity的finish源码可以得知,最终是调用到ActivityTaskManagerServicefinishActivity方法,这个方法有四个参数,其中有个用来标识Activity的参数也就是最重要的参数——token。所以去源码里面找找token~

由于我们捕获的地方是在handleMessage回调方法中,所以只有一个参数Message可以用,那我么你就从这方面入手。回到刚才我们处理消息的源码中,看看能不能找到什么线索:

  1. class H extends Handler {
  2. public void handleMessage(Message msg) {
  3. switch (msg.what) {
  4. case EXECUTE_TRANSACTION:
  5. final ClientTransaction transaction = (ClientTransaction) msg.obj;
  6. mTransactionExecutor.execute(transaction);
  7. break;
  8. }
  9. }
  10. }
  11. public void execute(ClientTransaction transaction) {
  12. final IBinder token = transaction.getActivityToken();
  13. executeCallbacks(transaction);
  14. executeLifecycleState(transaction);
  15. mPendingActions.clear();
  16. log("End resolving transaction");
  17. }

可以看到在源码中,Handler是怎么处理EXECUTE_TRANSACTION消息的,获取到msg.obj对象,也就是ClientTransaction类实例,然后调用了execute方法。而在execute方法中。。。咦咦咦,这不就是token吗?

(找到的过于快速了哈,主要是activity启动销毁这部分的源码解说并不是今天的重点,所以就一笔带过了)

找到token,那我们就通过反射进行Activity的销毁就行啦:

  1. private void finishMyCatchActivity(Message message) throws Throwable {
  2. ClientTransaction clientTransaction = (ClientTransaction) message.obj;
  3. IBinder binder = clientTransaction.getActivityToken();
  4. Method getServiceMethod = ActivityManager.class.getDeclaredMethod("getService");
  5. Object activityManager = getServiceMethod.invoke(null);
  6. Method finishActivityMethod = activityManager.getClass().getDeclaredMethod("finishActivity", IBinder.class, int.class, Intent.class, int.class);
  7. finishActivityMethod.setAccessible(true);
  8. finishActivityMethod.invoke(activityManager, binder, Activity.RESULT_CANCELED, null, 0);
  9. }

啊,终于搞定了,但是小光还是一脸疑惑的看着我:

“我还是去看Cockroach库的源码吧~”

“我去,,”

总结

今天主要就说了一件事:如何捕获程序中的异常不让APP崩溃,从而给用户带来最好的体验。主要有以下做法:

  • 通过在主线程里面发送一个消息,捕获主线程的异常,并在异常发生后继续调用Looper.loop方法,使得主线程继续处理消息。
  • 对于子线程的异常,可以通过Thread.setDefaultUncaughtExceptionHandler来拦截,并且子线程的停止不会给用户带来感知。
  • 对于在生命周期内发生的异常,可以通过替换ActivityThread.mH.mCallback的方法来捕获,并且通过token来结束Activity或者直接杀死进程。但是这种办法要适配不同SDK版本的源码才行,所以慎用,需要的可以看文末Cockroach库源码。

可能有的朋友会问,为什么要让程序不崩溃呢?会有哪些情况需要我们进行这样操作呢?

其实还是有很多时候,有些异常我们无法预料或者给用户带来几乎是无感知的异常,比如:

  • 系统的一些bug
  • 第三方库的一些bug
  • 不同厂商的手机带来的一些bug

等等这些情况,我们就可以通过这样的操作来让APP牺牲掉这部分的功能来维护系统的稳定性。

参考

Cockroach
一文读懂 Handler 机制全家桶
zyogte进程(Java篇)
wanAndroid

拜拜

好了,到了说再见的时候了。

最后给大家推荐一个剧—棋魂,嘿嘿,小光就是里面的主角。

这些优秀的开源库又何尝不是指引我们前行进步的光呢~

有一起学习的小伙伴可以关注下??我的公众号——码上积木,每天剖析一个知识点,我们一起积累知识。公众号回复111可获得面试题《思考与解答》以往期刊。

原文链接:http://www.cnblogs.com/jimuzz/p/14113556.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号