热修复 - Tinker多渠道加固配置
# 一、问题
腾讯的热修复方案 Tinker 为加固应用提供了支持,需要在 gradle 脚本中,通过 isProtectedApp
配置当前的基准包(base apk)是否为加固 apk ,而这个配置是全局性的,Tinker 没有为多渠道提供单独的配置,这意味着,如果你的 app 工程在各个渠道不是全部统一使用加固或非加固的话,那么在为线上 apk 制作补丁包时,你不得不总要考虑是否需要修改 isProtectedApp
的值。为了提升工作效率,确保产出的补丁准确无误,非常有必要固化各渠道 isProtectedApp
的值。
# 二、摸索
先来初步认识一下 isProtectedApp
,它的作用是什么?下面是官方 demo tinker-sample-android
中对 isProtectedApp
的注释:
tinkerPatch {
buildConfig {
...
/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
*/
isProtectedApp = false // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
}
...
}
翻译过来就是说,isProtectedApp
的值是可选的,默认为 false
;作用是让 Tinker 知道是否应该将基准包(base apk)视为被加固工具加固过的受保护的 apk;如果值为 true
,则生成的补丁包将是 一个包含所有变更过的 class 的 dex 文件,而不是那些 dexdiff 补丁信息文件。简而言之,就是生成的补丁包文件会有不同。
接下来是要搞清楚 isProtectedApp
在 Tinker 内部是怎么用的,为了搞清这个问题,我 clone 了一份 Tinker 源码,研究了一下补丁的生成过程,下面是关键流程的分析与结论。
# 1、TinkerPatchPlugin
我们在生成补丁时,需要在 Gradle 面板中执行 tinkerPatchXXX 任务(比如:tinkerPatchRelease、tinkerPatchXiaomiRelease),然后等待 tinker 帮我们生成对应渠道的补丁包即可。该功能由 Tinker 开发的 Gradle 插件提供,对应的插件类就是 TinkerPatchPlugin
,源码如下:
class TinkerPatchPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
...
mProject.afterEvaluate {
...
android.applicationVariants.all { ApkVariant variant ->
...
// 创建 tinkerPatchXXX 任务
TinkerPatchSchemaTask tinkerPatchBuildTask = mProject.tasks.create("tinkerPatch${capitalizedVariantName}", TinkerPatchSchemaTask)
tinkerPatchBuildTask.signConfig = variant.signingConfig
variant.outputs.each { variantOutput ->
// setPatchNewApkPath() 内部有这么一段代码,作用是让 tinkerPatchXXX 任务 依赖于 assembleXXX 任务:
// tinkerPatchBuildTask.dependsOn Compatibilities.getAssembleTask(mProject, variant)
setPatchNewApkPath(configuration, variantOutput, variant, tinkerPatchBuildTask)
setPatchOutputFolder(configuration, variantOutput, variant, tinkerPatchBuildTask)
...
}
}
}
}
从上面的源码中,可以得知以下几点:
tinkerPatchXXX
任务的具体实现在TinkerPatchSchemaTask
类中。tinkerPatchXXX
任务依赖assembleXXX
任务,所以每次打补丁时,都会重新 build 一次。
# 2、TinkerPatchSchemaTask
来看看 tinkerPatchXXX
任务的具体实现类 TinkerPatchSchemaTask
:
public class TinkerPatchSchemaTask extends DefaultTask {
@Internal
TinkerPatchExtension configuration
TinkerPatchSchemaTask() {
...
configuration = project.tinkerPatch
}
@TaskAction
def tinkerPatch() {
InputParam.Builder builder = new InputParam.Builder()
...
for (def i = 0; i < newApks.size(); ++i) {
...
builder.setOldApk(oldApk.getAbsolutePath())
.setNewApk(newApk.getAbsolutePath())
...
.setIsProtectedApp(configuration.buildConfig.isProtectedApp)
InputParam inputParam = builder.create()
Runner.gradleRun(inputParam)
...
}
}
}
在 Gradle 中,一般任务(Task)会有继承自 DefaultTask
,被 @TaskAction
修饰的方法就是任务的执行逻辑。TinkerPatchSchemaTask
中被 @TaskAction
修饰的方法是 tinkerPatch()
,它就是 tinkerPatchXXX
任务的具体实现。在该方法中,我们看到了 isProtectedApp
被赋值到 InputParam
实例中,然后传递给 Runner.gradleRun(inputParam)
。
# 3、Runner
跟进到 Runner
类中,可以看到 inputParam
最终会被 Runner
实例的 mConfig
持有:
public class Runner {
protected Configuration mConfig;
public static void gradleRun(InputParam inputParam) {
Runner m = new Runner(true);
m.run(inputParam);
}
private void run(InputParam inputParam) {
loadConfigFromGradle(inputParam);
tinkerPatch();
}
private void loadConfigFromGradle(InputParam inputParam) {
...
mConfig = new Configuration(inputParam);
}
}
而 Runner.gradleRun(inputParam)
通过 run()
方法最终会触发到 tinkerPatch()
方法:
public class Runner {
protected Configuration mConfig;
...
protected void tinkerPatch() {
Logger.d("-----------------------Tinker patch begin-----------------------");
Logger.d(mConfig.toString());
try {
//gen patch
ApkDecoder decoder = new ApkDecoder(mConfig);
decoder.onAllPatchesStart();
decoder.patch(mConfig.mOldApkFile, mConfig.mNewApkFile);
decoder.onAllPatchesEnd();
//gen meta file and version file
PatchInfo info = new PatchInfo(mConfig);
info.gen();
//build patch
PatchBuilder builder = new PatchBuilder(mConfig);
builder.buildPatch();
} catch (Throwable e) {
goToError(e, ERRNO_USAGE);
}
Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin());
Logger.d("Tinker patch done, you can go to file to find the output %s", mConfig.mOutFolder);
Logger.d("-----------------------Tinker patch end-------------------------");
}
}
这个 tinkerPatch()
就是 Tinker 生成补丁包的核心方法了,分为 3 大部分:
ApkDecoder
:管理各Decoder
协同生成补丁文件(manifestDecoder
、dexPatchDecoder
、soPatchDecoder
、resPatchDecoder
)PatchInfo.gen()
:生成 meta 文件和 version 文件PatchBuilder.buildPatch()
:将上面的 补丁文件 和 信息文件 打包成补丁包、签名
而用到 isProtectedApp
的地方有两处,分别在 ApkDecoder
、PatchInfo
。
# 4、UniqueDexDiffDecoder & DexDiffDecoder
ApkDecoder
管理了各个 Decoder
,其中,dexPatchDecoder
是 UniqueDexDiffDecoder
实例:
public class ApkDecoder extends BaseDecoder {
private final UniqueDexDiffDecoder dexPatchDecoder;
public ApkDecoder(Configuration config) throws IOException {
dexPatchDecoder = new UniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE);
...
}
}
public class UniqueDexDiffDecoder extends DexDiffDecoder {
...
}
而 UniqueDexDiffDecoder
继承自 DexDiffDecoder
,dex 补丁生成的核心逻辑在 DexDiffDecoder
中:
public class DexDiffDecoder extends BaseDecoder {
@Override
public void onAllPatchesEnd() throws Exception {
...
if (config.mIsProtectedApp) {
// 对于加固app,则将变更的类以及相关信息写入到 patch dex
generateChangedClassesDexFile();
} else {
// 对于非加固app,则使用 dexdiff 算法生成 patch dex,补丁包更小
generatePatchInfoFile();
}
...
}
}
在 DexDiffDecoder
的 onAllPatchesEnd()
方法中使用到了 isProtectedApp
,用于分别对加固和非加固的基准包(base apk)生成 dex 补丁文件,不过,generateChangedClassesDexFile()
与 generatePatchInfoFile()
具体实现细节这里不展开,有兴趣的可以自己研究下,两者的区别看上述代码注释即可,这就是官方对 isProtectedApp
解释对应到的具体代码位置。
# 5、PatchInfo & PatchInfoGen
最后来看看另一处使用 isProtectedApp
的地方,PatchInfo
是 PatchInfoGen
的包装类,PatchInfo.gen()
方法最终调用的 PatchInfoGen.gen()
:
public class PatchInfo {
private final PatchInfoGen infoGen;
public PatchInfo(Configuration config) {
infoGen = new PatchInfoGen(config);
}
/**
* gen the meta file txt
* such as rev, version ...
* file version, hotpatch version class
*/
public void gen() throws Exception {
infoGen.gen();
}
}
public class PatchInfoGen {
...
public void gen() throws Exception {
addTinkerID();
addProtectedAppFlag();
...
}
private void addProtectedAppFlag() {
// If user happens to specify a value with this key, just override it for logic correctness.
config.mPackageFields.put(TypedValue.PKGMETA_KEY_IS_PROTECTED_APP, config.mIsProtectedApp ? "1" : "0");
}
}
在 PatchInfoGen.gen()
方法中调用了 addProtectedAppFlag()
方法,将 boolean 类型的 isProtectedApp
转换为数字 0
或 1
,最终会保存到 package_meta.txt
文件中。
# 三、解决方案
通过上面的源码分析,可以知道在生成补丁时,isProtectedApp
的两个作用:
- 供
DexDiffDecoder
判断具体的 dex 补丁生成方式 - 供
PatchInfoGen
确定is_protected_app
的值,最后记录在package_meta.txt
文件中
另外,在上述流程中,可以发现各环节使用的 isProtectedApp
的值,归根到底均来源于一处,即 TinkerPatchSchemaTask
类中的 configuration
属性,而 configuration
又是 project.tinkerPatch
的引用:
public class TinkerPatchSchemaTask extends DefaultTask {
@Internal
TinkerPatchExtension configuration
TinkerPatchSchemaTask() {
...
configuration = project.tinkerPatch
}
}
别忘了,TinkerPatchSchemaTask
对应的是 tinkerPatchXXX
任务,如果我们能在 tinkerPatchXXX
任务执行前,篡改掉 project.tinkerPatch
中 isProtectedApp
的值,那么后续各环节中所使用的就是被篡改后的 isProtectedApp
了。为了实现该功能,需要用到 Gradle 提供的 Task 相关的几个方法:
tasks.findByName()
:通过任务名字精确获取到对应的 task。task.doFirst{}
:在doFirst{}
闭包中编写的代码逻辑,会被放到 task 的执行阶段的最前面。afterEvaluate{}
:配置阶段完成后的监听回调。
结合上述几个 api,最终的 Gradle 代码如下:
apply from: 'configure/script/tinkerconfig.gradle' // Tinker 的 gradle 配置,同官方Demo
// 注意:以下代码必须放在 Tinker 配置之后,否则 tasks.findByName 找不到 tinkerPatchXXX 任务
afterEvaluate {
android.applicationVariants.all { variant ->
// println "tinkerPatchTask ----> ${tasks.findByName("tinkerPatch${variant.name.capitalize()}")}"
tasks.findByName("tinkerPatch${variant.name.capitalize()}").doFirst {
// 打印原本的 project.tinkerPatch.buildConfig 信息
println "original project.tinkerPatch --> ${project.tinkerPatch.buildConfig}"
// 渠道区分,xiaomi渠道的 base apk 会加固,其他渠道不加固
def isProtectedApp
if (variant.name.startsWith("xiaomi")) {
isProtectedApp = true
} else {
isProtectedApp = false
}
println "change ${variant.name}'s `isProtectedApp` --> ${isProtectedApp}"
project.tinkerPatch.buildConfig.isProtectedApp = isProtectedApp
}
}
}
# 四、方案优化
上述代码中,isProtectedApp
的赋值部分属于硬编码,还是有优化的空间的,你可以使用函数,结合闭包组合的方式,将 isProtectedApp
的赋值提前到 productFlavors
配置阶段,比如:
// build.gradle
apply from: 'configure/script/basic.gradle'
android {
productFlavors {
xiaomi profileCommon(
isProtectedApp: true
) >> {
buildConfigField "String", "USER_ID", '"GitLqr"'
}
googlePlay profileCommon(
isProtectedApp: false
) >> {
buildConfigField "String", "USER_ID", '"CharyLin"'
}
}
}
这样效果是不是更加直观了呢?下面是 basic.gradle
脚本中的代码:
// basic.gradle
project.ext.variantIsProtectedAppMap = [:]
project.ext.profileCommon = { profiles = [:] ->
return {
def flavorName = delegate.name
android.buildTypes.forEach {
def variant = flavorName + it.name.capitalize() // xiaomiRelease
project.ext.variantIsProtectedAppMap[variant] = profiles.getOrDefault('isProtectedApp', false)
}
}
}
apply from: 'configure/script/tinkerconfig.gradle'
afterEvaluate {
android.applicationVariants.all { variant ->
// println "tinkerPatchTask ----> ${tasks.findByName("tinkerPatch${variant.name.capitalize()}")}"
tasks.findByName("tinkerPatch${variant.name.capitalize()}").doFirst {
println "original project.tinkerPatch --> ${project.tinkerPatch.buildConfig}"
def isProtectedApp = variantIsProtectedAppMap[variant.name]
println "change ${variant.name}'s `isProtectedApp` --> ${isProtectedApp}"
project.tinkerPatch.buildConfig.isProtectedApp = isProtectedApp
}
}
}
至此,Tinker 的多渠道加固配置问题就解决了。如果你对 Gradle 的语法、插件、任务等概念不熟悉,可以阅读下列文章来学习 Gradle:
- 01
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08