背景: 在Android app运行中,有时一些无关紧要的异常出现时希望App 不崩溃,能继续让用户操作,可以有效提升用户体验和增加业务价值。
新流程:
哪些场景需要Catch
Crash Config配置信息:
支持从网络上获取Crash配置表,动态防护,避免crash。
使用: 在Application onCreate中调用:
CrashPortrayHelper.INSTANCE.init(this);
实现原理—源代码:
CrashPortray.kt
package com.mcd.library.crashProtectimport com.google.gson.annotations.SerializedName
import java.io.Serializabledata 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.crashProtectimport 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.InvocationTargetExceptionobject CrashPortrayHelper {private var crashPortrayConfig: List<CrashPortray>? = nullprivate lateinit var application: Applicationprivate lateinit var actionImpl: IAppprivate const val crashProtectClosed: Boolean = false // 是否关闭该功能fun init(application: Application) {if (AppConfigLib.isDebugMode() || crashProtectClosed) { // debug模式下不进行初始化return}CrashPortrayHelper.application = applicationcrashPortrayConfig = 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")) //lottiecrashList.add(CrashPortray(className = "JsonEncodingException")) //lottiereturn 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>? = crashPortrayConfigif (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_versionif (crashPortray.osVersion.isNotEmpty()&& !crashPortray.osVersion.contains(Build.VERSION.SDK_INT)) {continue}//3. modelif (crashPortray.model.isNotEmpty()&& crashPortray.model.firstOrNull { Build.MODEL.equals(it, true) } == null) {continue}var throwableName = throwable.javaClass.simpleNameval message = throwable.message ?: ""if (throwable.cause is InvocationTargetException) { // 处理原始异常(华为等机型)throwableName = (throwable.cause as InvocationTargetException).targetException.javaClass.simpleName ?: ""}//4. class_nameif (crashPortray.className.isNotEmpty()&& crashPortray.className != throwableName) {continue}//5. messageif (crashPortray.message.isNotEmpty() && !message.contains(crashPortray.message)) {continue}//6. stackif (crashPortray.stack.isNotEmpty()) {var match = falsethrowable.stackTrace.forEach { element ->val str = element.toString()if (crashPortray.stack.find { str.contains(it) } != null) {match = truereturn@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.crashProtectimport android.os.Looper
import com.mcd.appcatch.AppInfoOperateProvider
import com.mcd.appcatch.appEvent.AppEventName
import com.mcd.library.utils.JsonUtilobject CrashUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {private var oldHandler: Thread.UncaughtExceptionHandler? = nullfun 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.crashProtectimport android.content.Context
import java.io.Fileinterface IApp {fun showToast(context: Context, msg: String)fun cleanCache(context: Context)fun finishCurrentPage()fun getVersionName(context: Context): Stringfun downloadFile(url: String): File?fun readStringFromCache(key : String): Stringfun writeStringToCache(file: File, content: String)
}