0%

2018-12-09 APP 接入 Bugly 热更新

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 的情况

主要是做了两件事情

  • 自定义 Application
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

  • 自定义 ApplicationLike
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 提供了一系列相关接口方法,如是否通知用户重启、如何打加固包等,更多详情可参考:

  1. Bugly 热更新使用指南
  2. Bugly Android Demo
  3. Bugly 热更新常见问题
  4. Bugly 热更新常用 API