Tinker 热修复方案 - 接入

我们 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()
    }
}
复制代码

参考文档

  1. Tinker 接入指南
  2. Tinker 自定义扩展
  3. Tinker API概览
  4. 安卓App热补丁动态修复技术介绍

猜你喜欢

转载自juejin.im/post/7085543877641240613