Revisión de Tinker - Acceso

La solución hot-fix utilizada por nuestra aplicación es tinker. Actualmente, hemos encontrado algunos problemas al adaptarnos a Android 7.0. Wechat en github es relativamente lento para actualizar, por lo que debemos mirar el código fuente y cambiarlo usted mismo.

Documentación oficial:

github: github.com/Tencent/tin…

1 Agregar dependencias

Utilice el catálogo de versiones para la gestión de dependencias (Administración de dependencias de Android y complemento de configuración general del proyecto ), en el archivo toml, defina la versión de Tinker y las dependencias.

[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" }
复制代码

En settings.gradle.kts, especifique las dependencias del complemento Tinker.

pluginManagement {
    repositories {
        ...
    }
    resolutionStrategy {
        eachPlugin {
            when (requested.id.id) {
                ...
                "com.tencent.tinker.patch" -> {
                    useModule("com.tencent.tinker:tinker-patch-gradle-plugin:${requested.version}")
                }
            }
        }
    }
}
复制代码

En build.gradle.kts del módulo de la aplicación, aplique el complemento Tinker y agregue la dependencia 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 implementación de código

Cree un nuevo 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()
    }
}
复制代码

El código principalmente necesita lidiar con la lógica de la instalación de Tinker, informes de síntesis de parches, informes de carga de parches, manejo de excepciones, etc., y crear un nuevo TinkerManager para la inicialización.

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
    }
}
复制代码

Excepción de captura:

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
    }
}
复制代码

Monitor de síntesis de parches:

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
    }
}
复制代码

Monitoreo de carga de parches:

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

Servicio de escucha de resultados de carga:

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"/>
复制代码

otro:

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

Documentación de referencia

  1. Guía de acceso a Tinker
  2. Extensión personalizada Tinker
  3. Descripción general de la API Tinker
  4. Introducción de la tecnología de reparación dinámica Hot Patch de la aplicación Android

Supongo que te gusta

Origin juejin.im/post/7085543877641240613
Recomendado
Clasificación