关于Android加快应用崩溃效率

解决问题:Android应用崩溃后出现ANR

应用上线前遇到问题,在应用崩溃后发生了ANR,而且发生ANR的就是本来已经崩溃的应用。由于本次ANR发生在集成了新的crash日志手机模块,在经过过甩锅–打脸的流程后开始认真分析ANR的原因。
崩溃日志收集代码如下:

class MyApplication: Application {

    override fun onCreate() {
        super.onCreate()
        /** 
         * 经过分析发现在集成的sdk中也包含
         * Thread.setDefaultUncaughtExceptionHandler()代码,
         * 三方SDK也有手机崩溃日志的习惯。
         * 而本次更新的就是我“优化”过的CrashHandler3
         */
        Thread.setDefaultUncaughtExceptionHandler(CrashHandler1())
        Thread.setDefaultUncaughtExceptionHandler(CrashHandler2())
        Thread.setDefaultUncaughtExceptionHandler(CrashHandler3(this))

        sendCrashLogFileToServer()
    }

}

/** 
 * "优化"过的CrashHandler3
 * 逻辑很简单,判断此次崩溃有没有Throwable对象:
 * 有,就有本地处理,处理完3秒后结束进程;
 * 没有,就使用系统的崩溃日志收集
 */
CrashHandler3(var context: Context): 
Thread.UncaughtExceptionHandler {

    private var defaultCrashHandler: UncaughtExceptionHandler
    init {
        defaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
    }
    override fun uncaughtException(t: Thread?, e: Throwable?) {

        if (!handlerExcception(e)) {
            defaultCrashHandler.uncaughtException(t, e)
        } else {
            try {
                Thread.sleep(3000)
            } catch (e: Exception?) {
                e?.printStackTrace()
            }
            android.os.Process.killProcess(android.os.Process.myPid())
            System.exit(1)
        }
        
    }


    private fun handlerException(e: Throwable?): Boolean {
        if (e == null) {
            return false
        } else {
            // TODO 显示Toast
            Handler(Looper.getMainLooper()).post {
                Toast.makeText(context, "应用崩溃", Toast.LENGTH_SHORT).show()
            }
            // TODO 从线程池中拿一个线程,来保存奔溃日志到文件
            saveCrashLogIntoFileWithThreadInThreadpool(e)
            return true
        }
    }
}

经过一宿的尝试,最终发现了问题在override fun uncaughtException(t: Thread?, e: Throwable?)方法上。

首先,查看Thread.setDefaultUncaughtExceptionHandler方法源码:

public class Thread implements Runnable {

    // other code ...


    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        defaultUncaughtExceptionHandler = eh; 
    }

    public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
        return defaultUncaughtExceptionHandler;
    }


    public interface UncaughtExceptionHandler {
        void uncaughtException(Thread t, Throwable e);
    }


     // other code ...
}

代码很明显的表示了CrashHandler保存在一个静态变量defaultUncaughtExceptionHandler里,每次调用setDefaultUncaughtExceptionHandler都会更新静态变量defaultUncaughtExceptionHandler,而在应用里有:

Thread.setDefaultUncaughtExceptionHandler(CrashHandler1())
Thread.setDefaultUncaughtExceptionHandler(CrashHandler2())
Thread.setDefaultUncaughtExceptionHandler(CrashHandler3(this))

最终确实CrashHandler1与CrashHandler2都能够执行,这是因为别人家的CrashHandler代码是这样:

class CrashHandler: Thread.UncaughtExceptionHandler {

    /** 
     * 定义UncaughtExceptionHandler类型的变量,用来保存当前线程的defaultCrashHandler,以后会用
     */
    private var defaultCrashHandler: UncaughtExceptionHandler
    init {
        defaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
    }

    override fun uncaughtException(t: Thread?, e: Throwable?) {
        // custom handle crash exception

        handleException(e)

        /** 
         * 这里是关键点
         * 因为在初始化自定义的CrashHandler前,
         * 保存了上次setDefaultUncaughtExceptionHandler后的defaultCrashHandler值
         * 自定义处理完CrashException后,在defaultCrashHandler不为null的情况下,
         * 调用上一个defaultCrashHandler的uncaughtException方法
         * 这样就不会影响其他三方sdk的CrashHandler的逻辑。
         * 而且,最关键的是,如果不主动调用defaultCrashHandler.uncaughtException(t, e),
         * Android应用就会出现ANR
         */
        if (defaultCrashHandler != null) {
            defaultCrashHandler.uncaughtException(t, e)
        } else {
            try {
                Thread.sleep(3000)
            } catch (e: Exception?) {
                e?.printStackTrace()
            }
            android.os.Process.killProcess(android.os.Process.myPid())
            System.exit(1)
        }

    }
}

还有一个关键点, volatile字段的使用方法:
假如有代码:

private int i = 301;
i = 302;
i = 304;

编译器在编译代码时会做代码优化,发现如上代码最终编译后的代码会变成:

private int i = 304;

即,直接赋最终值。

但是如果使用volatile字段来修饰:

private volatile int i = 301;
i = 302;
i = 304;

编译器不会最任何优化,编译后的代码依旧是:

private volatile int i = 301;
i = 302;
i = 304;

总结,volatile字段修饰的变量,保证了变量操作的原子性。
volatile字段还有一个特性,即可见性,
例如有变量:

private volatile int i = 301;

有线程A在启动,在无限循环中不断输出变量i,
然后执行 i++操作,此时i的值应该为302,
而线程A输出的只也会变为302,
如果变量不加volatile字段,则线程A会一直输出301。
不使用volatile字段的变量,CPU在运行代码时,会先将变量从内存读取到CPU cache,然后每次读取用CPU cache中的值
使用volatile字段的变量,CPU在运行代码时,会先将变量从内存读取变量直接运行