异常崩溃是Android项目中一个棘手的问题,即使你做了很多的try - catch处理,也不能保证不崩溃,一旦崩溃就会出现下图的弹窗,xx应用就会停止运行这种体验对于用户来说是很差的,所以很明显我们做的app已经崩溃了。
像现在的企业应用,有的是在崩溃的时候直接启动一个统计异常的Activity,然后用户可以填写异常信息描述上报;还有就是直接闪退,不会出现如上图的弹窗,事实上用户 的体验觉会更糟,不知道它为什么闪退。
那异常可能随时发生,不能在每个代码块中去处理,肯定需要统一处理异常问题,这个就需要Java中的一个工具UncaughtExceptionHandler
1 UncaughtExceptionHandler
class AppCrashHandler : Thread.UncaughtExceptionHandler {
override fun uncaughtException(t: Thread, e: Throwable) {
}
}
UncaughtExceptionHandler是Java线程中的一个接口,它能够捕获到某个线程发生的异常。像try-catch是只能捕获主线程中的异常,子线程发送异常不会catch住,但是UncaughtExceptionHandler是可以捕获子线程中出现的异常的,当异常发生时,会回调uncaughtException方法,在这里可以做异常的上报。
1.1 替代Android异常机制
在文章的开头,我们看到Android中异常处理的机制就是闪退 + 弹窗,那么我们想自己处理异常并替换掉Android的处理方式,这个诉求其实Java中已经实现了,就是调用Thread的
setDefaultUncaughtExceptionHandler
class AppCrashHandler : Thread.UncaughtExceptionHandler {
private var context: Context? = null
fun init(context: Context) {
this.context = context
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(t: Thread, e: Throwable) {
Log.e(TAG, "thread name ${t.name} throw error ${e.message}")
}
companion object {
private const val TAG = "AppCrashHandler"
val instance: AppCrashHandler by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
AppCrashHandler()
}
}
}
这样我们在app中初始化这个AppCrashHandler,看异常信息能不能捕获到。
class MainActivity : AppCompatActivity() {
private lateinit var bigView: BigView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// bigView = findViewById(R.id.big_view)
bigView.setImageUrl(assets.open("mybg.png"))
}
}
这里我们没有初始化BigView,而是直接调用了它的一个方法,这里肯定是会报错的!运行之后,我们看到了一份日志信息
E/AppCrashHandler: thread name main throw error Unable to start activity ComponentInfo{
com.lay.image_process/com.lay.image_process.MainActivity
}:
kotlin.UninitializedPropertyAccessException:
lateinit property bigView has not been initialized
主线程抛出异常,原因就是bigView没有被初始化,这就说明异常是被捕获到了,而且我们会发现,app并没有闪退,这就是说明,我们已经替代了Android的异常处理方式。
1.2 可选择的异常处理
在第一部分中,我们捕获了异常,而应用程序没有不要闪回这真的是个好办法吗?事实上,我们可以尝试,返回和点击事件实际上并不 因为所有的进程都被终止了。
所以,捕获只是一部分,捕获后的处理也很重要,因为对于一些异常,我们不想自己处理它们,而是直接走系统的异常处理,其实这种风险就会降低,因为我们自己处理全部异常也不现实,也可能没有系统处理的好
defaultSystemExpHandler = Thread.getDefaultUncaughtExceptionHandler()
通过getDefaultUncaughtExceptionHandler()方法获取到的就是系统默认的异常处理对象,那么什么样的异常可以放给系统处理呢?在第一小节中,我们打印出的日志信息中发现uncaughtException捕获到的异常不是空的,那么有可能就是捕获到的异常是空的,那么就需要交给系统处理。
override fun uncaughtException(t: Thread, e: Throwable?) {
Log.e(TAG, "thread name ${t.name} throw error ${e?.message}")
if (e == null) {
defaultSystemExpHandler?.uncaughtException(t, e)
} else {
}
}
如果捕获的异常不为空,那么我们需要自己处理异常,当异常发生时,app的进程已经到了崩溃的边缘,已经处于无响应状态,为什么 单击没有响应,是因为事件传递没有起作用,如果我们了解Android的事件处理机制,就应该明白,在ActivityThread的main方法中,初始化了Looper并开启了死循环处理系统事件,那么这个时候,Looper肯定是不运转了,如果我们想要处理异常,需要再激活一个Looper
override fun uncaughtException(t: Thread, e: Throwable?) {
Log.e(TAG, "thread name ${t.name} throw error ${e?.message}")
if (e == null) {
defaultSystemExpHandler?.uncaughtException(t, e)
} else {
executors.execute {
Looper.prepare()
//处理异常
Toast.makeText(context, "系统崩溃了~", Toast.LENGTH_SHORT).show()
Looper.loop()
}
}
}
从上图中我们能够看到,Toast已经提示系统崩溃的异常。
2 日志上传
其实上传日志的方式有很多,比如Bugly、阿里云等直接上传在云端;还有各种方式保存在本地文件,通过用户触发回捞发送到日志群中,各种各样的方式都存在。
那么我们在上传日志的时候,信息要完整,这样才能直接定位异常,做出快速反应,因此在捕获到异常后,我们需要收集日志信息并上传。
2.1 日志收集
日志收集通常需要获取当前应用的包信息以及硬件设备信息,包信息获取很简单,Android已经有很成熟的API
private fun collectBaseInfo() {
//获取包信息
val packageManager = context?.packageManager
packageManager?.let {
try {
val packageInfo =
it.getPackageInfo(context?.packageName ?: "", PackageManager.GET_ACTIVITIES)
val versionName = packageInfo.versionName
val versionCode = packageInfo.versionCode
infoMap["versionName"] = versionName
infoMap["versionCode"] = versionCode.toString()
} catch (e: Exception) {
}
}
}
那么对于硬件设备信息,其实在Build中有对应的字段,但是没有取值的方法,因此需要通过反射来获取对应的值
//通过反射获取Build的全部参数
val fields = Build::class.java.fields
if (fields != null && fields.isNotEmpty()) {
fields.forEach { field ->
field.isAccessible = true
infoMap[field.name] = field.get(null).toString()
}
}
那么我们通过打印日志,可以看到基本的信息都已经有了
E/AppCrashHandler: info -- {
versionName=1.0,
versionCode=1,
BOARD=goldfish_x86,
BOOTLOADER=unknown,
BRAND=google,
CPU_ABI=x86,
CPU_ABI2=armeabi-v7a,
DEVICE=generic_x86_arm,
DISPLAY=sdk_gphone_x86_arm-userdebug 9 PSR1.180720.122 6736742 dev-keys,
FINGERPRINT=google/sdk_gphone_x86_arm/generic_x86_arm:9/PSR1.180720.122/6736742:userdebug/dev-keys,
HARDWARE=ranchu,
HOST=abfarm200,
ID=PSR1.180720.122,
IS_DEBUGGABLE=true,
IS_EMULATOR=true,
MANUFACTURER=Google,
MODEL=AOSP on IA Emulator,
PERMISSIONS_REVIEW_REQUIRED=false,
PRODUCT=sdk_gphone_x86_arm,
RADIO=unknown, SERIAL=unknown,
SUPPORTED_32_BIT_ABIS=[Ljava.lang.String;@1139408,
\SUPPORTED_64_BIT_ABIS=[Ljava.lang.String;@2a0a7a1,
SUPPORTED_ABIS=[Ljava.lang.String;
@9009dc6,
TAGS=dev-keys,
TIME=1596587219000,
TYPE=userdebug,
UNKNOWN=unknown,
USER=android-build
}
这样我们已经采集到了一些基础信息,接下来就需要上传日志
2.2 日志存储
当我们的应用程序发生异常的时候,这时候触发了全局异常捕获,收集到了日志信息,这个时候,可以选择将日志上传到数据库,或者存储在内存中。
其实这两者都有缺点,上传到数据库会有性能问题,存储在内存中有可能会丢失部分数据,所以建议大家使用一种稳妥的方式:先将日志存储文件在某个文件夹下,等下次app启动的时候,选择将该日志上传,然后清空文件夹。
首先uncaughtException捕获到的异常是Throwable,我们在Logcat中看到的出现异常之后的堆栈信息,其实就是保存在Throwable中的,所以在上传的日志中,需要将这些堆栈信息保存在文件中。
private fun saveErrorInfo(e: Throwable) {
val stringBuffer = StringBuffer()
infoMap.forEach { (key, value) ->
stringBuffer.append("$key == $value")
}
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
//获取到堆栈信息
e.printStackTrace(printWriter)
printWriter.close()
//转换异常信息
val errorStackInfo = stringWriter.toString()
stringBuffer.append(errorStackInfo)
Log.e(TAG, "error -- ${stringBuffer.toString()}")
}
从我们看到的堆栈信息中,我们可以看到有很多行,每行都对应一个行号告诉我们异常在哪里,因此我们通过StringWriter承接所有的堆栈信息,等到所有堆栈信息遍历完成,都保存在了StringWriter中。
versionName == 1.0
versionCode == 1
BOARD == goldfish_x86
BOOTLOADER == unknown
BRAND == google
CPU_ABI == x86
CPU_ABI2 == armeabi-v7a
DEVICE == generic_x86_arm
DISPLAY == sdk_gphone_x86_arm-userdebug 9 PSR1.180720.122 6736742 dev-keys
FINGERPRINT == google/sdk_gphone_x86_arm/generic_x86_arm:9/PSR1.180720.122/6736742:userdebug/dev-keys
HARDWARE == ranchu
HOST == abfarm200
ID == PSR1.180720.122
IS_DEBUGGABLE == true
IS_EMULATOR == true
MANUFACTURER == Google
MODEL == AOSP on IA Emulator
PERMISSIONS_REVIEW_REQUIRED == false
PRODUCT == sdk_gphone_x86_arm
RADIO == unknown
SERIAL == unknown
SUPPORTED_32_BIT_ABIS == [Ljava.lang.String;@9544e25
SUPPORTED_64_BIT_ABIS == [Ljava.lang.String;@e52bbfa
SUPPORTED_ABIS == [Ljava.lang.String;@bdc65ab
TAGS == dev-keys
TIME == 1596587219000
TYPE == userdebug
UNKNOWN == unknown
USER == android-build
----------------异常信息捕获-------------
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.lay.image_process/com.lay.image_process.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property bigView has not been initialized
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property bigView has not been initialized
at com.lay.image_process.MainActivity.onCreate(MainActivity.kt:16)
at android.app.Activity.performCreate(Activity.java:7136)
at android.app.Activity.performCreate(Activity.java:7127)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
然后将该文件保存到sd卡,具体的存储逻辑就不写了,很简单。
然后,我们在存储完日志信息之后呢,就需要将进程干掉,可选择将进程重启
//这里就是将进程干掉
android.os.Process.killProcess(android.os.Process.myPid())
//这里等价 System.exit(1) 进程被干掉后,然后重启
exitProcess(1)
关于是否需要重启,这个需要谨慎使用,如果app首页就发生崩溃,那么会进入死循环,一直杀掉进程然后重启!
3 策略设计模式实现上传功能
其实本地文件存储,其实只是一种方式,其实还有其他的方式,像上传到云端、发送短信等等,那么业务方在调用的时候,可以选择要实现的方式,所以这种多形态的处理方式可以采用策略设计模式
interface LogHelper {
fun upload(context: Context,listener: LogUploadListener)
}
策略设计模式,核心在于易扩展,因此接口不可缺少,任何实现的方式都需要实现这个接口
interface LogUploadListener {
fun loadSuccess()
fun loadFail(reason:String)
}
同时还需要一个上传日志的状态监听接口,回调给业务方日志是否上传成功。
class NetUploadHelper : LogHelper {
override fun upload(context: Context, listener: LogUploadListener) {
//模拟网络上传
Thread.sleep(1000)
listener.loadSuccess()
}
}
class SmsLoadHelper : LogHelper {
override fun upload(context: Context, listener: LogUploadListener) {
Thread.sleep(2000)
listener.loadFail("网络连接失败")
}
}
接着有两个实现类,用来做具体的上传逻辑处理,那么用户选择的方式就是在AppCrashHandler中开放入口
fun setUploadFunc(helper: LogHelper) {
this.helper = helper
}
context?.let {
helper?.upload(it,object : LogUploadListener{`在这里插入代码片`
override fun loadSuccess() {
Log.e(TAG,"loadSuccess")
}
override fun loadFail(reason: String) {
Log.e(TAG,"loadFail $reason")
}
})
}
在日志上传的时候,调用upload方法上传日志,具体的实现类是业务方自行选择的,假设我选择了发短信
AppCrashHandler.instance.setUploadFunc(SmsLoadHelper())
打印的日志如下:
E/AppCrashHandler: loadFail 网络连接失败
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持,想学习了解更多Android核心性能优化知识的可以点击链接获取《App性能优化学习手册》。