关于热修复的作用,不用多说了,一句话概括就是通过让用户无感的方式来修复线上应用的bug。这里介绍的是微信Tinker。
下面的接入方式都是参考自Tinker官方文档来。我这里主要是把我接入的步骤(通过AndroidStudio + gradle方式)说一遍。下面的步骤都是基于tinker-support插件:1.0.8版本,以及sdk 1.3.1 进行。所以查看此文时需要参考官方文档是否有更新,避免接入的方式改变导致接入出现问题。Tinker的官方接入指南地址为:https://bugly.qq.com/docs/user-guide/instruction-manual-android-hotfix/?v=20170912151050
工程根目录下“build.gradle”文件中添加:
buildscript {
repositories {
jcenter()
}
dependencies {
// tinkersupport插件, 其中lastest.release指拉取最新版本,也可以指定明确版本号,例如1.0.4
classpath "com.tencent.bugly:tinker-support:1.0.8"
}
}
gradle配置:
在app module的“build.gradle”文件中添加(示例配置):
android {
defaultConfig {
ndk {
//设置支持的SO库架构
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
}
dependencies {
compile "com.android.support:multidex:1.0.1" // 多dex配置
//注释掉原有bugly的仓库
//compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.3.2
compile 'com.tencent.bugly:crashreport_upgrade:1.3.1'
compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.2.0
}
后续更新升级SDK时,只需变更配置脚本中的版本号即可。
在app module的“build.gradle”文件中添加:
// 依赖插件脚本,平时编写代码的时候这里其实可以注释,不然每次build都会生成基准包,只要在打正式包和补丁包的时候放开注释就好了
apply from: 'tinker-support.gradle'
您需要在同级目录下创建tinker-support.gradle这个文件哦。
tinker-support.gradle内容如下所示(示例配置):
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
* 此处填写每次构建生成的基准包目录
*/
def baseApkDir = "app-0208-15-10-00"
/**
* 对于插件各参数的详细解析请参考
*/
tinkerSupport {
// 开启tinker-support插件,默认值true
enable = true
// 指定归档目录,默认值当前module的子目录tinker
autoBackupApkDir = "${bakPath}"
// 是否启用覆盖tinkerPatch配置功能,默认值false
// 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 编译补丁包时,必需指定基线版本的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"
// 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
tinkerId = "base-1.0.1"
// 构建多渠道补丁时使用
// buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持)
// isProtectedApp = true
// 是否开启反射Application模式
enableProxyApplication = false
}
/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* 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"
//applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可选,设置mapping文件,建议保持旧apk的proguard混淆方式
//applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配
}
}
注意,这里记得修改为基线版本包名称的修改。我这里的基线版本输出文件名为 :tinker_demo-release
所以修改为如下:
我这里使用的是enableProxyApplication = false 的情况 。因为这是Tinker推荐的接入方式,一定程度上会增加接入成本,但具有更好的兼容性。true的情况具体看官方文档。
集成Bugly升级SDK之后,我们需要按照以下方式自定义ApplicationLike来实现Application的代码(以下是示例):
自定义Application:
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL, "xxx.xxx.SampleApplicationLike",
"com.tencent.tinker.loader.TinkerLoader", false);
}
}
注意:这个类集成TinkerApplication类,这里面不做任何操作,所有Application的代码都会放到ApplicationLike继承类当中
参数解析
参数1:tinkerFlags 表示Tinker支持的类型 dex only、library only or all suuport,default: TINKER_ENABLE_ALL
参数2:delegateClassName Application代理类 这里填写你自定义的ApplicationLike
参数3:loaderClassName Tinker的加载器,使用默认即可
参数4:tinkerLoadVerifyFlag 加载dex或者lib是否验证md5,默认为false
然后记得在manifest文件中修改为自定义的Application:
自定义ApplicationLike,我们只需把下面的代码拷贝到我们的项目中就可以了,记得修改onCreate方法中的Bugly.init()中的key:
public class SampleApplicationLike extends DefaultApplicationLike {
public static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onCreate() {
super.onCreate();
// 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
// 调试时,将第三个参数改为true
Bugly.init(getApplication(), "0123456789", false);
}
@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);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
getApplication().registerActivityLifecycleCallbacks(callbacks);
}
}
注意:tinker需要你开启MultiDex,你需要在dependencies中进行配置compile “com.android.support:multidex:1.0.1”才可以使用MultiDex.install方法; SampleApplicationLike这个类是Application的代理类,以前所有在Application的实现必须要全部拷贝到这里,在onCreate方法调用SDK的初始化方法,在onBaseContextAttached中调用Beta.installTinker(this);。
在此,Tinker的接入已经完成,是不是比想象中简单多了简单。
打正式包:
首先打正式包前,记得修改tinker-support.gradle文件中的tinkerId:
tinkerId最好是一个唯一标识,例如git版本号、versionName等等。 如果你要测试热更新,你需要对基线版本进行联网上报。
这里强调一下,基线版本配置一个唯一的tinkerId,而这个基线版本能够应用补丁的前提是集成过热更新SDK,并启动上报过联网,这样我们后台会将这个tinkerId对应到一个目标版本,例如tinkerId = “bugly_1.0.0” 对应了一个目标版本是1.0.0,基于这个版本打的补丁包就能匹配到目标版本。
然后会在tinkerSupport.gradle上配置的路径输出基线包,我这里是:build文件下的baseApk下:
在此我们需要记得保存好基线包文件夹以及基线包的TinkerId(补丁包与根据基线包TinkerId进行匹配,所以TinkerId一般使用版本号来标识)。
好了,我们把这基线包发布到bugly平台上即可:
打补丁包:
那现在刚刚发布线上的应用有问题了,那我们首先在基线包的代码上进行代码的修复,然后进行打补丁包进行修复了。
首先,修改tinkersuppor.gradle里面的baseApkDir配置,就是上面打基线包生成的以时间戳命名的文件夹名称:
然后我们把上面的 patch_signed_7zip.apk文件上传到bugly分发即可:
连接手机调试查看log,如果打印以下log证明补丁包已经下载成功:
至此bug修复完成。
1)、有人在写Demo测试时,把基准包上传了,补丁包上传时出了未匹配到可应用的App版本的报错。
那是因为我们的基准包虽然已经上传了,但是还没有一部手机安装使用并上报联网。所以Bugly的后台服务器找不到对应要下发的App版本,所以导致了该错误。
解决方案是,用手机安装一次基准包,查看log有以下打印,证明联网上报成功,然后在重新发布一下补丁包就不会出现以上问题了。
2 )、怎样去测试我这个补丁包能否正常使用,而且先不下发给所有用户?
那这时就要使用开发设备进行测试了。
我们在发布新补丁时有个开发设备可以选择,我们可以通过Bugly.setIsDevelopmentDevice(getApplication(), true);方法设置为开发者设备呐?而且需要在打基准包时就需要设置好。
那这时会有人疑问要去修改线上的基准包?这不实际吧,而且也做不了啊。
那怎么办呢?
首先 我们在线上版本的代码(每次上线前要利用git去保留一个线上版本的分支)上添加设置为开发者设备的代码,然后重新进行打基准包的操作(记得: tinkerId 和线上版本的一致),把这个基准包安装到测试手机上(注意不是上传到bugly)。
然后我们再进行代码的修复,打补丁包。把补丁包上传到bugly,设置为开发设备。然后我们在重新退出再开启测试机上的应用,查看日志,打印如下表明补丁包已经分发合并到当前测试手机的应用上。而线上的用户是接收不了的。
最后我们已经确保这补丁包没问题了,我们要把这补丁包分发给所有用户。那我们就可以再bugly平台上重新编辑补丁包的下发设置为全部设备即可。
到此,我们的Tinker热更新接入和使用已经完成。
关于为什么要进行多渠道打包的问题,相信大家都清除,就是为了方便统计更清楚地和了解应用在各应用市场的分布情况,便于产品和运营做一些针对性的产品运营推广方案。
Android原生是提供了一套多渠道打包的方案给开发者的,然后也有很多其它第三方也提供了打渠道包的方案,比如热门的美团walle打包,360打包工具等。
那我们就针对这几种打包方案结合Tinker热修复的接入进行分析:
Android原生的多渠道打包是通过再gradle文件中添加productFloavors配置,实现大渠道打包的。
但是这种方案有很多缺点。首先就是它打包速度很慢,每打一个包都要重新编译一次。假如我打一个包要3分钟,那我打10个渠道包,就要30分钟。假如中间有一些问题又需要重新进行打包,那不就是一个坑爹的过程?
而另一方面,因为这种方式,会修改buildConfig类中的FLAVOR字段, 会使得我们的每个安装包的dex文件都是不一样的,那使用Tinker热修复打的补丁包就要针对具体的渠道进行打补丁。假如我有10个渠道包,那就要针对这10个渠道包打10个补丁包,那是一个更坑爹的过程,老板高薪请你来打包的,内心惭愧啊。那就算你耐得住打包这寂寞过程,buly也是只允许同时下发5个版本的补丁。所以这个方式是不实际的。
美团的walle打包方案是基于Android Signature V2 Schme 签名下的新一代渠道包打包神器,它通过在Apk中的APK SignNature Block 区块添加自定义的渠道信息来生成渠道包,从而提高了渠道包的生成效率。也就是说它只是编译打包一次,然后每个渠道复制一次再往里面添加渠道信息而已。所以这个过程就算打100个包,速度也是非常快的。
walle有两种集成方式,一种是通过gradle进行集成,一种是通过命令行工具使用方式(https://github.com/Meituan-Dianping/walle/blob/master/walle-cli/README.md)
这里介绍gradle集成方式:
配置build.gradle
在位于项目的根目录 build.gradle 文件中添加Walle Gradle插件的依赖, 如下:
buildscript {
dependencies {
classpath 'com.meituan.android.walle:plugin:1.1.6'
}
}
并在当前App的 build.gradle 文件中apply这个插件,并添加上用于读取渠道号的arr包
apply plugin: 'walle'
dependencies {
compile 'com.meituan.android.walle:library:1.1.6'
}
并在当前App的 build.gradle 文件上配置插件,默认就行不用改:
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")
}
然后就在代码中可以通过以下方式获取渠道信息并上传给对应的统计平台:
String channel = WalleChannelReader.getChannel(this.getApplicationContext());
打渠道包的过程也很简单:
就是控制台下的Terminal通过gradle的命令进行打包: gradlew clean assembleReleaseChannels
然后我们打补丁包的过程仍然是原来的过程进行。
我们来测试一下:
那我们来试试把这些渠道包进行加固,就用360加固重签名之后再安装试试:
发现这获取不到渠道信息了。
Walle也提供了解决方案,建议我们先加固在进行渠道打包(https://github.com/Meituan-Dianping/walle/wiki/360%E5%8A%A0%E5%9B%BA%E5%A4%B1%E6%95%88%EF%BC%9F):
但是这种方式感觉不够直接,有没有更好的方案呐?有的,往下看。
首先,我们需要下载最新的360加固助手,登录账户。
配置好之后,我们只需在加固应用界面,选择需要的应用进行加固,它就会自动进行加固,然后打渠道包,再进行重签名。然后输出到指定的文件夹,全程自动化操作,而且过程非常快。一个字:爽!
这个过程仅需在tinkerSupport.gradle中修改isProtectedApp = true字段,然后打好基准包然后用加固助手进行自动化操作即可。
可以看到通过360加固助手的多渠道打包,根据对应的统计平台插入了各个渠道的信息。
说了那么多,其实在最终的建议方案就是 先接入Tinker热更新,然后打基准包,利用.360加固助手进行加固打渠道包在重签名。上传其中一个渠道包到bugly平台上。当有bug时,在基准包基础上打补丁包,把补丁包上传到bugly平台即可。
TinkerDemo地址