之前在公司的项目中集成微信的热修复/热更新平台-Tinker,也是我自己主动提出,然后慢慢研究。将近持续了两个星期的时间。这期间也踩过了很多坑,期间朋友和Tinker热修复讨论QQ群也对我多次进行解惑,因此打算整理出来,以泽同道!
主要功能点
既然说明了是完整版,就代表这和其他Tinker热修复平台相关文档有所不同。因为公司项目功能比较多,几乎涵盖了使用Tinker平台所能使用的大部分基础功能,主要包括:
- Tinker热修复基础功能
- 多渠道打包相关
- APK代码混淆
- APK加固支持
Tinker热修复基础功能
平台介绍
Tinker 是一个开源项目,它是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So 库以及资源,让应用能够在不需要重新安装的情况下实现更新。
为什么我们使用Tinker?
按照我自己使用的感受,1,使用简单;2,扩展性好;3,补丁包体积较小;
Tinker平台接入
1,平台注册 http://www.tinkerpatch.com/Index/login
获取Appkey
2,代码接入
2.1,添加依赖
Project gradle:
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.2.3' //无需再单独引用tinker的其他库 classpath("com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:${TINKERPATCH_VERSION}") { changing = true } } }
Module gradle:
dependencies { //分包:apply tinker插件 compile 'com.android.support:multidex:1.0.1' //tinker热修复核心库 compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") { changing = true } }
为了使用方便需要创建 tinkerpatch.gradle,存放Tinker相关配置,引入需添加:
apply from: 'tinkerpatch.gradle'
tinkerpatch.gradle:
apply plugin: 'tinkerpatch-support' /** * TODO: 请按自己的需求修改为适应自己工程的参数 */ def bakPath = file("${buildDir}/bakApk/") def baseInfo = "app-2.5.2-0113-11-26-00" def variantName = "release" /** * 对于插件各参数的详细解析请参考 * http://tinkerpatch.com/Docs/SDK */ tinkerpatchSupport { /** 可以在debug的时候关闭 tinkerPatch **/ /** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles, 这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加 你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro, 需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名, com.xxx前缀的包名不用修改 **/ //是否开启 tinkerpatchSupport 插件功能 tinkerEnable = isRelease() //是否反射 Application 实现一键接入 reflectApplication = true autoBackupApkPath = "${bakPath}" //TinkerPatch 平台 输入的版本号 appKey = "xxxxx4b69aef9687" /** 注意: 若发布新的全量包, appVersion一定要更新,与app VersionName保持一致 **/ appVersion = "2.5.2" def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/" def name = "${project.name}-${variantName}" baseApkFile = "${pathPrefix}/${name}.apk" baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt" baseResourceRFile = "${pathPrefix}/${name}-R.txt" /** 开启加固开关,上传此flavor的apk到加固网站进行加固 **/ protectedApp = true /** * 若有编译多flavors需求, 可以参照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample * 注意: 除非你不同的flavor代码是不一样的,不然建议采用zip comment或者文件方式生成渠道信息(相关工具:walle 或者 packer-ng) **/ } /** * 用于用户在代码中判断tinkerPatch是否被使能 */ android { defaultConfig { buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}" } } /** * 一般来说,我们无需对下面的参数做任何的修改 * 对于各参数的详细介绍请参考: * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 */ tinkerPatch { ignoreWarning = false useSign = true dex { dexMode = "jar" pattern = ["classes*.dex"] loader = [] } lib { pattern = ["lib/*/*.so"] } res { pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] ignoreChange = [] largeModSize = 100 } packageConfig { } sevenZip { zipArtifact = "com.tencent.mm:SevenZip:1.1.10" // path = "/usr/local/bin/7za" } buildConfig { keepDexApply = false } } import java.util.regex.Matcher import java.util.regex.Pattern /** * 如果只想在Release中打开tinker,可以把tinkerEnable赋值为这个函数的return * @return 是否为release */ def isRelease() { Gradle gradle = getGradle() String tskReqStr = gradle.getStartParameter().getTaskRequests().toString() Pattern pattern; if (tskReqStr.contains("assemble")) { println tskReqStr pattern = Pattern.compile("assemble(\\w*)(Release|Debug)") } else { pattern = Pattern.compile("generate(\\w*)(Release|Debug)") } Matcher matcher = pattern.matcher(tskReqStr) if (matcher.find()) { String task = matcher.group(0).toLowerCase() println("[BuildType] Current task: " + task) return task.contains("release") } else { println "[BuildType] NO MATCH FOUND" return true; } }
tinkerpatch.gradle中设置很重要,相关设置的含义在文档中已经做出了解释,这些说一下自己的见解
1,appVersion
这个是Tinker平台热修复需要指定的版本,和我们应用的VersionName没有任何关系,即
补丁下发-添加版本时,输入的App版本号,不过我还是将这个appVersion和我们应用的VersionName保持一致,每次发布版本时统一修改。(个人建议,仅供参考)。
2,def baseInfo = "app-2.5.2-0113-11-26-00"
每次生成补丁包之前,需将baseInfo 与 之前生成基准包后的build-bakApk目录下的文件名保持一致,不然生成的补丁包与基准包信息不一致,热修复失败。
Application:
public class App extends BaseApplication { private AppComponent appComponent; @Override public void onCreate() { super.onCreate(); init(); initTinker(); // initSensors(); initJPush(); initUmeng(); } /** * 初始化Tinker热更新配置 * <p> 目前存在限制 * 1,Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件; * 2,由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码; * 3,在Android N上,补丁对应用启动时间有轻微的影响; * 4,不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed"; * 5,由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker不再支持加固的动态更新; * 6,对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。 */ private void initTinker() { try { ApplicationLike tinkerApplicationLike; if (BuildConfig.TINKER_ENABLE) { tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike(); if (tinkerApplicationLike != null) TinkerPatch.init(tinkerApplicationLike) //是否自动反射Library路径,无须手动加载补丁中的So文件 //注意,调用在反射接口之后才能生效,你也可以使用Tinker的方式加载Library .reflectPatchLibrary() //向后台获取是否有补丁包更新,默认的访问间隔为3个小时 //若参数为true,即每次调用都会真正的访问后台配置 .fetchPatchUpdate(false) //设置访问后台补丁包更新配置的时间间隔,默认为3个小时 .setFetchPatchIntervalByHours(3) //向后台获得动态配置,默认的访问间隔为3个小时 //若参数为true,即每次调用都会真正的访问后台配置 .fetchDynamicConfig(new ConfigRequestCallback() { @Override public void onSuccess(HashMap<String, String> hashMap) { } @Override public void onFail(Exception e) { } }, false) //设置访问后台动态配置的时间间隔,默认为3个小时 .setFetchDynamicConfigIntervalByHours(3) //设置当前渠道号,对于某些渠道我们可能会想屏蔽补丁功能 //设置渠道后,我们就可以使用后台的条件控制渠道更新 .setAppChannel("default") //屏蔽部分渠道的补丁功能 .addIgnoreAppChannel("googleplay") //设置tinkerpatch平台的条件下发参数 //.setPatchCondition("user", MyConfig.getAuthorization()) //设置补丁合成成功后,锁屏重启程序 //默认是等应用自然重启 .setPatchRestartOnSrceenOff(true) //我们可以通过ResultCallBack设置对合成后的回调 //例如弹框什么 .setPatchResultCallback(new ResultCallBack() { @Override public void onPatchResult(PatchResult patchResult) { } }) //设置收到后台回退要求时,锁屏清除补丁 //默认是等主进程重启时自动清除 .setPatchRollbackOnScreenOff(true) //我们可以通过RollbackCallBack设置对回退时的回调 .setPatchRollBackCallback(new RollbackCallBack() { @Override public void onPatchRollback() { } }); } } catch (Exception e) { e.printStackTrace(); } } }
其他配置文件
gradle.properties:
TINKER_VERSION=1.8.0 TINKERPATCH_VERSION=1.1.8
BuildConfig:
// Fields from default config. public static final boolean TINKER_ENABLE = false;
3,基准包生成
基准包就是我们打包上线的Apk,所谓基准是相对与补丁包而言。
这里使用美团walle进行打包,在Android Studio 使用Terminal工具,输入gradlew clean assembleReleaseChannels
如果出现BUILD SUCCESSFUL表示打包成功
打包成功之后打开 app-build目录,就是我们打包成功的信息。
1,bakApk :基准包相关
2,outputs-channels :生成的多渠道包
4,生成补丁包:
在Gradle projects窗口,选中 project(xxx)-Tasks-tinker-tinkerPatchRelease,右键 点击run
然后等待执行完成即可,同样当出现BUILD SUCCESSFUL 表示生成补丁包成功。
补丁包位置:app-build-outputs-tinkerpatch
patch_signed_7zip.apk 就是此次基准包可以用于热修复的补丁包。
此次我修改了App.java文件中的一个Bug,我们现在看一下补丁包中都是有什么
在我们的包名路径下只有App文件,说明补丁包仅仅包括我们修改之后的应用与之前基准包有差异的文件。修改越少,补丁包体积越少,此次补丁包也仅有6.3KB。
5,补丁包在线参数
这是我之前的补丁详情:
我选择的是全量下发,Tinker支持补丁包的暂停/恢复下发,和补丁包删除。
Walle多渠道打包相关
如果我们没有接入热更新但有多渠道发布的功能,通常会接入Umeng 的多渠道打包功能。我也一样;但接入Tinker之后,就会疑惑,我如何针对所有的渠道基准包去生成补丁包,如果分开渠道打包,然后针对单渠道的基准包去生成补丁包的做法肯定是不可取的。Tinker官网推荐使用 [packer-ng-plugin]或者 walle 来进行多渠道打包,其中walle是支持最新的SchemaV2签名的。
我选择了Walle,下面介绍一下使用。
1,引入依赖
Project gradle:
dependencies { //美团walle多渠道打包 classpath 'com.meituan.android.walle:plugin:1.1.5' }
Module gradle:
dependencies { //美团多渠道 compile 'com.meituan.android.walle:library:1.1.5' } walle { // 指定渠道包的输出路径 apkOutputFolder = new File("${project.buildDir}/outputs/channels"); // 定制渠道包的APK的文件名称 apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk'; // 渠道配置文件 channelFile = new File("${project.getProjectDir()}/channel") }
2,配置多渠道文件
创建channel文件
3,打包
参考”Tinker热修复基础功能“中第三条:基准包生成。
再次补充一下打包不同的命令:
生成多渠道包: gradlew clean assembleReleaseChannels 支持 productFlavors: gradlew clean assembleMeituanReleaseChannels 生成单个渠道包: gradlew clean assembleReleaseChannels -PchannelList=oppo 生成多个渠道包: gradlew clean assembleReleaseChannels -PchannelList=oppo,vivo
APK代码混淆
这部分请按个人需要 参考:Android 代码混淆配置规范
APK加固支持
Tinker平台支持加固,选择加固厂商,需要加固厂商明确以下两点:
- 不能提前导入类;
- 在art平台若要编译oat文件,需要将内联取消。
官网推荐的包括:
乐加固
爱加密
梆梆加固
360加固
目前项目使用使用的主要包括:乐加固和360加固。
Tinker支持加固,主要在tinkerpatch.gradle文件中的 protectedApp 配置,需应用加固必须设置protectedApp=true。
不过注意:protectedApp=true, 这种模式仅仅可以使用在加固应用中
加固步骤:
- 生成开启
protectedApp = true
的基础包(这里假设此APK名为:protected.apk
); - 上传
protected.apk
到相应的加固网站进行加固,并发布应用市场(请遵循各个加固网站步骤,一般为下载加固包-》重新签名-》发布重签名加固包); - 在tinkerPatch后台根据appVersion建立相应的App版本(比如这里2个flavor,就需要建立2个App版本。App版本即为各自flavor中配置的appVersion);
- 基于各个flavor的基础包(这里的基础包是第一步中生成的未加固的版本)生成相应patch包,并上传至相应的App版本中,即完成补丁发布。