Android:How to dramatically reduce crashes

365 阅读3分钟

线上的崩溃是无法忍受的,那么有没有一种方法,将所有的崩溃进行 try catch,在开发过程中弹对话框提示,在线上环境上报进行分析。这样遇到崩溃时,结果就是用户的某些操作可能无效果,但相比崩溃,用户体验会好上很多。

思路

收到 github.com/jenly1314/N… 的启发。下面是具体的方法。

import android.app.Activity
import android.app.AlertDialog
import android.app.Application
import android.content.pm.ApplicationInfo
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import java.io.PrintWriter
import java.io.StringWriter

object CrashUtil {

    var resumeActivity: Activity? = null
    lateinit var application: Application
    var debug = false


    fun init(application: Application) {
        this.application = application
        debug = (application.applicationInfo.flags
                and ApplicationInfo.FLAG_DEBUGGABLE) != 0
        // 获得当前的 Activity
        application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks() {
            override fun onActivityResumed(activity: Activity?) {
                resumeActivity = activity
            }
        })

//         对主线程阻塞,通过下面的 loop 取出消息执行。
//         捕获主线程的异常
        Handler(Looper.getMainLooper()).post {
            while (true) {
                try {
                    Looper.loop()
                } catch (e: Throwable) {
                    //捕获异常处理
                    caughtException(Looper.getMainLooper().thread, e)
                }
            }
        }
        // 捕获子线程的异常
        Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler())
    }

    private class UncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
        override fun uncaughtException(t: Thread?, e: Throwable?) {
            caughtException(t, e)
        }
    }

    private fun caughtException(t: Thread?, e: Throwable?) {
        if (debug) {
            val sw = StringWriter()
            val pw = PrintWriter(sw)
            e?.printStackTrace(pw)
            val message = "线程信息: \n${t?.name}\n" +
                    "堆栈信息:\n $sw"
            Handler(Looper.getMainLooper()).post {
                AlertDialog.Builder(resumeActivity)
                    .setTitle("发生崩溃,信息如下")
                    .setMessage(message)
                    .setPositiveButton("确认", null)
                    .setCancelable(false)
                    .show()
            }
        } else {
            // report
        }
    }

    private open class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
        override fun onActivityPaused(activity: Activity?) {

        }

        override fun onActivityResumed(activity: Activity?) {
        }

        override fun onActivityStarted(activity: Activity?) {
        }

        override fun onActivityDestroyed(activity: Activity?) {
        }

        override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
        }

        override fun onActivityStopped(activity: Activity?) {
        }

        override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
        }
    }
}

在 init 函数中主要做了几个事情,

  1. 判断是否处于 debug 模式。
  2. 注册 activity 生命周期的监听器,获取 onResume 状态的 activity。
  3. 接管主线程的所有的消息执行,进行 try catch。
  4. 捕获所有非主线程的异常。

上述中着重解释第三点。

首先我们知道,app 的运行是从 ActivityThread 的 main 函数开始执行的。

public static void main(String[] args) {

		...
    Environment.initForCurrentUser();

		...
    Looper.prepareMainLooper();

		.....
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
		// loop
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

这里有一个 Looper.loop()。

进一步分析,这里会取出 MessageQueue 的下一个消息,如果消息为空,那么函数结束执行。这里就会有一个疑问,当 app 不执行任何操作时,但为什么 app 还在运行。

实际上queue.next()其实就是一个阻塞的方法,如果没有任务或没有主动退出,会一直在阻塞,一直等待主线程任务添加进来。

当队列有任务,就会打印信息 Dispatching to ...,然后就调用 msg.target.dispatchMessage(msg);执行任务,执行完毕就会打印信息 Finished to ...,我们就可以通过打印的信息来分析 ANR,一旦执行任务超过5秒就会触发系统提示ANR,但是我们对自己的APP肯定要更加严格,我们可以给我们设定一个目标,超过指定的时长就上报统计,帮助我们进行优化。

如果主线程发生了异常,就会退出循环,意味着APP崩溃,所以我们我们需要进行try-catch,避免APP退出,我们可以在主线程再启动一个 Looper.loop() 去执行主线程任务,然后try-catch这个Looper.loop()方法,就不会退出。

缺点

如果是在 Activity 的 onCreate 中发生崩溃,那么就会黑屏。但是这种情况基本在开发过程中都能发现,所以能大幅降低我们应用的崩溃率。

Message 的执行时间

在 Looper.loop() 方法中,会对消息的执行过程进行记录。

尤其是会打印出消息的执行时间。

因此我们可以根据时间差,来优化函数的执行。

public static void loop() {
				...
			 if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
  		if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
  			
}
if (debug) {
    Looper.getMainLooper().setMessageLogging {
        if (it.startsWith(">>>>> Dispatching to Handler")) {
            startTs = System.currentTimeMillis()
            msg = it
        } else if (it.startsWith("<<<<< Finished to Handler")) {
            val duration = System.currentTimeMillis() - startTs
            if (duration > 200) {
                Logger.d("主线程执行耗时过长\nduration:$duration\n msg:$msg")
            }
        }
    }
}

##MessageQueue

MessageQueue.next() 是一个带有阻塞的方法,只有退出或者有任务才会return,起阻塞的实现是使用Native层的 nativePollOnce() 函数,如果消息队列中没有消息存在nativePollOnce就不会返回,一直处于Native层等待状态。直到调用 quit() 退出或者调用 enqueueMessage(Message msg, long when) 有新的任务进来调用了Native层的nativeWake()函数,才会重新唤醒。

总结

性能优化的范围,大幅减少崩溃和优化函数的执行时间。