前言
之前研究Sophix的,之后为了对比又想着接入微信的Tinker,所以有了本篇文章。Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。本文就对Tinker的接入做简单的介绍,具体使用情况需参考公司具体业务。另一篇关于接入Sophix如下,作为对比使用。
Android——Sophix热修复接入
Tinker修复优缺点
1.1 优点
1.2 缺点
由于原理与系统限制,Tinker有以下已知问题:
- 修复部分冷启动方能生效
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
- 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
上面的介绍摘选自Tinker官方文档,如需要更加详细文档,访问:Tinker官方地址。在对Tinker有个简单了解后,下面我们就开始在项目中一步步集成Tinker了。
创建项目
进入Tinker热修复官网,登录账号之后点击上部的tab进入我的app栏目下,如下点击创建项目即可,创建项目的包名要与使用Tinker热修复项目一致。
项目接入Tinker
2.1 Project的build.gradle配置
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.9"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
2.2 module级别build.gradle配置
module下build.gradle文件添加版本依赖:
dependencies {
// 若使用annotation需要单独引用,对于tinker的其他库都无需再引用
// compileOnly("com.tinkerpatch.tinker:tinker-android-anno:1.9.9")
implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.9")
}
2.3权限配置
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
说明:
- 联网权限是用于拉取阿里服务器补丁
2.4 Patch配置
复制官网的tinkerpatch.gradle文件到当前app下面,有些参数需要修改(更多的设置需查看官网文档),并且当前app的build.gradle下添加:
apply from: 'tinkerpatch.gradle'
tinkerpatch.gradle文件具体如下:
apply plugin: 'tinkerpatch-support'
/**
* TODO: 请按自己的需求修改为适应自己工程的参数
*/
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.2-1225-14-34-18"
def variantName = "release"
/**
* 对于插件各参数的详细解析请参考
* http://tinkerpatch.com/Docs/SDK
*/
tinkerpatchSupport {
/** 可以在debug的时候关闭 tinkerPatch, isRelease() 可以判断BuildType是否为Release **/
tinkerEnable = true
reflectApplication = true
/**
* 是否开启加固模式,只能在APK将要进行加固时使用,否则会patch失败。
* 如果只在某个渠道使用了加固,可使用多flavors配置
**/
protectedApp = false
/**
* 实验功能
* 补丁是否支持新增 Activity (新增Activity的exported属性必须为false)
**/
supportComponent = true
autoBackupApkPath = "${bakPath}"
appKey = "c2305b76bdc4e9b3"
/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0.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"
backupFileNameFormat = '${appName}-${variantName}'
}
/**
* 用于用户在代码中判断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
}
}
说明:
- def bakPath 基包文件路径,在app目录下的build文件夹新创建一个bakApk文件夹下
- def baseInfo 基包文件名(打补丁包的时候,需要修改)后面带着的是生成的时间(为了确保唯一性)
- tinkerEnable 控制是release还是debug,处于debug状态的时候就是关闭补丁状态(默认为true)
- reflectApplication 是否使用反射接入无需修改application(默认是true)
- protectedApp tinker对于加固兼容性不好(默认为false)
- supportComponent 是否支持动态新增Activity,并且新增Activity的exported属性必须为false,否则不生效
- appKey 申请的appkey
- appVersion 每次打新的补丁一定要修改,否者服务器判断此值是否需要下发补丁,尽量与versionName保持一致(便于之后版本管理)
- 如果简单接入测试,只需配置上面appKey 和appVersion 即可
2.5 配置SDK参数
由于我们在tinkerpatch.gradle配置了reflectApplication =true,所以我们无需对application改造就可接入Tinker,如下:
public class SampleApplication extends Application {
private static final String TAG = "Tinker";
private ApplicationLike tinkerApplicationLike;
public SampleApplication() {
}
@Override
public void attachBaseContext(Context base) {
super.attachBaseContext(base);
//you must install multiDex whatever tinker is installed!
//MultiDex.install(base);
}
/**
* 由于在onCreate替换真正的Application,
* 我们建议在onCreate初始化TinkerPatch,而不是attachBaseContext
*/
@Override
public void onCreate() {
super.onCreate();
initTinkerPatch();
}
/**
* 我们需要确保至少对主进程跟patch进程初始化 TinkerPatch
*/
private void initTinkerPatch() {
// 我们可以从这里获得Tinker加载过程的信息
if (BuildConfig.TINKER_ENABLE) {
tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK
TinkerPatch.init(
tinkerApplicationLike
// new TinkerPatch.Builder(tinkerApplicationLike)
// .requestLoader(new OkHttp3Loader())
// .build()
)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setFetchPatchIntervalByHours(3)
;
// 获取当前的补丁版本
Log.e(TAG, "Current patch version is " + TinkerPatch.with().getPatchVersion());
// fetchPatchUpdateAndPollWithInterval 与 fetchPatchUpdate(false)
// 不同的是,会通过handler的方式去轮询
TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}
}
/**
* 在这里给出TinkerPatch的所有接口解释
* 更详细的解释请参考:http://tinkerpatch.com/Docs/api
*/
private void useSample() {
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("test", "1")
//设置补丁合成成功后,锁屏重启程序
//默认是等应用自然重启
.setPatchRestartOnSrceenOff(true)
//我们可以通过ResultCallBack设置对合成后的回调
//例如弹框什么
//注意,setPatchResultCallback 的回调是运行在 intentService 的线程中
.setPatchResultCallback(new ResultCallBack() {
@Override
public void onPatchResult(PatchResult patchResult) {
Log.i(TAG, "onPatchResult callback here");
}
})
//设置收到后台回退要求时,锁屏清除补丁
//默认是等主进程重启时自动清除
.setPatchRollbackOnScreenOff(true)
//我们可以通过RollbackCallBack设置对回退时的回调
.setPatchRollBackCallback(new RollbackCallBack() {
@Override
public void onPatchRollback() {
Log.i(TAG, "onPatchRollback callback here");
}
});
}
/**
* 自定义Tinker类的高级用法, 使用更灵活,但是需要对tinker有更进一步的了解
* 更详细的解释请参考:http://tinkerpatch.com/Docs/api
*/
private void complexSample() {
//修改tinker的构造函数,自定义类
TinkerPatch.Builder builder = new TinkerPatch.Builder(tinkerApplicationLike)
.listener(new DefaultPatchListener(this))
.loadReporter(new DefaultLoadReporter(this))
.patchReporter(new DefaultPatchReporter(this))
.resultServiceClass(TinkerServerResultService.class)
.upgradePatch(new UpgradePatch())
.patchRequestCallback(new TinkerPatchRequestCallback());
//.requestLoader(new OkHttpLoader());
TinkerPatch.init(builder.build());
}
}
测试Tinker补丁
3.1 生成基包
每次开发完成后,打开Studio右侧的Gradle,选择assemableRelease打正式包,这个过程是打基包,基包路径在之前的tinkerpatch.gradle配置的,生成的基包安装在手机上。
说明:
-
记得签名和打开混淆
3.2 生成补丁
这里修复的内容就是修改textview内容以及文字颜色,然后生成的基包名称赋值到tinkerpatch.gradle文件夹下的baseInfo,接着打开Gradle,选择tinkerPatchRelease进行打补丁
进过gradle之后生成的补丁包位于 build/outputs/tinkerPatch 下,这里只需要用到patch_signed_7zip.apk
3.3 测试补丁
3.3.1 添加版本
注意此处的版本号一定要与tinkerpatch.gradle文件的appversion一致
3.3.2 发布测试补丁
注意如果是正式项目,一定要做测试发布,防止出现未可知的问题,如下图,选择补丁文件和描述,然后记得选择开发预览之后再提交当前补丁
3.3.3 Tinker调试工具调试补丁
由于 Tinker 与代码相关,我们不能通过在代码设置是否为 debug 模式。这里我们提供了 debug 调试工具,它的 Github 地址为 tinkerpatch-debug-tool。 我们也可以通过点击此链接下载。
说明:
- 当测试手机已经安装基包和发布测试补丁时打开开关即可完成测试补丁
3.3.4 调试结果
- 如果测试通过,则可以再次通过编辑当前版本测试不同的发布测试
注意事项
- 最新Tinker支持新增Activity,需要tinkerpatch.gradle修改supportComponent = true,并且新增Activity的exported属性必须为false
- 修复补丁之后必须冷启动才能生效
- Tinker对于Android N版本存在兼容性问题
- 对于使用assemble方式生成基包,尽量选择关闭Instant run方式编译
- 具体更多接入细节请参考Tinker接入文档