概述
看了这篇 jsonchao 的这篇神文 深入探索Android稳定性优化
很是佩服。
里面有讲到了容灾方案
的建设,总结了4条。分别是
- 功能开关
- 统跳中心
- 动态修复
- 安全模式
功能开关
想实现功能开关,App一般需要有长连接的能力,或者写个轮询替代。公司也要建设相应的参数配置平台。这样就可以实现定时更新参数配置的能力。
开发某个重要功能、改动范围很大的业务的时候,都可以加上一个开关。
开关默认是打开了(获取不到就使用默认值true),所以默认会走新功能的代码处理逻辑。
如果遇到新功能有异常,在参数平台配置紧急配置新功能开关为关闭。
统跳中心
在 OkHttp 里写一个拦截器,可以提前拦截到请求Response。
检测返回的状态码。比如约定返回 ret:10015 就弹一个吐司,吐司文案取返回里的 msg 字段。约定返回 ret:10016 就使用 ARouter 进行H5页面跳转,跳转的url取返回里的 url 字段。
动态修复
其实就是集成 Tinker 这类的热修复库,拥有热修复能力。
安全模式
Crash 里面最严重的当属启动Crash,如果用户打开app就崩,打开app就崩,是个严重事故不说,更麻烦的是用户才不会清理数据重新打开,只可能一怒之下把 App 删除。
文章里面讲到 微信读书、蘑菇街、淘宝、天猫等APP都使用了安全模式保障客户端启动流程,启动失败后给用户自救机会。
安全模式的核心特点:多次crash后重置为安装初始状态,严重Bug可阻塞性热修复。
根据文章的介绍,我自己写了个实现。
1. 核心处理类
package com.yao.mocklocation.tool
import android.annotation.SuppressLint
import android.content.Context
import android.os.Process
import android.util.Log
import com.yao.mocklocation.util.AppUtil
import kotlin.system.exitProcess
@SuppressLint("StaticFieldLeak")
object CrashHandler : Thread.UncaughtExceptionHandler {
private const val TAG = "CrashHandler"
private const val SAFE_MODE = "safe_mode"
private const val CRASH_FILE_NAME_1 = "crash_file_name_1"
private const val CRASH_FILE_NAME_2 = "crash_file_name_2"
private const val CRASH_FILE_NAME_3 = "crash_file_name_3"
var levelOneSafeMode = false
var levelTwoSafeMode = false
private var defaultCrashHandler: Thread.UncaughtExceptionHandler? = null
private lateinit var context: Context
fun init(context: Context) {
this.context = context
defaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(this)
// 清除安全模式缓存记录
Handler().postDelayed({
clear()
}, 60 * 1000L) // 时间应改成可配置
// 检查看是否要进入安全模式
if (contains(CRASH_FILE_NAME_1) && contains(CRASH_FILE_NAME_2)) {
if (contains(CRASH_FILE_NAME_3)) {
entryLevelTwoSafeMode()
} else {
entryLevelOneSafeMode()
}
}
}
override fun uncaughtException(thread: Thread, throwable: Throwable) {
process(throwable)
if (defaultCrashHandler != null) {
defaultCrashHandler?.uncaughtException(thread, throwable)
} else {
Process.killProcess(Process.myPid())
exitProcess(0)
}
}
private fun process(throwable: Throwable) {
if (contains(CRASH_FILE_NAME_1)) {
if (contains(CRASH_FILE_NAME_2)) {
if (!contains(CRASH_FILE_NAME_3)) {
saveCrash(CRASH_FILE_NAME_3, throwable)
}
} else {
saveCrash(CRASH_FILE_NAME_2, throwable)
}
} else {
saveCrash(CRASH_FILE_NAME_1, throwable)
}
}
private fun entryLevelOneSafeMode() {
levelOneSafeMode = true
Log.e(TAG, "进入一级安全模式")
}
private fun entryLevelTwoSafeMode() {
levelOneSafeMode = true
levelTwoSafeMode = true
Log.e(TAG, "进入二级安全模式")
}
private fun contains(key: String?): Boolean {
return context.getSharedPreferences(SAFE_MODE, Context.MODE_PRIVATE).contains(key)
}
public fun loadCrash(key: String?): String? {
return context.getSharedPreferences(SAFE_MODE, Context.MODE_PRIVATE).getString(key, null)
}
/**
* 保存crash
*/
private fun saveCrash(key: String?, throwable: Throwable): Boolean {
// 这个方法里,正确的做法是把crash日志、cpu占用、内存占用、其他线程的堆栈等一些相关信息一起保存成一个文件。
// 然后用SP里保存文件路径,注意要使用commit。
val crashLog = Log.getStackTraceString(throwable)
return context.getSharedPreferences(SAFE_MODE, Context.MODE_PRIVATE).edit().putString(key, crashLog).commit()
}
/**
* 当版本有升级或者热修复后,可以调用此方法清楚所有标记
*/
public fun clear() {
context.getSharedPreferences(SAFE_MODE, Context.MODE_PRIVATE).edit().clear().apply()
}
}
2. 初始化
比如在 Application 里面初始化
package com.yao.mocklocation.core
import android.app.Application
import com.yao.mocklocation.tool.CrashHandler
class App : Application() {
override fun onCreate() {
super.onCreate()
CrashHandler.init(applicationContext)
}
}
3. 进入安全模式后的处理
安全模式的判断和相应的处理都是根据具体业务场景来定的。
举个例子,一般我们首页就会去请求 feed 流、广告弹窗、今日任务之类的接口。所以一级安全模式(连续2次崩溃)可以拦截掉首页的非必要请求,对于 feed 流返回一个空数据列表。对于广告弹窗、今日任务返回一些特殊错误码,确保下方业务不会处理。
二级安全模式(连续3次崩溃),可以在 Splash 页进行阻塞请求热修复接口,如果有数据则必须等待下载完热修复包 + 热修复完成,然后重启App。如果没有数据(可能刚发生的事故,还没来得及热修复处理)则清除一些业务缓存数据。例如token,让用户重新走登录流程。
if (CrashHandler.levelOneSafeMode) {
// 写在 OkHttp 的拦截器里,拦截一些非必要请求,自己构建一个Response返回下方业务不会处理的错误码
}
if (CrashHandler.levelTwoSafeMode) {
// 在Splash页面阻塞请求热修复接口,如果有数据则必须热修复完成然后重启App。
// 如果热修复接口没有数据,则清除业务缓存。
}