The hot fix solution we use for our APP is tinker. Currently, we have encountered some problems when adapting to Android7.0. The update of wechat on github is relatively slow, so we need to look at the source code again and change it by ourselves.

Official documents:

Making: github.com/Tencent/tin…

1 Adding a Dependency

Use Version CatLog for dependency management (Android dependency Management and Common project configuration plug-in). In the TOML file, define the Tinker version and dependencies.

[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" }
Copy the code

In settings.gradle. KTS, specify the tinker plugin dependencies.

pluginManagement {
    repositories {
        ...
    }
    resolutionStrategy {
        eachPlugin {
            when (requested.id.id) {
                ...
                "com.tencent.tinker.patch" -> {
                    useModule("com.tencent.tinker:tinker-patch-gradle-plugin:${requested.version}")}}}}}Copy the code

In build.gradle. KTS of app module, apply tinker plug-in and add Tinker dependency.

@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" // patch output path 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'") } }Copy the code

2 Code implementation

New 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() } }Copy the code

The code mainly needs to handle Tinker installation, patch synthesis report, patch loading report, exception handling and other logic, and create TinkerManager for initialization.

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(a): ApplicationLike {
        return applicationLike
    }

    fun initFastCrashProtect(a) {
        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}}Copy the code

Catch exception:

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(a) {
        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 isIllegalAccessError && 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(a): 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}}Copy the code

Patch composition Monitor:

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
    }
}
Copy the code

Patch loading monitor:

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)
    }
}
Copy the code

Load result listener 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(a) {
                            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(a) {
        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"/>
Copy the code

Other:

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(a): Boolean {
        return false
    }

    fun isBackground(a): 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
        }
        valchr = 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(a)}}Copy the code

Reference documentation

  1. Tinker Access Guide
  2. Tinker custom extension
  3. Tinker API overview
  4. Introduction to hot patch dynamic repair technology of Android App