Android Tinker集成采坑
官方文档 https://github.com/Tencent/tinker/wiki
官方demo怎么配置都可以从demo中找到 https://github.com/Tencent/tinker/tree/dev/tinker-sample-android
Tinker提供了两种接入方式,命令行接入和gradle接入。正常的项目中都基本都使用gradle,一次配置好以后就可以很方便的使用了,所以本次只使用gradle方式。
本文基于1.9.13版本,因为有好几个地方都需要用到版本信息,所以将它放在gradle.properties文件中方便版本的管理
TINKER_VERSION=1.9.13
在总工程的的build.gradle配置tinker的classpath,因为tinker定义了一些自己的gradle脚本,后面在配置参数的时候会用到。
classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") {
changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
exclude group: 'com.android.tools.build', module: 'gradle'
}
然后在app的gradle文件中配置核心库和谷歌的分包库,现在的应用功能都很多所以体积很大一般都会用到multidex
//核心sdk库
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
//注解编译器,生成application的时候用
annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
implementation "com.android.support:multidex:1.0.3"
先配置app的gradle文件中android这个标签下的内容
//配置签名,这里使用demo中的签名文件,真实项目中替换成自己的
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")
}
}
// 支持大工程模式
dexOptions {
jumboMode = true
}
//release包开始混淆
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}
然后开始配置tinker的参数,官方指南上gradle参数详解官方指南上有参数的详解,建议都看一遍,更容易知道参数的作用和应该怎么配置。
def bakPath = file("${buildDir}/bakApk/")
ext {
//是否启用tinker
tinkerEnabled = true
//每次打包完都需要更改下面的三个路径,如果支持多渠道打包,下面第四个参数也需要修改
//old apk 的路径
tinkerOldApkPath = "${bakPath}/app-release-0508-10-52-50.apk"
//old apk 混淆 mapping 文件的路径
tinkerApplyMappingPath = "${bakPath}/app-release-0508-10-52-50-mapping.txt"
//old apk R文件的路径
tinkerApplyResourcePath = "${bakPath}/app-release-0508-10-52-50-R.txt"
//多渠道打包的路径
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
static def gitSha() {
// 每次打包的时候版本要一致,官方demo的是git的版本,这里使用versionName
String gitRev = "1.0"
return gitRev
}
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
//判断是否启用tinker
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* old apk 的路径
*/
oldApk = getOldApkPath()
/**
* 在产生patch的时候是否忽略tinker的警告,最好不忽略
* case 1: minSdkVersion小于14,但是dexMode的值为"raw"
* case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
* case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
* case 4: 定义在dex.loader用于加载补丁的类出现修改;
* case 5: resources.arsc改变,但没有使用applyResourceMapping编译
*/
ignoreWarning = false
/**
* 是否启用签名,一般强制使用
*/
useSign = true
/**
* 是否启用tinker
*/
tinkerEnable = buildWithTinker()
/**
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* 指定old apk 混淆时的打包文件
*/
applyMapping = getApplyMappingPath()
/**
* 指定old apk 的资源文件
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* 每个patch文件的唯一标识符
*/
tinkerId = getTinkerIdValue()
/**
* 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
*/
keepDexApply = false
/**
* 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
*/
isProtectedApp = false
/**
* 是否支持新增非export的Activity
*/
supportHotplugComponent = false
}
dex {
/**
* 只能是'raw'或者'jar'。
* 对于'raw'模式,我们将会保持输入dex的格式。
* 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,
* 而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
*/
dexMode = "jar"
/**
* 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
*这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。
* 这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
* 这里需要定义的类有:
* 1. 你自己定义的Application类;
* 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
* 3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
* 4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。
* 这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
* 5. 使用1.7.6版本之后的gradle版本,参数1、2会自动填写。若使用newApk或者命令行版本编译,1、2依然需要手动填写
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
// "com.hsm.tinkertest.BuildInfo"
]
}
//lib相关的配置项
lib {
/**
* 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
*/
pattern = ["lib/*/*.so"]
}
//res相关的配置项
res {
/**
* 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,
* 例如assets/...,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* 若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
* 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,
* 但是会增加合成时的复杂度。默认大小为100kb
*/
largeModSize = 100
}
//用于生成补丁包中的'package_meta.txt'文件
packageConfig {
/**
* configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,
* 你可以定义其他的信息, 在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
* 但是建议直接通过修改代码来实现,例如BuildConfig。
*/
configField("patchMessage", "tinker is sample to use")
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
/**
* 7zip路径配置项,执行前提是useSign为true
*/
sevenZip {
/**
* 将自动根据机器属性获得对应的7za运行文件
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
//是否配置了多渠道
boolean hasFlavors = flavors.size() > 0
def date = new Date().format("MMdd-HH-mm-ss")
/**
* old apk复制到指定目录
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
tasks.all {
if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {
copy {
def fileNamePrefix = "${project.name}-${variant.baseName}"
def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
from variant.outputs.first().outputFile
into destPath
rename { String fileName ->
fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
}
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename { String fileName ->
fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
//多渠道
project.afterEvaluate {
//sample use for build all flavor for one time
if (hasFlavors) {
task(tinkerPatchAllFlavorRelease) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
}
}
}
task(tinkerPatchAllFlavorDebug) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
}
}
}
}
}
}
task sortPublicTxt() {
doLast {
File originalFile = project.file("public.txt")
File sortedFile = project.file("public_sort.txt")
List<String> sortedLines = new ArrayList<>()
originalFile.eachLine {
sortedLines.add(it)
}
Collections.sort(sortedLines)
sortedFile.delete()
sortedLines.each {
sortedFile.append("${it}\n")
}
}
}
OK,参数配置完成,下面开始写代码。
先写一个TinkerManager类来管理Tinker的初始化
public class TinkerManager {
private static final String TAG = "Tinker.TinkerManager";
private static ApplicationLike applicationLike;
/**
* 保证只初始化一次
*/
private static boolean isInstalled = false;
public static void setTinkerApplicationLike(ApplicationLike appLike) {
applicationLike = appLike;
}
public static ApplicationLike getTinkerApplicationLike() {
return applicationLike;
}
public static void setUpgradeRetryEnable(boolean enable) {
UpgradePatchRetry.getInstance(applicationLike.getApplication()).setRetryEnable(enable);
}
public static void installTinker(ApplicationLike appLike) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore");
return;
}
//监听patch文件加载过程中的事件
LoadReporter loadReporter = new DefaultLoadReporter(appLike.getApplication());
//监听patch文件合成过程中的事件
PatchReporter patchReporter = new DefaultPatchReporter(appLike.getApplication());
//监听patch文件接收到之后可以做一些校验
PatchListener patchListener = new CustomPatchListener(appLike.getApplication());
//升级策略
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(appLike,
loadReporter, patchReporter, patchListener,
CustomResultService.class, upgradePatchProcessor);
isInstalled = true;
}
}
这里面有几个类需要注意
- LoadReporter类:监听patch文件加载过程中的事件,这里使用DefaultLoadReporter,如果有需要可以继承DefaultLoadReporter写自己的业务逻辑
- PatchReporter :监听patch文件合成过程中的事件,这里使用DefaultPatchReporter,如果哟需要可以继承DefaultPatchReporter写自己的业务逻辑
- PatchListener :监听patch文件接收到之后可以做一些校验,这个一般用的比较多,为了保证我们下载的patch包的没有被篡改,可以重写PatchListener,写一些自己的校验
- AbstractPatch :升级策略,一般不用修改
- CustomResultService:继承自系统的DefaultTinkerResultService,决定在patch安装完以后的后续操作,因为tinker修复完之后需要重启才能生效,tinker默认是加载完patch包之后直接杀死进程。这样可能会不太友好,如果不想直接杀进程可以继承DefaultTinkerResultService类,写我们自己的逻辑。
CustomPatchListener和CustomResultService的样例:
public class CustomPatchListener extends DefaultPatchListener {
private String currentMD5;
public void setCurrentMD5(String md5Value) {
this.currentMD5 = md5Value;
}
public CustomPatchListener(Context context) {
super(context);
}
/**
* 校验
* @return
*/
@Override
public int patchCheck(String path, String patchMd5) {
//做自己的校验
return super.patchCheck(path, patchMd5);
}
}
/**
* 决定在patch安装完以后的后续操作,默认实现是杀进程
*/
public class CustomResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.CustomResultService";
//返回patch文件的结果
@Override
public void onPatchResult(final PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "CustomResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "CustomResultService receive result: %s", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
if (result.isSuccess) {
Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();
}
}
});
// is success and newPatch, it is nice to delete the raw file, and restart at once
// for old patch, you can't delete the patch file
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
//默认是直接重启体验可能不好,这里只是在后台重启
if (checkIfNeedKill(result)) {
if (Utils.isBackground()) {
TinkerLog.i(TAG, "it is in background, just restart process");
restartProcess();
} else {
TinkerLog.i(TAG, "tinker wait screen to restart process");
new Utils.ScreenState(getApplicationContext(), new Utils.ScreenState.IOnScreenOff() {
@Override
public void onScreenOff() {
restartProcess();
}
});
}
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
/**
* you can restart your process through service or broadcast
*/
private void restartProcess() {
TinkerLog.i(TAG, "app is background now, i can kill quietly");
//you can send service or broadcast intent to restart your process
android.os.Process.killProcess(android.os.Process.myPid());
}
}
为了使真正的Application实现可以在补丁包中修改,tinker建议Appliction类的所有逻辑移动到ApplicationLike代理类中。
@DefaultLifeCycle(application = ".SampleTinkerApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class CustomTinkerLike extends DefaultApplicationLike {
CustomTinkerLike mCustomTinkerLike;
public CustomTinkerLike(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();
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//必须使用multiDex
MultiDex.install(base);
mCustomTinkerLike = this;
TinkerManager.setTinkerApplicationLike(this);
//在 installed 之前设置
TinkerManager.setUpgradeRetryEnable(true);
//installTinker after load multiDex
//or you can put com.tencent.tinker.** to main dex
TinkerManager.installTinker(this);
Tinker tinker = Tinker.with(getApplication());
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}
- 自定义一个CustomTinkerLike继承自DefaultApplicationLike,以前在我们自定义的Application中初始化的代码都移动到这里的onCreate()方法中。
- 添加注解DefaultLifeCycle,第一个是application的名字,编译的时候会自动给我们生成一个application类,然后把这个生成的application注册到AndroidManifest.xml中。
最后Activity中定义一个按钮点击加载patch包
public void load(View view) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
}
到这里配置和代码就都完成了,下面开始打包,先打基础包
线上的包基本都是release包,前面已经配置了签名,所以这就只打release包。
可以使用命令行输入命令./gradlew assemableRelease
,也可以使用studio的快捷操作,快捷操作图片如下
打完包之后,tinker会讲outputs/release文件夹下的打包好的文件复制一份到bakApk文件夹中一份,并重命名,这个bakApk文件夹是前面在gradle中配置的。还有混淆的mapping文件和R文件也复制一份重命名放到bakApk文件夹下面。
把打包好的apk装到手机上,然后修改一些代码,开始打补丁包
如图修改gradle中的oldApk的信息。然后调用tinker的命令打包如下图
打包完成之后在outputs文件夹下会多出来一个tinkerPatch文件夹。patch_signed_7zip.apk就死我们需要的patch包了。直接放到前面加载sdk文件的路径,或者从网络下载到该路径,之后调用加载的方法就完成修复了。