1.简述
Bugly采用Tinker作为热修复的解决方案,Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码,So库以及资源,让应用能够不需要重新安装的情况下实现更新。Tinker不能让补丁实时生效,它必须在打上补丁后重启App。在开始集成之前,我们有必要了解清楚,Tinker有哪些不足:
由于原理与系统限制,Tinker有以下已知问题:
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
- 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
2.添加插件依赖
项目的build.gradle:
buildscript {
ext.kotlin_version = '1.3.30'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1' //3.6.3不支持
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// tinkersupport插件, 其中lastest.release指拉取最新版本,也可以指定明确版本号,例如1.0.4
// 注意:自tinkersupport 1.0.3版本起无需再配tinker插件的classpath。
classpath "com.tencent.bugly:tinker-support:1.1.5"
// 多渠道插件(多渠道打包推荐使用)
classpath 'com.meituan.android.walle:plugin:1.1.6'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3.集成SDK
app的build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.geespace.hotfix"
minSdkVersion 23
targetSdkVersion 26
versionCode 1
versionName "1.1"
// 开启multidex
multiDexEnabled true
ndk {
//设置支持的SO库架构
abiFilters 'armeabi-v7a' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
dexOptions {
// 支持大工程模式
jumboMode = true
}
// 签名配置
signingConfigs {
release {
try {
storeFile file("./keystore/release.keystore")
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
storeFile file("./keystore/debug.keystore")
}
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
// 构建类型
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
multiDexKeepProguard file('multidex-config.pro')
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}
// 多渠道配置
// flavorDimensions "default"
//
// productFlavors {
// xiaomi {
//
// }
// yyb {
//
// }
// wdj {
//
// }
// }
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
//注释掉原有bugly的仓库
//compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本号,也可以指定明确的版本号,例如1.3.4
implementation 'com.tencent.bugly:crashreport_upgrade:latest.release'
// 指定tinker依赖版本(注:应用升级1.3.5版本起,不再内置tinker)
implementation 'com.tencent.tinker:tinker-android-lib:latest.release'
//implementation 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.2.0
// walle 瓦力(多渠道使用)
implementation 'com.meituan.android.walle:library:1.1.6'
//注意: 升级SDK已经集成crash上报功能,已经集成Bugly的用户需要注释掉原来Bugly的jcenter库;
// 已经配置过符号表的Bugly用户保留原有符号表配置; Bugly SDK(2.1.5及以上版本)
// 已经将Java Crash和Native Crash捕获功能分开,如果想使用NDK库,需要配置:
// compile 'com.tencent.bugly:nativecrashreport:latest.release'
}
// 依赖插件脚本
apply from: 'tinker-support.gradle'
// 多渠道使用walle示例(注:多渠道使用)
apply from: 'multiple-channel.gradle'
4.配置Tinker
在app的build.gradle文件同级目录下创建一个tinker-support.gradle文件.内容如下:
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
* 此处填写每次构建生成的基准包目录
*/
def baseApkDir = "app-0611-15-14-38"
//def myTinkerId="1.1-base"
def myTinkerId="1.1-patch"
/**
* 对于插件各参数的详细解析请参考 如果没有特殊需求下面的参数都可以不用更改;
* 如果apk需要加固等可以参考具体描述设置参数
*/
tinkerSupport {
// 开启tinker-support插件,默认值true
enable = true
// 指定归档目录,默认值当前module的子目录tinker
autoBackupApkDir = "${bakPath}"
//建议设置true,用户就不用再自己管理tinkerId的命名,插件会为每一次构建的base包自动生成唯一的tinkerId,
// 默认命名规则是versionname.versioncode_时间戳
//具体参考https://github.com/BuglyDevTeam/Bugly-Android-Demo/wiki/Tinker-ID%E8%AF%A5%E6%80%8E%E4%B9%88%E8%AE%BE%E7%BD%AE
// autoGenerateTinkerId = true
// 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
// 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。
// 这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等
//tinkerId = "1.1-base"
tinkerId = "${myTinkerId}"
// 是否启用覆盖tinkerPatch配置功能,默认值false
// 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
overrideTinkerPatchConfiguration = false
// 编译补丁包时,必需指定基线版本的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"
// 构建多渠道补丁时使用
buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 是否启用加固模式,默认为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}/${baseApkDir}/app-release.apk"
ignoreWarning = false
useSign = true
dex {
dexMode = "jar"
pattern = ["classes*.dex"]
loader = []
}
lib {
pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.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 = "${myTinkerId}"
//applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可选,设置mapping文件,建议保持旧apk的proguard混淆方式
applyResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配
}
}
1.overrideTinkerPatchConfiguration
当overrideTinkerPatchConfiguration=true时,tinkerPatch可以省略不写,Bugly会加载默认的Tinker配置,但请注意,如果你的so文件不是存放在libs目录下(与src目录同级),又或者资源文件的存放在你自定义的目录中,那么这时候你要小心了,这些文件在制作补丁时不会被检测,也就是说这些so文件和资源文件将不会被热修复,这种情况下就需要将
overrideTinkerPatchConfiguration=false,并设置tinkerPatch的lib和res属性。
2.baseApkDir
baseApkDir时基准包(也称基线包)的目录,在生成补丁时需要根据基准包在bakApk下具体文件夹名字修改。
3.tinkerId
tinkerId时Bugly热修复方案最重要的一个因素,一般来说我们可以使用git版本号、versionName等等,它会将补丁包与基准包产生对应关系,假设基准包的tinkerId为1.1-base,则生成的补丁包中的YAPATCH.MF文件关系如下:
对于同一个基准包,可以发布多个补丁包。
Q:如果我打了多个补丁包,比如先上传了补丁A已经下发到了设备中并且修复,如果我又打了一个补丁,这种情况会怎样?
A:如果你的基于基线版本打了多个补丁包,并且上传了多个,我们会以你最后上传的补丁为准,就是说后面的补丁会覆盖前面的补丁
5.初始化SDK
Bugly的初始化工作需要在Applicaiton中完成,但是对原生Tinker来说,默认的Applicaiton是无法完成热修复的,Tinker针对Applicaiton无法热修复的问题,给予了两个选择:
- enableProxyApplication = false的情况
这是推荐的接入方式,一定程度上会增加接入成本,但是具有更好的兼容性.
package com.geespace.hotfix.application;
import com.tencent.tinker.loader.app.TinkerApplication;
import com.tencent.tinker.loader.shareutil.ShareConstants;
/**
* Created by maozonghong
* on 2020/6/10
*/
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL,
"com.geespace.hotfix.application.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
}
package com.geespace.hotfix.application;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.multidex.MultiDex;
import com.meituan.android.walle.WalleChannelReader;
import com.tencent.bugly.Bugly;
import com.tencent.bugly.beta.Beta;
import com.tencent.bugly.beta.interfaces.BetaPatchListener;
import com.tencent.tinker.entry.DefaultApplicationLike;
import java.util.Locale;
/**
* Created by maozonghong
* on 2020/6/10
*/
//注意:tinker需要你开启MultiDex,你需要在dependencies中进行配置
// compile "com.android.support:multidex:1.0.1"才可以使用MultiDex.install方法;
// SampleApplicationLike这个类是Application的代理类,以前所有在Application的实现必须要全部拷贝到这里,
// 在onCreate方法调用SDK的初始化方法,在onBaseContextAttached中调用Beta.installTinker(this);
public class SampleApplicationLike extends DefaultApplicationLike{
public static final String TAG = "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();
Beta.autoCheckUpgrade=true;
// 设置是否开启热更新能力,默认为true
Beta.enableHotfix = true;
// 设置是否自动下载补丁,默认为true
Beta.canAutoDownloadPatch = true;
// 设置是否自动合成补丁,默认为true
Beta.canAutoPatch = true;
// 设置是否提示用户重启,默认为false
Beta.canNotifyUserRestart = true;
// 补丁回调接口
Beta.betaPatchListener = new BetaPatchListener() {
@Override
public void onPatchReceived(String patchFile) {
Log.e(TAG,"onPatchReceived:"+patchFile);
Toast.makeText(getApplication(), "补丁下载地址" + patchFile, Toast.LENGTH_SHORT).show();
}
@Override
public void onDownloadReceived(long savedLength, long totalLength) {
Log.e(TAG,"onDownloadReceived: savedLength"+savedLength+",totalLength:"+totalLength);
Toast.makeText(getApplication(),
String.format(Locale.getDefault(), "%s %d%%",
Beta.strNotificationDownloading,
(int) (totalLength == 0 ? 0 : savedLength * 100 / totalLength)),
Toast.LENGTH_SHORT).show();
}
@Override
public void onDownloadSuccess(String msg) {
Log.e(TAG,"onDownloadSuccess:"+msg);
Toast.makeText(getApplication(), "补丁下载成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onDownloadFailure(String msg) {
Log.e(TAG,"onDownloadFailure:"+msg);
Toast.makeText(getApplication(), "补丁下载失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onApplySuccess(String msg) {
Log.e(TAG,"onApplySuccess:"+msg);
Toast.makeText(getApplication(), "补丁应用成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onApplyFailure(String msg) {
Log.e(TAG,"onApplyFailure:"+msg);
Toast.makeText(getApplication(), "补丁应用失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onPatchRollback() {
}
};
// 设置开发设备,默认为false,上传补丁如果下发范围指定为“开发设备”,需要调用此接口来标识开发设备
Bugly.setIsDevelopmentDevice(getApplication(), true);
// 多渠道需求塞入
String channel = WalleChannelReader.getChannel(getApplication());
Bugly.setAppChannel(getApplication(), channel);
// 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
// 调试时,将第三个参数改为true
Bugly.init(getApplication(), "23a0cc94f2", true);
}
@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);
}
@Override
public void onTerminate() {
super.onTerminate();
Beta.unInit();
}
}
- enableProxyApplication = true 的情况
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
// 调试时,将第三个参数改为true
Bugly.init(this, "900029763", 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,主要是为了降低接入成本,我们插件会动态替换AndroidMinifest文件中的Application为我们定义好用于反射真实Application的类(需要您接入SDK 1.2.2版本 和 插件版本 1.0.3以上)。
6.AndroidManifest.xml配置
1.权限配置
2.Activity配置
3.FileProvider配置
7.制作基准包
在app编码完成并测试通过后,就是打包上线了,上线前打的包就是基准包。
- 打开app下的tinker.support.gradle文件
- 配置基准包的tinkerId
-
双击运行build下的assembleRelease
AS在执行assembleRelease指令时,就是在编译基准包了,当编译完成时,app的build目录下会自动生成基准包文件夹,以时间戳来命名的(也就是说,每次执行assembleRelease指令都会在build目录下创建不同的基准包文件夹)。见下图:
这两个文件对之后制作补丁包来说是非常重要的,你需要做的就是将这两个文件保存好,可以保存到云盘,硬盘,git服务器上,但是不能就让它这样放着,因为当你执行clean project时,app的build目录会被删除,这样基准包与R文件都会丢失。
8.制作补丁包
- 修改baseApkDir的值为基准包所有文件夹的名字。
- 修改补丁包的 tinkerId
-
打开侧边的Gradle标签,找到项目的主Module,双击tinker-support下的buildTinkerPatchRelease指令,生成补丁包。
当编译完成后,在app的build/outputs/patch目录下会在"patch_singed_7zip.apk"文件,它就是补丁包,双击打开它,可以看到其中有一个YAPATCH.MF,里面记录了基准包与补丁包的tinkerId,Created-Time,VersionName等.
制作好的补丁,可以上传到bugly后台,进行下发。
Q. 我该上传哪个补丁?patch目录跟tinkerPatch目录下的补丁有什么区别吗?
A:你必须要上传build/outputs/patch目录下的补丁包,
9.多渠道打包
这里推荐使用walle来打多渠道包,新一代多渠道打包神器。
配置示例:
// 多渠道使用walle示例(注:多渠道使用)
apply from: 'multiple-channel.gradle'
创建multiple-channel.gradle,内容如下:
apply plugin: 'walle'
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")
}
创建channel配置:
命令行打多渠道包:
执行 assembleReleaseChannels Task
ok,到此已经实现快速打多渠道包了。
如何获取渠道信息?
dependencies {
........
// walle 瓦力(多渠道使用)
implementation 'com.meituan.android.walle:library:1.1.6'
...............
}
在代码中获取渠道信息:
String channel = WalleChannelReader.getChannel(this.getApplicationContext());
Bugly.setAppChannel(getApplication(), channel);
集成当中遇到的问题:
1.Could not find method getAaptOptions() for arguments [] on task
的问题
应该是gralde太新了,目前不支持gralde 5.0.我使用gradle版本5.6.4,修改了资源文件,进行补丁文件打包的时候 一直报这个错。
2.java.io.FileNotFoundException: E:...\build\intermediates\tinker_intermediates\values_backup
解决方法:
1.基准文件备份下
2.clean项目
3.重新打补丁包
3.org.gradle.internal.event.ListenerNotificationException: Failed to notify project evaluation listener.
我一开始使用gradle插件版本3.6.3,apply from: 'tinker-support.gradle'的时候 一直报这个错,后面的得知时gradle插件版本太高了,改成3.6.3以下的插件版本就ok了。
4.Sample Application: com.tencent.tinker.loader.a: Tinker Exception:createDelegate failed
我使用的是enableProxyApplication = false模式,运行一直报创建代理Applicaiton失败.我是打release包,因为开启了 minifyEnabled true,
可能是混淆了,把minfiyEnable设置为false 就好了。
5.Caused by: com.tencent.tinker.android.dex.DexException: Unexpected magic: [100, 101, 120, 10, 48, 51, 55, 0]
打补丁包的过程中一直出行这个错误,github上看到有人说是tinker不支持java1.8,这特么就尴尬了,我项目中用到lambda,难道只能放弃?
后面偶然网上说可能出现的问题
- 基线版本问题
基线版本最好采用gradle assemble、gradle assembleDebug、gradle assembleRelease等生成
直接点击运行图标生成的apk貌似有问题。 - 确认Patch包与基线包之间是否更改过下列相关信息,如不一致则报错。'
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0.0"
- minSdkVersion >=26 会出现这个问题
以上就是本人集成过程中碰到的一些问题,大部分都是跟gradle和插件版本有关,所以,升级到最新的gradle版本会碰到很多问题。
最后贴下Demo链接:
https://github.com/maozonghong/BuglyTinkerDemo