热修复——Tinker 的集成与使用,android 系统工程师面试题
添加 tinker-patch-gradle-plugin 的依赖
buildscript {dependencies {classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')}}
2、在 app 的 gradle 文件(app/build.gradle)中:
需要注意一点,Tinker 需要使用到 MulitDex,原话在[Bugly 文档的热更新 API 接口部分](
)。
1)添加 tinker 的库依赖
Gradle 版本小于 2.3 的这么写:
dependencies {compile "com.android.support:multidex:1.0.1"//可选,用于生成 application 类 provided('com.tencent.tinker:tinker-android-anno:1.9.1')//tinker 的核心库 compile('com.tencent.tinker:tinker-android-lib:1.9.1')}
Gradle 版本大等于 2.3 的这么写:
dependencies {implementation "com.android.support:multidex:1.0.1"//tinker 的核心库 implementation("com.tencent.tinker:tinker-android-lib:1.9.1") { changing = true }//可选,用于生成 application 类 annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }compileOnly("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }}
2)开启 multiDex
defaultConfig {...multiDexEnabled true}
3)应用 tinker 的 gradle 插件
这部分可先不管,在第三部分《Tinker 的配置及任务》的第 2 节《配置 Tinker 与任务》中会添加。可跳过这部分继续往下看。
//apply tinker 插件 apply plugin: 'com.tencent.tinker.patch
三、Tinker 的配置及任务
1、开启支持大工程模式
Tinker 文档中推荐将 jumboMode 设置为 true。
android {dexOptions {// 支持大工程模式 jumboMode = true}...}
2、配置 Tinker 与任务
将下面的配置全部复制粘贴到 app 的 gradle 文件(app/build.gradle)末尾,内容很多,但现在只需要看懂 bakPath 与 ext 括号内的东东就好了。
// Tinker 配置与任务 def bakPath = file("{buildDir}/bakApk/")ext {// 是否使用Tinker(当你的项目处于开发调试阶段时,可以改为false)tinkerEnabled = true// 基础包文件路径(名字这里写死为old-app.apk。用于比较新旧app以生成补丁包,不管是debug还是release编译)tinkerOldApkPath = "{bakPath}/old-app.apk"// 基础包的 mapping.txt 文件路径(用于辅助混淆补丁包的生成,一般在生成 release 版 app 时会使用到混淆,所以这个 mapping.txt 文件一般只是用于 release 安装包补丁的生成)tinkerApplyMappingPath = "{bakPath}/old-app-R.txt"//only use for build all flavor, if not, just ignore this fieldtinkerBuildFlavorDirectory = "${bakPath}/flavor"}
def getOldApkPath() {return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath}
def getApplyMappingPath() {return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath}
def getApplyResourceMappingPath() {return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath}
def getTinkerIdValue() {return hasProperty("TINKER_ID") ? TINKER_ID : android.defaultConfig.versionName}
def buildWithTinker() {return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled}
def getTinkerBuildFlavorDirectory() {return ext.tinkerBuildFlavorDirectory}
if (buildWithTinker()) {//apply tinker 插件 apply plugin: 'com.tencent.tinker.patch'
// 全局信息相关的配置项 tinkerPatch {tinkerEnable = buildWithTinker()// 是否打开 tinker 的功能。oldApk = getOldApkPath() // 基准 apk 包的路径,必须输入,否则会报错。ignoreWarning = false // 是否忽略有风险的补丁包。这里选择不忽略,当补丁包风险时会中断编译。useSign = true // 在运行过程中,我们需要验证基准 apk 包与补丁包的签名是否一致,我们是否需要为你签名。// 编译相关的配置项 buildConfig {applyMapping = getApplyMappingPath()// 可选参数;在编译新的 apk 时候,我们希望通过保持旧 apk 的 proguard 混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置 applyMapping 也不会影响任何的 assemble 编译。applyResourceMapping = getApplyResourceMappingPath()// 可选参数;在编译新的 apk 时候,我们希望通过旧 apk 的 R.txt 文件保持 ResId 的分配,这样不仅可以减少补丁包的大小,同时也避免由于 ResId 改变导致 remote view 异常。tinkerId = getTinkerIdValue()// 在运行过程中,我们需要验证基准 apk 包的 tinkerId 是否等于补丁包的 tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用 git 版本号、versionName 等等。keepDexApply = false// 如果我们有多个 dex,编译补丁时可能会由于类的移动导致变更增多。若打开 keepDexApply 模式,补丁包将根据基准包的类分布来编译。isProtectedApp = false // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。supportHotplugComponent = false // 是否支持新增非 export 的 Activity(1.9.0 版本开始才有的新功能)}// dex 相关的配置项 dex {dexMode = "jar"// 只能是'raw'或者'jar'。 对于'raw'模式,我们将会保持输入 dex 的格式。对于'jar'模式,我们将会把输入 dex 重新压缩封装到 jar。如果你的 minSdkVersion 小于 14,你必须选择‘jar’模式,而且它更省存储空间,但是验证 md5 时比'raw'模式耗时。默认我们并不会去校验 md5,一般情况下选择 jar 模式即可。pattern = ["classes*.dex","assets/secondary-dex-?.jar"]// 需要处理 dex 路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如 assets/...loader = [// 定义哪些类在加载补丁包的时候会用到。这些类是通过 Tinker 无法修改的类,也是一定要放在 main dex 的类。// 如果你自定义了 TinkerLoader,需要将它以及它引用的所有类也加入 loader 中;// 其他一些你不希望被更改的类,例如 Sample 中的 BaseBuildInfo 类。这里需要注意的是,这些类的直接引用类也需要加入到 loader 中。或者你需要将这个类变成非 preverify。]}// lib 相关的配置项 lib {pattern = ["lib//.so","src/main/jniLibs//.so"]// 需要处理 lib 路径,支持*、?通配符,必须使用'/'分割。与 dex.pattern 一致, 路径是相对安装包的,例如 assets/...}// res 相关的配置项 res {pattern = ["res/", "assets/", "resources.arsc", "AndroidManifest.xml"]// 需要处理 res 路径,支持*、?通配符,必须使用'/'分割。与 dex.pattern 一致, 路径是相对安装包的,例如 assets/...,务必注意的是,只有满足 pattern 的资源才会放到合成后的资源包。ignoreChange = [// 支持*、?通配符,必须使用'/'分割。若满足 ignoreChange 的 pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的情况,ignoreChange 与上面的 pattern 一致,即会完全忽略所有资源的修改。"assets/sample_meta.txt"]largeModSize = 100// 对于修改的资源,如果大于 largeModSize,我们将使用 bsdiff 算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为 100kb}// 用于生成补丁包中的'package_meta.txt'文件 packageConfig {// configField("key", "value"), 默认我们自动从基准安装包与新安装包的 Manifest 中读取 tinkerId,并自动写入 configField。// 在这里,你可以定义其他的信息,在运行时可以通过 TinkerLoadResult.getPackageConfigByName 得到相应的数值。// 但是建议直接通过修改代码来实现,例如 BuildConfig。configField("platform", "all")configField("patchVersion", "1.0")// configField("patchMessage", "tinker is sample to use")}// 7zip 路径配置项,执行前提是 useSign 为 truesevenZip {zipArtifact = "com.tencent.mm:SevenZip:1.1.10"}}List<String> flavors = new ArrayList<>();project.android.productFlavors.each { flavor ->flavors.add(flavor.name)}boolean hasFlavors = flavors.size() > 0def date = new Date().format("MMdd-HH-mm-ss")
/**
bak apk and mapping/android.applicationVariants.all { variant ->/*
task type, you want to bak*/def taskName = variant.name
tasks.all {if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {copy {def fileNamePrefix = "{variant.baseName}"def newFileNamePrefix = hasFlavors ? "{fileNamePrefix}-${date}"
def destPath = hasFlavors ? file("{project.name}-{variant.flavorName}") : bakPathfrom variant.outputs.first().outputFileinto destPathrename { String fileName ->fileName.replace("{newFileNamePrefix}.apk")}
from "{variant.dirName}/mapping.txt"into destPathrename { String fileName ->fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")}
from "{variant.dirName}/R.txt"into destPathrename { String fileName ->fileName.replace("R.txt", "{newFileNamePrefix}-R.txt")}}}}}}project.afterEvaluate {//sample use for build all flavor for one timeif (hasFlavors) {task(tinkerPatchAllFlavorRelease) {group = 'tinker'def originOldPath = getTinkerBuildFlavorDirectory()for (String flavor : flavors) {def tinkerTask = tasks.getByName("tinkerPatch{flavor.capitalize()}Release")dependsOn tinkerTaskdef preAssembleTask = tasks.getByName("process{flavor.capitalize()}ReleaseManifest")preAssembleTask.doFirst {String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)project.tinkerPatch.oldApk = "{originOldPath}/{project.name}-{originOldPath}/{project.name}-{originOldPath}/{project.name}-${flavorName}-release-R.txt"
}
}}
task(tinkerPatchAllFlavorDebug) {group = 'tinker'def originOldPath = getTinkerBuildFlavorDirectory()for (String flavor : flavors) {def tinkerTask = tasks.getByName("tinkerPatch{flavor.capitalize()}DebugManifest")preAssembleTask.doFirst {String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)project.tinkerPatch.oldApk = "{flavorName}/{flavorName}-debug.apk"project.tinkerPatch.buildConfig.applyMapping = "{flavorName}/{flavorName}-debug-mapping.txt"project.tinkerPatch.buildConfig.applyResourceMapping = "{flavorName}/{flavorName}-debug-R.txt"}
}}}}}
其中,有几点配置在这里说明一下,方便理解后续的操作(当 tinkerEnabled = true 的情况下):
app 的生成目录是:主 Module(一般是名为 app)/build/bakApk 文件夹。
补丁包的生成路径:主 Module(一般是名为 app)/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk。
基础包的名字:old-app.apk,放于 bakApk 文件夹下。
基础包的 mapping.txt 和 R.txt 文件一般在编译 release 签名的 apk 时才会用到。
在用到 mapping.txt 文件时,需要重命名为 old-app-mapping.txt,放于 bakApk 文件夹下。
在用到 R.txt 文件时,需要重命名为 old-app-R.txt,放于 bakApk 文件夹下。
对于 mapping.txt 和 R.txt 文件,在配置中有说明,请回配置中仔细看。
上面只是我项目中的配置,这些其实都是可以自定义的,建议在搞清楚配置内容之后再去自定义修改。
什么是基础包??
基础包就是已经上架的 apk 文件(假设是 1.0 版本)。这其实很好理解,在新版本的 App 上架之前(假设是 2.0 版本),我们会用到 Tinker 来修复 1.0 版 App 中存在的 bug,这时就需要用到 Tinker 来产生补丁包文件,而补丁包文件的本质,就是修复好 Bug 的 App 与 1.0 版本 App 之间的文件差异。在 2.0 版本上架之前,我们可能会多次产生新的补丁包,用于修复在用户手机上的 1.0 版 App,所以补丁包必须以 1.0 版 App 作为参考标准,也就是说用户手机上的 app 就是基础包,即当前应用市场上的 apk 文件(前面说的 1.0 版本)。
四、Tinker 封装与拓展
1、拷贝文件
将 Demo 中提供的 tinker 包下的所有文件及文件夹都拷贝到自己项目中。
这些文件其实就是 Tinker 官方 Demo 中的文件完全复制过来的,只是多加了一些注释。
简单说明下,这几个文件的作用:
SampleUncaughtExceptionHandler:Tinker 的全局异常捕获器。
MyLogImp:Tinker 的日志输出实现类。
SampleLoadReporter:加载补丁时的一些回调。
SamplePatchListener:过滤 Tinker 收到的补丁包的修复、升级请求。
SamplePatchReporter:修复或者升级补丁时的一些回调。
SampleTinkerReport:修复结果(成功、冲突、失败等)。
SampleResultService::patch 补丁合成进程将合成结果返回给主进程的类。
TinkerManager:Tinker 管理器(安装、初始化 Tinker)。
TinkerUtils:拓展补丁条件判定、锁屏或后台时应用重启功能的工具类。
这些只是对 Tinker 功能的拓展和封装罢了,都是可选的,但这些文件对项目的功能完善会有所帮助,建议加入到自己的项目中。
如果你仅仅只是为了修复 bug,而不做过多的工作(如:上传打补丁信息到服务器等),则无须理会这些文件的作用,当然你也可以自己封装。
对于这些自定义类及错误码的详细说明,请参考:[「Tinker 官方 Wiki:可选的自定义类」](
)。
2、清单文件中添加服务
前面添加的文件中,有一个 SampleResultService 文件,是四大组件之一,所以必须在清单文件中声明。
<serviceandroid:name="com.lqr.tinker.service.SampleResultService"android:exported="false"/>
五、编写 Application 的代理类
Tinker 表示,Application 无法动态修复,所以有两种选择:
使用「继承 TinkerApplication + DefaultApplicationLike」。
使用「DefaultLifeCycle 注解 + DefaultApplicationLike」。
当然,如果你觉得你自定义的 Application 不会用到热修复,可无视这部分;
但下方代码中的 initTinker()方法记得要拷贝到你项目中,用于初始化 Tinker。
第 1 种方式感觉比较鸡肋,这里使用第 2 种(Tinker 官方推荐的方式):「DefaultLifeCycle 注解 + TinkerApplicationLike」,DefaultLifeCycle 注解生成 Application,下面就来编写 Application 的代理类:
1、编写 TinkerApplicationLike
将下方的代码拷贝到项目中,注释简单明了,不多解释:
@SuppressWarnings("unused")@DefaultLifeCycle(application = "com.lqr.tinker.MyApplication",// application 类名。只能用字符串,这个 MyApplication 文件是不存在的,但可以在 AndroidManifest.xml 的 application 标签上使用(name)flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlagsloaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 我们这里使用默认即可!(可不写)loadVerifyFlag = fals
e)//tinkerLoadVerifyFlagpublic class TinkerApplicationLike extends DefaultApplicationLike {
private Application mApplication;private Context mContext;private Tinker mTinker;
// 固定写法 public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);}
// 固定写法 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {getApplication().registerActivityLifecycleCallbacks(callback);}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)@Overridepublic void onBaseContextAttached(Context base) {super.onBaseContextAttached(base);mApplication = getApplication();mContext = getApplication();initTinker(base);// 可以将之前自定义的 Application 中 onCreate()方法所执行的操作搬到这里...}
private void initTinker(Context base) {// tinker 需要你开启 MultiDexMultiDex.install(base);
TinkerManager.setTinkerApplicationLike(this);// 设置全局异常捕获 TinkerManager.initFastCrashProtect();//开启升级重试功能(在安装 Tinker 之前设置)TinkerManager.setUpgradeRetryEnable(true);//设置 Tinker 日志输出类 TinkerInstaller.setLogIml(new MyLogImp());//安装 Tinker(在加载完 multiDex 之后,否则你需要将 com.tencent.tinker.**手动放到 main dex 中)TinkerManager.installTinker(this);mTinker = Tinker.with(getApplication());}
}
2、搬运自定义 Application 中的操作
把项目中在自定义 Application 的操作移到 TinkerApplicationLike 的 onCreate()或 onBaseContextAttached()方法中。
public class TinkerApplicationLike extends DefaultApplicationLike {...@Overridepublic void onCreate() {super.onCreate();// 将之前自定义的 Application 中 onCreate()方法所执行的操作搬到这里...}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)@Overridepublic void onBaseContextAttached(Context base) {super.onBaseContextAttached(base);mApplication = getApplication();mContext = getApplication();initTinker(base);// 或搬到这里...}}
3、清单文件中注册
将 @DefaultLifeCycle 中 application 对应的值,即"com.lqr.tinker.MyApplication",赋值给清单文件的 application 标签的 name 属性,如下:
<applicationandroid:name="com.lqr.tinker.MyApplication"android:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme">...</application>
注意:
此时 name 属性会报红,因为项目源码中根本不存在 MyApplication.java 文件,但不必担心,因为它是动态生成的,Build 一下项目就好了,不管它也无所谓。
对于 Application 代理类的详细说明,请参考:[「Tinker 官方 Wiki:Application 代理类」](
)。
到这里就已经集成好 Tinker 了,但只是本地集成而已,服务端下发补丁包到 app 的文章之后会陆续发布更新。
六、常用 API
现在来了解下代码中会用到的几个 Tinker 的重要 API。
1、请求打补丁
TinkerInstaller.onReceiveUpgradePatch(context, 补丁包的本地路径);
2、卸载补丁
Tinker.with(getApplicationContext()).cleanPatch();// 卸载所有的补丁 Tinker.with(getApplicationContext()).cleanPatchByVersion(版本号)// 卸载指定版本的补丁
3、杀死应用的其他进程
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
4、Hack 方式修复 so
TinkerLoadLibrary.installNavitveLibraryABI(this, abi);
abi:cpu 架构类型
5、非 Hack 方式修复 so
TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "lib/" + abi, so 库的模块名); // 加载任意 abi 库 TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), so 库的模块名); // 只适用于加载 armeabi 库 TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), so 库的模块名); // 只适用于加载 armeabi-v7a 库
loadArmLibrary()与 loadArmV7Library()本质是调用了 loadLibraryFromTinker(),有兴趣的可以查看下源码。
对于 Tinker 所有 API 的详细说明,请参考:[「Tinker 官方 Wiki:Tinker-API 概览」](
)。
七、测试
因为布局简单且不是重点,这里就给出一张 Demo 的运行图片,剩下的就靠想像了。
1、编译基础包
没有基础包,那要补丁有什么用?所以,第一步就是打包一个 apk。
在 Terminal 中使用命令行./gradlew assembleDebug。不会命令行无所谓,Android Studio 为我们提供了图形化操作,根据下图操作即可:
如果你是要 release 签名的打包,则双击 assembleRelease,不过还要配置签名文件,这个后面再说。
编译完成后,可以在 build 目录下会自动创建一个 bakApk 文件夹,里面就有打包好的 apk 文件,因为之后的所有生成的补丁包都以这个 apk 会标准,所以这就是那个基础包文件(相当于应用市场上的 app)。
如果这个 apk 文件是 release 签名且是要放到应用市场上的,那么你必须将 apk 与 R.txt(如果有使用混淆的话,还会有一个 mapping.txt)这几个文件保存好,切记。
评论