热修复 - Tinker多渠道加固配置
Posted GitLqr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了热修复 - Tinker多渠道加固配置相关的知识,希望对你有一定的参考价值。
欢迎关注微信公众号:FSA全栈行动 👋
一、问题
腾讯的热修复方案 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 // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
...
代码出自:https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle
翻译过来就是说,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:
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有ios, Python等文章, 可能有你想要了解的技能知识点哦~
以上是关于热修复 - Tinker多渠道加固配置的主要内容,如果未能解决你的问题,请参考以下文章