应用上线前遇到问题,在应用崩溃后发生了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在运行代码时,会先将变量从内存读取变量直接运行