为什么使用Tinker

当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。

这个Tinker官方给出的一个图表:

Android Tinker 基本使用教程_android-studio

1.怎么使用:

Tinker 的使用还是比较简单的,这是Tinker官方的github链接 : https://github.com/Tencent/tinker.

官方上面的文档很久没更新了,可以直接下载他最新的demo,按照demo上的来集成。

2. 导入依赖:

注意,这里有个坑,gradle版本不能用超过4.0.0版本的,不然构建会出错,tinker还不支持超过4.0.0版本,我这里用的3.5.3版本的

gradle依赖
allprojects {
dependencies {

classpath "com.android.tools.build:gradle:3.5.3"
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1"
}
}

dependencies {
//可选,如果不用注解构建application可不用
// annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.14.5") { changing = true }
// compileOnly("com.tencent.tinker:tinker-android-anno:1.9.14.5") { changing = true }

//tinker's main Android lib
implementation('com.tencent.tinker:tinker-android-lib:1.9.14.5')

//dex分包
implementation "androidx.multidex:multidex:2.0.1"
}

3. build.gradle配置:

在你的build.gradle加上以下配置 。tinker.gradle是我新建的一个gradle,目的是将tinker配置独立出来

${TINKER_ID}是我在 gradle.properties里面配置的一个常量TINKER_ID=100
每次发布新基准包都需要修改此版本号 注意:(发布补丁包不需要修改)

apply from: 'tinker.gradle'
android {

........

defaultConfig {
.......

javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
multiDexEnabled true
buildConfigField "String", "MESSAGE", "\"I am the base apk\""
buildConfigField "String", "TINKER_ID", "\"${TINKER_ID}\""
buildConfigField "String", "PLATFORM", "\"all\""
}

dexOptions {
jumboMode = true
}
}

tinker.gradle配置信息:可直接复制, 后面只需要修改ext {}里面的内容,其他的无需要可不用修改

def bakPath = file("${buildDir}/bakApk/")

ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//基准apk路径
tinkerOldApkPath = "${bakPath}/app-debug-0828-10-39-58.apk"
//未开启混淆,则不需要填写
tinkerApplyMappingPath = "${bakPath}/"
//基准apk中的R文件路径
tinkerApplyResourcePath = "${bakPath}/app-debug-0828-10-39-58-R.txt"
//如果你修复了res文件,需要指定你bug版本的R.txt文件
tinkerBuildFlavorDirectory = "${bakPath}/"

}

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 buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}



if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'

