背景: 在Android app运行中,有时一些无关紧要的异常出现时希望App 不崩溃,能继续让用户操作,可以有效提升用户体验和增加业务价值。
新流程:
哪些场景需要Catch
Crash Config配置信息:
支持从网络上获取Crash配置表,动态防护,避免crash。
使用: 在Application onCreate中调用:
CrashPortrayHelper.INSTANCE.init(this);
实现原理—源代码:
CrashPortray.kt
package com.mcd.library.crashProtect
import com.google.gson.annotations.SerializedName
import java.io.Serializable
data class CrashPortray(
@SerializedName("class_name")
val className: String = "",
val message: String = "",
val stack: List<String> = emptyList(),
@SerializedName("app_version")
val appVersion: List<String> = emptyList(),
@SerializedName("os_version")
val osVersion: List<Int> = emptyList(),
val model: List<String> = emptyList(),
val type: String = "all",
@SerializedName("clear_cache")
val clearCache: Int = 0,
@SerializedName("finish_page")
val finishPage: Int = 0,
val toast: String = ""
) : Serializable {
fun valid(): Boolean {
return className.isNotEmpty() || message.isNotEmpty() || stack.isNotEmpty()
}
}
CrashPortrayHelper.kt
package com.mcd.library.crashProtect
import android.app.Application
import android.content.Context
import android.os.Build
import com.mcd.library.AppConfigLib
import com.mcd.library.common.McdLifecycleCallback
import com.mcd.library.utils.CacheUtil
import com.mcd.library.utils.DialogUtil
import java.io.File
import java.lang.reflect.InvocationTargetException
object CrashPortrayHelper {
private var crashPortrayConfig: List<CrashPortray>? = null
private lateinit var application: Application
private lateinit var actionImpl: IApp
private const val crashProtectClosed: Boolean = false // 是否关闭该功能
fun init(application: Application) {
if (AppConfigLib.isDebugMode() || crashProtectClosed) { // debug模式下不进行初始化
return
}
CrashPortrayHelper.application = application
crashPortrayConfig = getCrashConfig()
actionImpl = getAppImpl()
CrashUncaughtExceptionHandler.init()
}
private fun getCrashConfig(): List<CrashPortray> {
// 从网络获取crash配置
val crashList = AppConfigLib.getCrashPortrays() ?: ArrayList()
//添加本地默认配置
crashList.apply {
this.addAll(getSystemException())
this.addAll(getRNException())
this.addAll(getSDKException())
}
return crashList
}
// 三方sdk异常
private fun getSDKException(): List<CrashPortray> {
val crashList = mutableListOf<CrashPortray>()
crashList.add(
CrashPortray(
className = "IllegalArgumentException",
message = "[PaymentActivity] not attached to window manager"
)
) // 支付页
crashList.add(CrashPortray(className = "EOFException")) //lottie
crashList.add(CrashPortray(className = "JsonEncodingException")) //lottie
return crashList
}
// 系统异常
private fun getSystemException(): List<CrashPortray> {
val crashList = mutableListOf<CrashPortray>()
crashList.add(CrashPortray(className = "BadTokenException"))
crashList.add(CrashPortray(className = "AssertionError"))
crashList.add(CrashPortray(className = "NoSuchMethodError"))
crashList.add(CrashPortray(className = "NoClassDefFoundError"))
crashList.add(CrashPortray(className = "CannotDeliverBroadcastException"))
crashList.add(CrashPortray(className = "OutOfMemoryError"))
crashList.add(CrashPortray(className = "DeadSystemRuntimeException"))
crashList.add(CrashPortray(className = "DeadSystemException"))
crashList.add(CrashPortray(className = "NullPointerException"))
crashList.add(CrashPortray(className = "TimeoutException"))
crashList.add(CrashPortray(className = "RemoteException"))
crashList.add(CrashPortray(className = "SecurityException"))
crashList.add(CrashPortray(className = "TransactionTooLargeException"))
crashList.add(CrashPortray(className = "SQLiteFullException"))
crashList.add(CrashPortray(className = "ConcurrentModificationException"))
crashList.add(CrashPortray(className = "InvocationTargetException"))
return crashList
}
// RN异常
private fun getRNException(): List<CrashPortray> {
val crashList = mutableListOf<CrashPortray>()
crashList.add(CrashPortray(className = "TooManyRequestsException"))
crashList.add(
CrashPortray(
className = "RuntimeException",
message = "Attempting to call JS function on a bad application bundle"
)
)
crashList.add(
CrashPortray(
className = "RuntimeException",
message = "Illegal callback invocation from native module"
)
)
crashList.add(
CrashPortray(
className = "CppException",
message = "facebook::react::Recoverable"
)
)
crashList.add(CrashPortray(className = "JavascriptException"))
crashList.add(
CrashPortray(
className = "UnsupportedOperationException",
message = "Tried to obtain display from a Context not associated with one"
)
)
crashList.add(CrashPortray(className = "MissingWebViewPackageException"))
return crashList
}
private fun getAppImpl(): IApp {
return object : IApp {
override fun showToast(context: Context, msg: String) {
DialogUtil.showShortPromptToast(context, msg)
}
override fun cleanCache(context: Context) {
CacheUtil.trimCache(context.applicationContext)
}
override fun finishCurrentPage() {
McdLifecycleCallback.getInstance().finishActivityWithNumber(1)
}
override fun getVersionName(context: Context): String =
AppConfigLib.getCurrentVersionName()
override fun downloadFile(url: String): File? {
return null
}
override fun readStringFromCache(key: String): String {
return ""
}
override fun writeStringToCache(file: File, content: String) {
}
}
}
fun needProtect(throwable: Throwable): Boolean {
val config: List<CrashPortray>? = crashPortrayConfig
if (config.isNullOrEmpty()) {
return false
}
kotlin.runCatching {
for (i in config.indices) {
val crashPortray = config[i]
if (!crashPortray.valid()) {
continue
}
//1. app 版本号
if (crashPortray.appVersion.isNotEmpty()
&& !crashPortray.appVersion.contains(actionImpl.getVersionName(application))
) {
continue
}
//2. os_version
if (crashPortray.osVersion.isNotEmpty()
&& !crashPortray.osVersion.contains(Build.VERSION.SDK_INT)
) {
continue
}
//3. model
if (crashPortray.model.isNotEmpty()
&& crashPortray.model.firstOrNull { Build.MODEL.equals(it, true) } == null
) {
continue
}
var throwableName = throwable.javaClass.simpleName
val message = throwable.message ?: ""
if (throwable.cause is InvocationTargetException) { // 处理原始异常(华为等机型)
throwableName = (throwable.cause as InvocationTargetException).targetException.javaClass.simpleName ?: ""
}
//4. class_name
if (crashPortray.className.isNotEmpty()
&& crashPortray.className != throwableName
) {
continue
}
//5. message
if (crashPortray.message.isNotEmpty() && !message.contains(crashPortray.message)
) {
continue
}
//6. stack
if (crashPortray.stack.isNotEmpty()) {
var match = false
throwable.stackTrace.forEach { element ->
val str = element.toString()
if (crashPortray.stack.find { str.contains(it) } != null) {
match = true
return@forEach
}
}
if (!match) {
continue
}
}
//7. 相应操作
if (crashPortray.clearCache == 1) {
actionImpl.cleanCache(application)
}
if (crashPortray.finishPage == 1) {
actionImpl.finishCurrentPage()
}
if (crashPortray.toast.isNotEmpty()) {
actionImpl.showToast(application, crashPortray.toast)
}
return true
}
}
return false
}
}
CrashUncaughtExceptionHandler.kt
package com.mcd.library.crashProtect
import android.os.Looper
import com.mcd.appcatch.AppInfoOperateProvider
import com.mcd.appcatch.appEvent.AppEventName
import com.mcd.library.utils.JsonUtil
object CrashUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
private var oldHandler: Thread.UncaughtExceptionHandler? = null
fun init() {
oldHandler = Thread.getDefaultUncaughtExceptionHandler()
oldHandler?.let {
Thread.setDefaultUncaughtExceptionHandler(this)
}
}
override fun uncaughtException(t: Thread, e: Throwable) {
if (CrashPortrayHelper.needProtect(e)) {
report(e)
bandage()
return
}
//旧的处理方式
oldHandler?.uncaughtException(t, e)
}
// crash 信息上报
private fun report(e: Throwable) {
kotlin.runCatching {
AppInfoOperateProvider.getInstance().saveEventInfo(
AppEventName.Crash.crash_protect_report,
System.currentTimeMillis(), e.message + JsonUtil.encode(e.stackTrace.take(5))) // 取message+异常堆栈前5条
}
}
/**
* 让主线程恢复运行
*/
private fun bandage() {
try {
if (Looper.myLooper() != Looper.getMainLooper()) {
return
}
Looper.loop()
} catch (e: Exception) {
uncaughtException(Thread.currentThread(), e)
}
}
}
IApp.kt
package com.mcd.library.crashProtect
import android.content.Context
import java.io.File
interface IApp {
fun showToast(context: Context, msg: String)
fun cleanCache(context: Context)
fun finishCurrentPage()
fun getVersionName(context: Context): String
fun downloadFile(url: String): File?
fun readStringFromCache(key : String): String
fun writeStringToCache(file: File, content: String)
}