Android app Java层异常捕获方案

背景: 在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)
}
  • 10
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值