tinkerPatch {
/**
* 默认为null
* 将旧的apk和新的apk建立关联
* 从build / bakApk添加apk
*/
oldApk = getOldApkPath()
/**
* 可选,默认'false'
*有些情况下我们可能会收到一些警告
*如果ignoreWarning为true,我们只是断言补丁过程
* case 1:minSdkVersion低于14,但是你使用dexMode与raw。
* case 2:在AndroidManifest.xml中新添加Android组件,
* case 3:装载器类在dex.loader {}不保留在主要的dex,
* 它必须让tinker不工作。
* case 4:在dex.loader {}中的loader类改变,
* 加载器类是加载补丁dex。改变它们是没有用的。
* 它不会崩溃,但这些更改不会影响。你可以忽略它
* case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建
*/
ignoreWarning = true

/**
* 可选,默认为“true”
* 是否签名补丁文件
* 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功
* 我们将使用sign配置与您的构建类型
*/
useSign = true

/**
* 可选,默认为“true”
* 是否使用tinker构建
*/
tinkerEnable = buildWithTinker()

/**
* 警告,applyMapping会影响正常的android build!
*/
buildConfig {
/**
* 可选,默认为'null'
* 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的
* apk映射文件如果minifyEnabled是启用!
* 警告:你必须小心,它会影响正常的组装构建!
*/
applyMapping = getApplyMappingPath()
/**
* 可选,默认为'null'
* 最好保留R.txt文件中的资源id,以减少java更改
*/
applyResourceMapping = getApplyResourceMappingPath()

/**
* 必需,默认'null'
* 因为我们不想检查基地apk与md5在运行时(它是慢)
* tinkerId用于在试图应用补丁时标识唯一的基本apk。
* 我们可以使用git rev,svn rev或者简单的versionCode。
* 我们将在您的清单中自动生成tinkerId
*/
tinkerId = TINKER_ID.toInteger()

/**
* 如果keepDexApply为true,则表示dex指向旧apk的类。
* 打开这可以减少dex diff文件大小。
*/
keepDexApply = false

/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
*/
isProtectedApp = false

/**
* optional, default 'false'
* Whether tinker should support component hotplug (add new component dynamically).
* If this attribute is true, the component added in new apk will be available after
* patch is successfully loaded. Otherwise an error would be announced when generating patch
* on compile-time.
*
* <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
*/
supportHotplugComponent = false
}

dex {
/**
* 可选,默认'jar'
* 只能是'raw'或'jar'。对于原始,我们将保持其原始格式
* 对于jar,我们将使用zip格式重新包装dexes。
* 如果你想支持下面14,你必须使用jar
* 或者你想保存rom或检查更快,你也可以使用原始模式
*/
dexMode = "jar"

/**
* 必需,默认'[]'
* apk中的dexes应该处理tinkerPatch
* 它支持*或?模式。
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* 必需,默认'[]'
* 警告,这是非常非常重要的,加载类不能随补丁改变。
* 因此,它们将从补丁程序中删除。
* 你必须把下面的类放到主要的dex。
* 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication}
* 自己的tinkerLoader,和你使用的类
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}

lib {
/**
* 可选,默认'[]'
* apk中的图书馆应该处理tinkerPatch
* 它支持*或?模式。
* 对于资源库,我们只是在补丁目录中恢复它们
* 你可以得到他们在TinkerLoadResult与Tinker
*/
pattern = ["lib/*/*.so"]
}

res {
/**
* 可选,默认'[]'
* apk中的什么资源应该处理tinkerPatch
* 它支持*或?模式。
* 你必须包括你在这里的所有资源,
* 否则,他们不会重新包装在新的apk资源。
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

/**
* 可选,默认'[]'
* 资源文件排除模式,忽略添加,删除或修改资源更改
* *它支持*或?模式。
* *警告,我们只能使用文件没有relative与resources.arsc
*/
ignoreChange = ["assets/sample_meta.txt"]

/**
* 默认100kb
* *对于修改资源,如果它大于'largeModSize'
* *我们想使用bsdiff算法来减少补丁文件的大小
*/
largeModSize = 100
}

packageConfig {
/**
*可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
* 包元文件gen。路径是修补程序文件中的assets / package_meta.txt
* 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
* 或TinkerLoadResult.getPackageConfigByName
* 我们将从旧的apk清单为您自动获取TINKER_ID,
* 其他配置文件(如下面的patchMessage)不是必需的
*/
configField("patchMessage", "tinker is sample to use")
/**
*只是一个例子,你可以使用如sdkVersion,品牌,渠道...
* 你可以在SamplePatchListener中解析它。
* 然后你可以使用补丁条件!
*/
configField("platform", "all")
/**
* 补丁版本通过packageConfig
*/
configField("patchVersion", "1.0.2")
}
//或者您可以添加外部的配置文件,或从旧apk获取元值
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")

/**
* 如果你不使用zipArtifact或者path,我们只是使用7za来试试
*/
sevenZip {
/**
* 可选,默认'7za'
* 7zip工件路径,它将使用正确的7za与您的平台
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* 可选,默认'7za'
* 你可以自己指定7za路径,它将覆盖zipArtifact值
*/
// 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")

/**
* bak apk and mapping
*/
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

if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
def packageAndroidArtifact = variant.packageApplicationProvider.get()
if (packageAndroidArtifact != null) {
try {
from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
} catch (Exception e) {
from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
}
} else {
from variant.outputs.first().mainOutputFile.outputFile
}
} else {
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"
from "${buildDir}/intermediates/symbol_list/${variant.dirName}/R.txt"
from "${buildDir}/intermediates/runtime_symbol_list/${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"
}

}
}
}
}
}

4. 构建Application:

Application有两种构建方式

1. 使用注解生成的方式:

新建一个SampleApplicationLike 类,让他继承DefaultApplicationLike类,然后加上注解DefaultLifeCycle, SampleApplication则是生成的application, 你自己原来application里面初始化的东西则放到SampleApplicationLike的onCreate初始化

@DefaultLifeCycle(application = "com.example.tinkertest.SampleApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
private 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初始化,


}
}

然后在manifest里面配置application,name指向SampleApplication

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
android:name=".SampleApplication"
android:theme="@style/AppTheme">

.......

</application>

2. 不使用注解的方式:

SampleApplicationLike 和上面的一样,只是这里去掉注解

//@DefaultLifeCycle(application = "com.example.tinkertest.SampleApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
...
}

然后新建一个 SampleApplication(名字随意) 继承TinkerApplication ,ShareConstants.TINKER_ENABLE_ALL 代表加载所有, 第二个参数则是你创建的SampleApplicationLike类。

然后manifest里面指向SampleApplication,注意不要在SampleApplication里面做任何操作,所有第三方框架的初始化都放在SampleApplicationLike的oncreate里面

public class SampleApplication extends TinkerApplication {

public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL, SampleApplicationLike.class.getName());
}
}

文件访问权限,和高版本provider配置相关自行谷歌百度,或者直接Clone我github项目配置

5. Tinker初始化:

Application构建完成之后则需要对Tinker进行初始化

在之前的SampleApplicationLike 类里面重写onBaseContextAttached方法

public class SampleApplicationLike extends DefaultApplicationLike {
private 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初始化,


}



