乐趣区

关于android:能否让APP永不崩溃小光和我的对决

前言

对于 拦挡异样 ,想必大家都晓得能够通过Thread.setDefaultUncaughtExceptionHandler 来拦挡 App 中产生的异样,而后再进行解决。

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

让我的 APP 永不解体

既然咱们能够拦挡解体,那咱们间接把 APP 中所有的异样拦挡了,不杀死程序。这样一个 不会解体的 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("子线程异样")
            }
        }
    }
}

小光一顿操作,写下了整套代码,为了验证它的猜测,写了两种触发异样的状况:子线程解体和主线程解体

  • 运行,点击按钮 2,触发子线程异样解体:

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

  • 而后点击按钮 1,触发主线程异样解体:

“嘿嘿,卡住了,再点几下,间接 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;”> 主线程解体 </figcaption>

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

小光的思考(异样源码剖析)

首先科普下 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 回调办法中还调用了这个 LoggingHandleruncaughtException办法,难道这个 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;

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

接下来就是进行捕捉后的 解决工作 了,要不杀死过程,要么杀死 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 源码 能够得悉,最终是调用到 ActivityTaskManagerServicefinishActivity办法,这个办法有四个参数,其中有个用来标识 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 就义掉这部分的性能来保护零碎的稳定性。

我的库存,须要的小伙伴请点击我的 GitHub 收费支付

退出移动版