【云+社区年度征文】能否让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了”
同时之前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
牺牲掉这部分的功能来维护系统的稳定性。
参考
拜拜
好了,到了说再见的时候了。
最后给大家推荐一个剧—棋魂,嘿嘿,小光就是里面的主角。
这些优秀的开源库又何尝不是指引我们前行进步的光呢~
感谢大家的阅读,有一起学习的小伙伴可以关注下我的公众号——码上积木❤️❤️
每日三问知识点/面试题,积少成多。
公众号回复“111”,还可以获取面试题《思考与解答》往期期刊哦~每月更新