/**
* install multiDex before install tinker
* so we don't need to put the tinker lib classes in the main dex
*
* @param base
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);

TinkerManager.setTinkerApplicationLike(this);
TinkerManager.setUpgradeRetryEnable(true);
TinkerManager.installTinker(this);
Tinker.with(getApplication());
}
}

TinkerManager类相关,具体可看参考文章末尾给出的github链接源码

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);
}


/**
* all use default class, simply Tinker install method
*/
public static void sampleInstallTinker(ApplicationLike appLike) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore");
return;
}
TinkerInstaller.install(appLike);
isInstalled = true;
}


/**
* you can specify all class you want.
* sometimes, you can only install tinker in some process you want!
*
* @param appLike
*/
public static void installTinker(ApplicationLike appLike) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore");
return;
}
//or you can just use DefaultLoadReporter
LoadReporter loadReporter = new SampleLoadReporter(appLike.getApplication());
//or you can just use DefaultPatchReporter
PatchReporter patchReporter = new SamplePatchReporter(appLike.getApplication());
//or you can just use DefaultPatchListener
PatchListener patchListener = new SamplePatchListener(appLike.getApplication());
//you can set your own upgrade patch if you need
AbstractPatch upgradePatchProcessor = new UpgradePatch();

TinkerInstaller.install(appLike,
loadReporter, patchReporter, patchListener,
SampleResultService.class, upgradePatchProcessor);

isInstalled = true;
}
}

SampleResultService会接收到补丁加载的状态,成功还是失败,

public class SampleResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.SampleResultService";


@Override
public void onPatchResult(final PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "SampleResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "SampleResultService 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(), "补丁加载成功,请重启应用", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getApplicationContext(), "补丁加载失败", Toast.LENGTH_LONG).show();
}
}
});
}

}

在application配置此Service

<application
...>
<service
android:name=".SampleResultService"
android:exported="false"/>
</application>

6. 打基准包(也就是正常出来的安装包):

点击assemble

Android Tinker 基本使用教程_jar_02


然后在你的app/build/bakApk下面,这里我直接使用的deubg包

Android Tinker 基本使用教程_android studio_03


打完包之后,安装到手机之后的界面:

Android Tinker 基本使用教程_android_04

7. 打补丁包(基于基准包打补丁包):

修改下之前的界面,让他多出来一个查看补丁信息的按钮

Android Tinker 基本使用教程_android_05


然后将之前打出来的基准包app-debug-0828-14-47-04.apk和app-debug-0828-14-47-04-R.txt填入之前的tinker.gradleext里面的tinkerOldApkPath 和tinkerApplyResourcePath

如下:

ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//基准apk路径
tinkerOldApkPath = "${bakPath}/app-debug-0828-14-47-04.apk"
//未开启混淆,则不需要填写
tinkerApplyMappingPath = "${bakPath}/"
//基准apk中的R文件路径
tinkerApplyResourcePath = "${bakPath}/app-debug-0828-14-47-04-R.txt"
//如果你修复了res文件,需要指定你bug版本的R.txt文件
tinkerBuildFlavorDirectory = "${bakPath}/"

}

修改完成之后开始打补丁包:
我这里用的debug版本,则点击tinkerPathDebug

Android Tinker 基本使用教程_android studio_06


生成的补丁包在app\build\outputs\apk\tinkerPatch\debug下面

patch_signed_7zip.apk则就是你的补丁包

然后将他push 到手机根目录下面

Android Tinker 基本使用教程_android studio_07


打开app,点击加载补丁包:

Android Tinker 基本使用教程_jar_08


然后重启应用,多了个查看补丁信息按钮,点击之后出现当前的补丁信息

Android Tinker 基本使用教程_android_09

另附三个按钮的事件方法:

/**
* 加载热补丁插件
*/
private void loadPatch() {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
}

/**
* 查看补丁信息
*/
private void showInfo() {
// add more Build Info
final StringBuilder sb = new StringBuilder();
Tinker tinker = Tinker.with(getApplicationContext());
if (tinker.isTinkerLoaded()) {
sb.append(String.format("[补丁已加载] \n"));
sb.append(String.format("[基准包版本号] %s \n", BuildConfig.TINKER_ID));

sb.append(String.format("[补丁号] %s \n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName(ShareConstants.TINKER_ID)));
sb.append(String.format("[补丁版本] %s \n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName("patchVersion")));
sb.append(String.format("[补丁占用空间] %s k \n", tinker.getTinkerRomSpace()));

} else {
sb.append(String.format("[补丁未加载] \n"));
sb.append(String.format("[基准包版本号] %s \n", BuildConfig.TINKER_ID));

sb.append(String.format("[TINKER_ID] %s \n", ShareTinkerInternals.getManifestTinkerID(getApplicationContext())));
}

textView.setText(sb);
}

/**
* 清除补包
*/
private void cleanPatch(){
Tinker.with(getApplicationContext()).cleanPatch();
}

另附文章的github demo 链接

​https://github.com/cmcy/TinkerTest​