我们 APP 使用的热修复方案是 tinker,目前在适配 Android7.0 的时候遇到了一些问题,github 上微信又更新的比较慢,所以要再看看源码,自己改一下。
官方文档:
github: github.com/Tencent/tin…
1 添加依赖
使用 version catlog 进行依赖管理(Android 依赖管理及通用项目配置插件),在 toml 文件中,定义 tinker 版本和依赖。
[versions]
tinker = "1.9.14.19"
[libraries]
tinker-android-lib = { module = "com.tencent.tinker:tinker-android-lib", version.ref = "tinker" }
tinker-android-anno = { module = "com.tencent.tinker:tinker-android-anno", version.ref = "tinker" }
[plugins]
tinker = { id = "com.tencent.tinker.patch", version.ref = "tinker" }
复制代码
在 settings.gradle.kts 中,指定 tinker 插件的依赖。
pluginManagement {
repositories {
...
}
resolutionStrategy {
eachPlugin {
when (requested.id.id) {
...
"com.tencent.tinker.patch" -> {
useModule("com.tencent.tinker:tinker-patch-gradle-plugin:${requested.version}")
}
}
}
}
}
复制代码
在 app 模块的 build.gradle.kts 中,应用 tinker 插件,添加 tinker 依赖。
@file:Suppress("UnstableApiUsage")
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
...
alias(libs.plugins.tinker)
}
dependencies {
implementation(libs.tinker.android.lib)
compileOnly(libs.tinker.android.anno)
}
tinkerPatch {
oldApk = "${buildDir.path}/outputs/apk/xxx.apk"
// 补丁输出路径
outputFolder = "${buildDir.path}/bakApk/"
ignoreWarning = true
allowLoaderInAnyDex = true
removeLoaderForAllDex = true
useSign = true
tinkerEnable = true
buildConfig {
applyMapping = "${buildDir.path}/outputs/apk/release/mapping.txt"
applyResourceMapping = "${buildDir.path}/outputs/apk/release/resource_mapping.txt"
tinkerId = getTinkerIdValue()
isProtectedApp = false
supportHotplugComponent = false
keepDexApply = false
}
res {
pattern = listOf("res/*", "assets/*", "resources.arsc", "AndroidManifest.xml")
}
dex {
pattern = listOf("classes*.dex", "assets/secondary-dex-?.jar")
}
lib {
pattern = listOf("lib/*/*.so")
}
}
fun getTinkerIdValue(): String {
try {
return Runtime.getRuntime().exec("git rev-parse --short HEAD", null, project.rootDir)
.inputStream.reader().use { it.readText().trim() }
} catch (e: Exception) {
throw IllegalStateException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
复制代码
2 代码实现
新建 MainApplicationLike。
@DefaultLifeCycle(
application = "xx.xxx.MainApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false
)
class MainApplicationLike(
application: Application?,
tinkerFlags: Int,
tinkerLoadVerifyFlag: Boolean,
applicationStartElapsedTime: Long,
applicationStartMillisTime: Long,
tinkerResultIntent: Intent?
) : BaseTinkerApplication(
application,
tinkerFlags,
tinkerLoadVerifyFlag,
applicationStartElapsedTime,
applicationStartMillisTime,
tinkerResultIntent
) {
override fun onBaseContextAttached(base: Context?) {
super.onBaseContextAttached(base)
TinkerManager.setTinkerApplicationLike(this)
TinkerManager.initFastCrashProtect()
//should set before tinker is installed
TinkerManager.setUpgradeRetryEnable(true)
TinkerManager.installTinker(this)
}
override fun onCreate() {
super.onCreate()
}
}
复制代码
代码上主要需要处理 Tinker 的安装、补丁合成上报、补丁加载上报、异常处理等逻辑,新建 TinkerManager 进行初始化。
object TinkerManager {
private val TAG = "Tinker.TinkerManager"
private lateinit var applicationLike: ApplicationLike
private var uncaughtExceptionHandler: TinkerUncaughtExceptionHandler? = null
private var isInstalled = false
fun setTinkerApplicationLike(appLike: ApplicationLike) {
applicationLike = appLike
}
fun getTinkerApplicationLike(): ApplicationLike {
return applicationLike
}
fun initFastCrashProtect() {
if (uncaughtExceptionHandler == null) {
uncaughtExceptionHandler = TinkerUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler)
}
}
fun setUpgradeRetryEnable(enable: Boolean) {
UpgradePatchRetry.getInstance(applicationLike.application).setRetryEnable(enable)
}
/**
* all use default class, simply Tinker install method
*/
fun sampleInstallTinker(appLike: ApplicationLike?) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore")
return
}
TinkerInstaller.install(appLike)
isInstalled = true
}
/**
* you can specify all class you want.
* sometimes, you can only install tinker in some process you want!
*
* @param appLike
*/
fun installTinker(appLike: ApplicationLike) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore")
return
}
//or you can just use DefaultLoadReporter
val loadReporter: LoadReporter = TinkerLoadReporter(appLike.application)
//or you can just use DefaultPatchReporter
val patchReporter: PatchReporter = TinkerPatchReporter(appLike.application)
//or you can just use DefaultPatchListener
val patchListener: PatchListener = TinkerPatchListener(appLike.application)
//you can set your own upgrade patch if you need
val upgradePatchProcessor: AbstractPatch = UpgradePatch()
TinkerInstaller.install(appLike, loadReporter, patchReporter, patchListener, TinkerResultService::class.java, upgradePatchProcessor)
isInstalled = true
}
}
复制代码
捕获异常:
const val MAX_CRASH_COUNT = 3
class TinkerUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
private val TAG = "Tinker.SampleUncaughtExHandler"
private var ueh: Thread.UncaughtExceptionHandler? = null
private val QUICK_CRASH_ELAPSE = (10 * 1000).toLong()
private val DALVIK_XPOSED_CRASH = "Class ref in pre-verified class resolved to unexpected implementation"
fun SampleUncaughtExceptionHandler() {
ueh = Thread.getDefaultUncaughtExceptionHandler()
}
override fun uncaughtException(thread: Thread?, ex: Throwable) {
TinkerLog.e(TAG, "uncaughtException:" + ex.message)
tinkerFastCrashProtect()
tinkerPreVerifiedCrashHandler(ex)
ueh!!.uncaughtException(thread, ex)
}
/**
* Such as Xposed, if it try to load some class before we load from patch files.
* With dalvik, it will crash with "Class ref in pre-verified class resolved to unexpected implementation".
* With art, it may crash at some times. But we can't know the actual crash type.
* If it use Xposed, we can just clean patch or mention user to uninstall it.
*/
private fun tinkerPreVerifiedCrashHandler(ex: Throwable) {
val applicationLike = TinkerManager.getTinkerApplicationLike()
if (applicationLike == null || applicationLike.application == null) {
TinkerLog.w(TAG, "applicationlike is null")
return
}
if (!TinkerApplicationHelper.isTinkerLoadSuccess(applicationLike)) {
TinkerLog.w(TAG, "tinker is not loaded")
return
}
var throwable: Throwable? = ex
var isXposed = false
while (throwable != null) {
if (!isXposed) {
isXposed = TinkerUtils.isXposedExists(throwable)
}
// xposed?
if (isXposed) {
var isCausedByXposed = false
//for art, we can't know the actually crash type
//just ignore art
if (throwable is IllegalAccessError && throwable.message!!.contains(DALVIK_XPOSED_CRASH)) {
//for dalvik, we know the actual crash type
isCausedByXposed = true
}
if (isCausedByXposed) {
TinkerReporter.onXposedCrash()
TinkerLog.e(TAG, "have xposed: just clean tinker")
//kill all other process to ensure that all process's code is the same.
ShareTinkerInternals.killAllOtherProcess(applicationLike.application)
TinkerApplicationHelper.cleanPatch(applicationLike)
ShareTinkerInternals.setTinkerDisableWithSharedPreferences(applicationLike.application)
return
}
}
throwable = throwable.cause
}
}
/**
* if tinker is load, and it crash more than MAX_CRASH_COUNT, then we just clean patch.
*/
private fun tinkerFastCrashProtect(): Boolean {
val applicationLike = TinkerManager.getTinkerApplicationLike()
if (applicationLike == null || applicationLike.application == null) {
return false
}
if (!TinkerApplicationHelper.isTinkerLoadSuccess(applicationLike)) {
return false
}
val elapsedTime = SystemClock.elapsedRealtime() - applicationLike.applicationStartElapsedTime
//this process may not install tinker, so we use TinkerApplicationHelper api
if (elapsedTime < QUICK_CRASH_ELAPSE) {
val currentVersion = TinkerApplicationHelper.getCurrentVersion(applicationLike)
if (ShareTinkerInternals.isNullOrNil(currentVersion)) {
return false
}
val sp =
applicationLike.application.getSharedPreferences(ShareConstants.TINKER_SHARE_PREFERENCE_CONFIG, Context.MODE_MULTI_PROCESS)
val fastCrashCount = sp.getInt(currentVersion, 0) + 1
if (fastCrashCount >= MAX_CRASH_COUNT) {
TinkerReporter.onFastCrashProtect()
TinkerApplicationHelper.cleanPatch(applicationLike)
TinkerLog.e(TAG, "tinker has fast crash more than %d, we just clean patch!", fastCrashCount)
return true
} else {
sp.edit().putInt(currentVersion, fastCrashCount).commit()
TinkerLog.e(TAG, "tinker has fast crash %d times", fastCrashCount)
}
}
return false
}
}
复制代码
补丁合成监听:
class TinkerPatchReporter(context: Context) : DefaultPatchReporter(context) {
private val TAG = "Tinker.SamplePatchReporter"
override fun onPatchServiceStart(intent: Intent?) {
super.onPatchServiceStart(intent)
TinkerReporter.onApplyPatchServiceStart()
}
override fun onPatchDexOptFail(patchFile: File?, dexFiles: List<File?>?, t: Throwable) {
super.onPatchDexOptFail(patchFile, dexFiles, t)
TinkerReporter.onApplyDexOptFail(t)
}
override fun onPatchException(patchFile: File?, e: Throwable?) {
super.onPatchException(patchFile, e)
TinkerReporter.onApplyCrash(e)
}
override fun onPatchInfoCorrupted(patchFile: File?, oldVersion: String?, newVersion: String?) {
super.onPatchInfoCorrupted(patchFile, oldVersion, newVersion)
TinkerReporter.onApplyInfoCorrupted()
}
override fun onPatchPackageCheckFail(patchFile: File?, errorCode: Int) {
super.onPatchPackageCheckFail(patchFile, errorCode)
TinkerReporter.onApplyPackageCheckFail(errorCode)
}
override fun onPatchResult(patchFile: File?, success: Boolean, cost: Long) {
super.onPatchResult(patchFile, success, cost)
TinkerReporter.onApplied(cost, success)
}
override fun onPatchTypeExtractFail(patchFile: File?, extractTo: File?, filename: String?, fileType: Int) {
super.onPatchTypeExtractFail(patchFile, extractTo, filename, fileType)
TinkerReporter.onApplyExtractFail(fileType)
}
override fun onPatchVersionCheckFail(patchFile: File?, oldPatchInfo: SharePatchInfo?, patchFileVersion: String?) {
super.onPatchVersionCheckFail(patchFile, oldPatchInfo, patchFileVersion)
TinkerReporter.onApplyVersionCheckFail()
}
}
class TinkerPatchListener(context: Context) : DefaultPatchListener(context) {
private val TAG = "Tinker.SamplePatchListener"
protected val NEW_PATCH_RESTRICTION_SPACE_SIZE_MIN = (60 * 1024 * 1024).toLong()
private var maxMemory = (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).memoryClass
init {
TinkerLog.i(TAG, "application maxMemory:$maxMemory")
}
/**
* because we use the defaultCheckPatchReceived method
* the error code define by myself should after `ShareConstants.ERROR_RECOVER_INSERVICE
*
* path
* newPatch
` *
*/
override fun patchCheck(path: String?, patchMd5: String?): Int {
val patchFile = File(path)
TinkerLog.i(TAG, "receive a patch file: %s, file size:%d", path, SharePatchFileUtil.getFileOrDirectorySize(patchFile))
var returnCode = super.patchCheck(path, patchMd5)
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
returnCode = TinkerUtils.checkForPatchRecover(NEW_PATCH_RESTRICTION_SPACE_SIZE_MIN, maxMemory)
}
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
val sp = context.getSharedPreferences(ShareConstants.TINKER_SHARE_PREFERENCE_CONFIG, Context.MODE_MULTI_PROCESS)
//optional, only disable this patch file with md5
val fastCrashCount = sp.getInt(patchMd5, 0)
if (fastCrashCount >= MAX_CRASH_COUNT) {
returnCode = ERROR_PATCH_CRASH_LIMIT
}
}
// Warning, it is just a sample case, you don't need to copy all of these
// Interception some of the request
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
val properties = ShareTinkerInternals.fastGetPatchPackageMeta(patchFile)
if (properties == null) {
returnCode = ERROR_PATCH_CONDITION_NOT_SATISFIED
} else {
val platform = properties.getProperty(TinkerUtils.PLATFORM)
TinkerLog.i(TAG, "get platform:$platform")
// check patch platform require
if (platform == null || platform != BuildInfo.PLATFORM) {
returnCode = ERROR_PATCH_CONDITION_NOT_SATISFIED
}
}
}
TinkerReporter.onTryApply(returnCode == ShareConstants.ERROR_PATCH_OK)
return returnCode
}
}
复制代码
补丁加载监听:
class TinkerLoadReporter(context: Context) : DefaultLoadReporter(context) {
private val TAG = "Tinker.SampleLoadReporter"
override fun onLoadPatchListenerReceiveFail(patchFile: File?, errorCode: Int) {
super.onLoadPatchListenerReceiveFail(patchFile, errorCode)
TinkerReporter.onTryApplyFail(errorCode)
}
override fun onLoadResult(patchDirectory: File?, loadCode: Int, cost: Long) {
super.onLoadResult(patchDirectory, loadCode, cost)
when (loadCode) {
ShareConstants.ERROR_LOAD_OK -> TinkerReporter.onLoaded(cost)
}
Looper.myQueue().addIdleHandler {
if (UpgradePatchRetry.getInstance(context).onPatchRetryLoad()) {
TinkerReporter.onReportRetryPatch()
}
false
}
}
override fun onLoadException(e: Throwable, errorCode: Int) {
super.onLoadException(e, errorCode)
TinkerReporter.onLoadException(e, errorCode)
}
override fun onLoadFileMd5Mismatch(file: File?, fileType: Int) {
super.onLoadFileMd5Mismatch(file, fileType)
TinkerReporter.onLoadFileMisMatch(fileType)
}
/**
* try to recover patch oat file
*
* @param file
* @param fileType
* @param isDirectory
*/
override fun onLoadFileNotFound(file: File?, fileType: Int, isDirectory: Boolean) {
super.onLoadFileNotFound(file, fileType, isDirectory)
TinkerReporter.onLoadFileNotFound(fileType)
}
override fun onLoadPackageCheckFail(patchFile: File?, errorCode: Int) {
super.onLoadPackageCheckFail(patchFile, errorCode)
TinkerReporter.onLoadPackageCheckFail(errorCode)
}
override fun onLoadPatchInfoCorrupted(oldVersion: String?, newVersion: String?, patchInfoFile: File?) {
super.onLoadPatchInfoCorrupted(oldVersion, newVersion, patchInfoFile)
TinkerReporter.onLoadInfoCorrupted()
}
override fun onLoadInterpret(type: Int, e: Throwable?) {
super.onLoadInterpret(type, e)
TinkerReporter.onLoadInterpretReport(type, e)
}
override fun onLoadPatchVersionChanged(oldVersion: String?, newVersion: String?, patchDirectoryFile: File?, currentPatchName: String?) {
super.onLoadPatchVersionChanged(oldVersion, newVersion, patchDirectoryFile, currentPatchName)
}
}
复制代码
加载结果监听 service:
class TinkerResultService : DefaultTinkerResultService() {
private val TAG = "Tinker.SampleResultService"
override fun onPatchResult(result: PatchResult?) {
if (result == null) {
TinkerLog.e(TAG, "SampleResultService received null result!!!!")
return
}
TinkerLog.i(TAG, "SampleResultService receive result: %s", result.toString())
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(applicationContext)
val handler = Handler(Looper.getMainLooper())
handler.post {
if (result.isSuccess) {
Toast.makeText(applicationContext, "patch success, please restart process", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(applicationContext, "patch fail, please check reason", Toast.LENGTH_LONG).show()
}
}
// is success and newPatch, it is nice to delete the raw file, and restart at once
// for old patch, you can't delete the patch file
if (result.isSuccess) {
deleteRawPatchFile(File(result.rawPatchFilePath))
//not like TinkerResultService, I want to restart just when I am at background!
//if you have not install tinker this moment, you can use TinkerApplicationHelper api
if (checkIfNeedKill(result)) {
if (TinkerUtils.isBackground()) {
TinkerLog.i(TAG, "it is in background, just restart process")
restartProcess()
} else {
//we can wait process at background, such as onAppBackground
//or we can restart when the screen off
TinkerLog.i(TAG, "tinker wait screen to restart process")
TinkerUtils.ScreenState(applicationContext, object : TinkerUtils.IOnScreenOff {
override fun onScreenOff() {
restartProcess()
}
})
}
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!")
}
}
}
/**
* you can restart your process through service or broadcast
*/
private fun restartProcess() {
TinkerLog.i(TAG, "app is background now, i can kill quietly")
//you can send service or broadcast intent to restart your process
Process.killProcess(Process.myPid())
}
}
<service
android:name=".tinker.service.TinkerResultService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
复制代码
其他:
object BuildInfo {
var DEBUG: Boolean = BuildConfig.DEBUG
lateinit var VERSION_NAME: String
var VERSION_CODE: Int = 0
lateinit var MESSAGE: String
lateinit var TINKER_ID: String
lateinit var PLATFORM: String
fun initInfo(versionName: String, versionCode: Int, message: String, tinkerId: String, platform: String) {
this.VERSION_NAME = versionName
this.VERSION_CODE = versionCode
this.MESSAGE = message
this.TINKER_ID = tinkerId
this.PLATFORM = platform
}
}
object TinkerUtils {
private val TAG = "Tinker.Utils"
val PLATFORM = "platform"
val MIN_MEMORY_HEAP_SIZE = 45
private var background = false
fun isGooglePlay(): Boolean {
return false
}
fun isBackground(): Boolean {
return background
}
fun setBackground(back: Boolean) {
background = back
}
fun checkForPatchRecover(roomSize: Long, maxMemory: Int): Int {
if (isGooglePlay()) {
return ERROR_PATCH_GOOGLEPLAY_CHANNEL
}
if (maxMemory < MIN_MEMORY_HEAP_SIZE) {
return ERROR_PATCH_MEMORY_LIMIT
}
//or you can mention user to clean their rom space!
return if (!checkRomSpaceEnough(roomSize)) {
ERROR_PATCH_ROM_SPACE
} else ShareConstants.ERROR_PATCH_OK
}
fun isXposedExists(thr: Throwable): Boolean {
val stackTraces = thr.stackTrace
for (stackTrace in stackTraces) {
val clazzName = stackTrace.className
if (clazzName != null && clazzName.contains("de.robv.android.xposed.XposedBridge")) {
return true
}
}
return false
}
@Deprecated("")
fun checkRomSpaceEnough(limitSize: Long): Boolean {
var allSize: Long
var availableSize: Long = 0
try {
val data = Environment.getDataDirectory()
val sf = StatFs(data.path)
availableSize = sf.availableBlocks.toLong() * sf.blockSize.toLong()
allSize = sf.blockCount.toLong() * sf.blockSize.toLong()
} catch (e: Exception) {
allSize = 0
}
return if (allSize != 0L && availableSize > limitSize) {
true
} else false
}
fun getExceptionCauseString(ex: Throwable?): String? {
val bos = ByteArrayOutputStream()
val ps = PrintStream(bos)
return try {
// print directly
var t = ex
while (t!!.cause != null) {
t = t.cause
}
t.printStackTrace(ps)
toVisualString(bos.toString())
} finally {
try {
bos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun toVisualString(src: String?): String? {
var cutFlg = false
if (null == src) {
return null
}
val chr = src.toCharArray() ?: return null
var i = 0
while (i < chr.size) {
if (chr[i] > 127.toChar()) {
chr[i] = 0.toChar()
cutFlg = true
break
}
i++
}
return if (cutFlg) {
String(chr, 0, i)
} else {
src
}
}
class ScreenState(context: Context, onScreenOffInterface: IOnScreenOff?) {
init {
val filter = IntentFilter()
filter.addAction(Intent.ACTION_SCREEN_OFF)
context.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, `in`: Intent) {
val action = if (`in` == null) "" else `in`.action!!
TinkerLog.i(TAG, "ScreenReceiver action [%s] ", action)
if (Intent.ACTION_SCREEN_OFF == action) {
onScreenOffInterface?.onScreenOff()
}
context.unregisterReceiver(this)
}
}, filter)
}
}
interface IOnScreenOff {
fun onScreenOff()
}
}
复制代码