APP 接入 Bugly 热更新
公司最近项目在考虑加入热更新能力,综合目前开源的多个项目方案,我们采取了基于 Tinker 封装的 Bugly 热更新解决方案。
热补丁方案对比
主要参考阿里的 深入探索 Android 热修复技术原理
我们项目规模小,热修复实际运用场景很少,增加热补丁功能没有后台人力支援,没有项目经费;根据这些实际情况,我们选择了免费开源的 Tinker 项目,并使用了基于 Tinker 的免费后台发布系统 bugly。
Tinker 原理
Tinker 原理用官方的一张图展示
Bugly 介绍
Bugly 热更新能力目前采用微信 Tinker 开源方案,无需重新发版即可解决线上问题,同时 Bugly 提供热更新管理后台免费给开发者使用。
下面是 Bugly 官方给出的一些优势,可参考 Bugly Android 热更新指南 :
- 无需关注Tinker是如何合成补丁的
- 无需自己搭建补丁管理后台
- 无需考虑后台下发补丁策略的任何事情
- 无需考虑补丁下载合成的时机,处理后台下发的策略
- 提供了更加方便集成Tinker的方式
- 通过HTTPS及签名校验等机制保障补丁下发的安全性
- 丰富的下发维度控制,有效控制补丁影响范围
- 提供了应用升级一站式解决方案
Bugly 接入
第一步:添加依赖
工程根目录下 build.gradle
里添加
1 2 3 4 5 6 7 8 9 10 11
| buildscript { repositories { google() jcenter() } dependencies { ... classpath "com.tencent.bugly:tinker-support:1.1.5" ... } }
|
第二步:集成 SDK
在 app module 下的 build.gradle
文件中添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| android { ... // recommend dexOptions { jumboMode = true } defaultConfig { ... ndk { // 设置支持的SO库架构 abiFilters 'arm64-v8a', 'x86', 'armeabi-v7a', 'x86_64' } // 开启multidex multiDexEnabled true } } dependencies { ... compile "com.android.support:multidex:1.0.1" // 多dex配置 compile 'com.tencent.bugly:crashreport_upgrade:1.3.6' compile 'com.tencent.tinker:tinker-android-lib:1.9.9' compile 'com.tencent.bugly:nativecrashreport:latest.release' }
// 依赖插件脚本 apply from: 'tinker-support.gradle'
|
上面最后一行是将 tinker 相关的配置单独抽离出来,便于我们单独统一管理。我们在同级目录下新建 tinker-support.gradle
文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/** * 此处填写每次构建生成的基准包目录 */ def baseApkDir = "app-1205-11-28-48"
/** * 对于插件各参数的详细解析请参考 */ tinkerSupport {
// 开启tinker-support插件,默认值true enable = true
// 指定归档目录,默认值当前module的子目录tinker autoBackupApkDir = "${bakPath}"
// 是否启用覆盖tinkerPatch配置功能,默认值false // 开启后tinkerPatch配置不生效,即无需添加tinkerPatch overrideTinkerPatchConfiguration = true
/** * 单渠道apk 打补丁包配置 */ // 编译补丁包时,必需指定基线版本的apk,默认值为空 // 如果为空,则表示不是进行补丁包的编译 // @{link tinkerPatch.oldApk } baseApk = "${bakPath}/${baseApkDir}/app-release.apk"
// 对应tinker插件applyMapping baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"
// 对应tinker插件applyResourceMapping baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"
/** * 多渠道apk 构建补丁时使用 */ // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性 tinkerId = "patch-1.0.test"
// 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持) // isProtectedApp = true
// 是否开启反射Application模式 enableProxyApplication = false
// 是否支持新增非export的Activity(注意:设置为true才能修改AndroidManifest文件) supportHotplugComponent = true
}
/** * 一般来说,我们无需对下面的参数做任何的修改 * 对于各参数的详细介绍请参考: * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 */ tinkerPatch { //oldApk ="${bakPath}/${appName}/app-release.apk" 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 //tinkerId = "1.0.1-base" // 可选,设置mapping文件,建议保持旧apk的proguard混淆方式 //applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配 //applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" } }
|
注意:详细配置可参考 tinker-support 配置说明
这里面有针对单渠道的 apk 打补丁包 以及 多渠道的补丁包,下面会分别介绍。
第三步 初始化 SDK
初始化时包含两种接入,即 enableProxyApplication
为 true 和 false 两种情况。
推荐是 enableProxyApplication=false
,有一定的接入成本,但是兼容性更好。
enableProxyApplication=false
的情况
主要是做了两件事情
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| /** * 注意:这个类集成TinkerApplication类,这里面不做任何操作 * 所有Application的代码都会放到ApplicationLike继承类当中 * * 参数解析: * 参数1:int tinkerFlags 表示Tinker支持的类型 * dex only、library only or all suuport,default: TINKER_ENABLE_ALL * 参数2:String delegateClassName Application代理类 * 这里填写你自定义的ApplicationLike * 参数3:String loaderClassName Tinker的加载器,使用默认即可 * 参数4:boolean tinkerLoadVerifyFlag 加载dex或者lib是否验证md5,默认为false */ public class AppApplication extends TinkerApplication {
public AppApplication() { super(ShareConstants.TINKER_ENABLE_ALL, "com.shopee.fms.common.AppApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false); } }
|
这里记得在 AndroidManifest.xml
中配置成新的 application
这里要注意几点:
1 2 3 4
| - 将我们自己 Application 类以及它的继承类的所有代码拷贝到自己的 ApplicationLike 继承类中 - Application 的 attachBaseContext 方法实现要单独移动到 onBaseContextAttached 中 - 在 ApplicationLike 中,引用 application 的地方改成 getApplication() - 对其他引用 Application 或它的静态对象与方法的地方,改成引用 ApplicationLike 的静态对象与方法
|
更详细的可以参考 Tinker 官方的做法 SampleApplicationLike
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| public class AppApplicationLike extends DefaultApplicationLike {
public static final String TAG = "Tinker.SampleApplicationLike"; private static Application app;
public AppApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) { super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent); app = getApplication(); }
public static synchronized Application getApplicationContext() { if (app == null) { app = new AppApplication(); } return app; }
@Override public void onCreate() { super.onCreate(); initBuglySdk(); }
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); // you must install multiDex whatever tinker is installed! MultiDex.install(base) // 安装tinker // TinkerManager.installTinker(this); 替换成下面Bugly提供的方法 Beta.installTinker(this); }
@Override public void onTerminate() { super.onTerminate(); Beta.unInit(); }
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); }
/** * 初始化 Bugly SDK */ private void initBuglySdk() {
// 设置是否开启热更新能力,默认为true Beta.enableHotfix = true; // 设置是否自动下载补丁,默认为true Beta.canAutoDownloadPatch = true; // 设置是否自动合成补丁,默认为true Beta.canAutoPatch = true; // 设置是否提示用户重启,默认为false Beta.canNotifyUserRestart = true; // 设置开发设备,默认为false,上传补丁如果下发范围指定为“开发设备”,需要调用此接口来标识开发设备 Bugly.setIsDevelopmentDevice(getApplication(), true);
// CrashReport.initCrashReport(getApplication(), "a3a0d5c1aa", true); Bugly.init(getApplication(), "a3a0d5c1aa", true); Task.callInBackground(new Callable<Void>() { @Override public Void call() throws Exception { String userId = LoginInstance.getInstance().getUserUid() + ":" + LoginInstance.getInstance().getUserPhone(); if (!TextUtils.isEmpty(userId)) { CrashReport.setUserId(userId); } return null; } }); } }
|
注意
如果之前有集成过 bugly 的 crash 上报,这里需要统一采用 Bugly.init(getApplication(), "bugly注册分配的appid", true)
的方式初始化
enableProxyApplication=true
的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MyApplication extends Application {
@Override public void onCreate() { super.onCreate(); // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId // 调试时,将第三个参数改为true Bugly.init(this, "appId", false); }
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // you must install multiDex whatever tinker is installed! MultiDex.install(base); // 安装tinker Beta.installTinker(); } }
|
无须改造 Application,插件会自动反射来替换 Manifest 中的 Application 为真实的 Application
第四步 AndroidManifest.xml 配置
#####权限配置
1 2 3 4 5 6
| <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.READ_LOGS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
Activity 配置以及 FileProvider 配置用于应用升级,我们只使用热更新,因此这里可不用处理
如若需要,请参考 Android 热更新使用指南
第五步 混淆配置
混淆文件中配置
1 2 3 4 5 6
| -dontwarn com.tencent.bugly.** -keep public class com.tencent.bugly.**{*;} # tinker混淆规则 -dontwarn com.tencent.tinker.** -keep class com.tencent.tinker.** { *; }
-keep class android.support.**{*;}//如果使用了 support-v4 包
|
打补丁包
前面讲了如何接入,下面我们来讲解下怎么生成补丁文件,上传补丁以及应用补丁等。
单渠道打补丁包模式
单渠道情况下,在打包发布 apk 时需要设置 tinkerid,便于后续可能需要打补丁使用。
此 tinkerid 需要保证唯一,在生成基准包(即将发布的包)后,需要将对应的 apk、mapping 文件、R 文件保存。
1. 生成正式发布包(基准包)
如下图所示,这些都是在生成正式发布包时注意的。
2. 正式包出问题时,生成补丁文件
需要生成补丁文件时,需要修改 tinker-support.gradle
脚本文件。详细说明如图所示:
3. 上传补丁文件并下发用户
在 bugly 官网,找到对应的产品热更新模块,如图所示操作,用户重启 app 即可修复。
注意:这里选择文件上传时可能会报错,没有对应版本。这是因为需要目标 apk 进行联网,将 tinkerid 上报,后台才能识别到。当然正式包上线,这里肯定有联网会匹配到目标版本;如果是自己测试,需要注意这一点。
多渠道打补丁包模式
前面介绍了单渠道模式下的补丁包生成过程,大部分情况下我们还是有很多渠道包的,可以看看这篇文章 Bugly多渠道热更新解决方案。
多渠道打包官方原生支持是 productFlavors,这种方式每打一个渠道包需要全部编译一次,因此效率比较低。
bugly 官方这里推荐使用美团的多渠道打包组件 walle,只需要修改 apk signature block 来添加自定义的渠道信息即可生成渠道包,速度非常快。
考虑到我们项目多渠道包主要针对不同国家版本,每个国家一个渠道包,虽然整体代码是同一套,但是不同渠道线上版本不能保证一致,因此无法通过一个补丁完成所有渠道修复工作。这里我们项目采用的是 productFlavors 这种方式。
1. 多渠道生成正式发布包(基准包)
如图所示:
2. 生成补丁文件
3. 上传补丁文件并下发用户
这里和单渠道类似,只不过是针对每个渠道单独上传配置下发。
另外说明
1.tinkerid 是 tinker 识别每一个 apk 包的唯一标识,它被写入到 AndroidManifest
文件中。
补丁文件中的 YAPATCH.MF
包含目标版本和补丁版本的相关信息,用于补丁生效使用。
2.如果生成补丁包时报错
1 2
| Execution failed for task ':app:tinkerProcessReleaseResourceId'. > java.io.FileNotFoundException: build\intermediates\tinker_intermediates\values_backup
|
这个时候可以备份下基准文件,然后 clean 项目后再重新打补丁包即可,可以参考这个 issue
总结
bugly 提供了一系列相关接口方法,如是否通知用户重启、如何打加固包等,更多详情可参考:
- Bugly 热更新使用指南
- Bugly Android Demo
- Bugly 热更新常见问题
- Bugly 热更新常用 API