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
- Tinker Access Guide
- Tinker custom extension
- Tinker API overview
- Introduction to hot patch dynamic repair technology of Android App