###Tinker简介与核心原理
之前的文章中,我们学会了使用AndFix进行线上BUG的热修复。但是有一些BUG可能是因为资源文件、配置文件等非方法引起的BUG的时候,AndFix就无能为力了。因此这里有必要介绍Tinker。
关于什么是TInker,Tinker的官方文档里面有一句话:Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
关于Tinker的优势,除了有微信的大量用户认可之外(兼容性好),在下面的一张图里面也可以看到Tinker的功能强大:
有关更多的介绍,请参考官方文档,这里不再赘述:
下面简单介绍一下Tinker的核心原理:
- 基于Android原生的类加载器,研发了自己的类加载器,用来加载Patch文件中的字节码文件。并且通过AssetManager来加载Patch文件中的资源。
- 基于Android原生的AAPT,研发了自己的AAPT
- 基于于Android的Dex文件格式,研发了自己的一套DexDiff算法
###Tinker基本接入
鉴于Tinker官方文档的晦涩难懂,我们在这里做一个简单的介绍。
首先在app的gradle中引入tinker的核心库:
//可选,用于生成application类
//provided "com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}"
//tinker的核心库
compile "com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"
复制代码
其中需要注意的是TINKER_VERSION是在gradle.properties中配置的参数:
TINKER_VERSION=1.9.0
复制代码
然后,我们封装一个管理类:
public class TinkerManager {
private static TinkerManager sInstance;
private ApplicationLike mApplicationLike;
private boolean mIsInstall = false;
private static CustomPatchListener sCustomPatchListener;
private TinkerManager() {
}
public static TinkerManager getInstance() {
if (sInstance == null) {
synchronized (TinkerManager.class) {
if (sInstance == null) {
sInstance = new TinkerManager();
}
}
}
return sInstance;
}
public void install(ApplicationLike applicationLike) {
if (!mIsInstall) {
mApplicationLike = applicationLike;
sCustomPatchListener = new CustomPatchListener(getApplication());
TinkerInstaller.install(mApplicationLike);
mIsInstall = true;
}
}
public void addPatch(String path, String md5) {
if (Tinker.isTinkerInstalled()) {
sCustomPatchListener.setCurrentMD5(md5);
TinkerInstaller.onReceiveUpgradePatch(getApplication(), path);
}
}
private Context getApplication() {
if (mApplicationLike != null) {
return mApplicationLike.getApplication().getApplicationContext();
}
return null;
}
}
复制代码
其中:
- install方法中主要通过调用TinkerInstaller的方法进行初始化。有关ApplicationLike的知识在下面进行介绍。
- install方法中还初始化了一个CustomPatchListener对象,这个对象主要跟Patch文件的加载有关。
- addPatch方法主要是用于加载补丁文件,主要是调用了TinkerInstaller的onReceiveUpgradePatch方法进行补丁加载。
然后我们创建一个自定义的TinkerApplicationLike类,这个类主要是关联了应用的Application的生命周期,降低了代码的入侵性。然后在onBaseContextAttached回调中对TinkerManager进行初始化:
public class TinkerApplicationLike extends ApplicationLike {
public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
TinkerManager.getInstance().install(this);
}
}
复制代码
最后,我们创建自己的Application(也可以通过注解的方式生成,这里不做介绍),通过继承TinkerApplication并通过super方法关联上面的TinkerApplicationLike:
public class App extends TinkerApplication {
public App() {
super(ShareConstants.TINKER_ENABLE_ALL,
"com.nan.tinkerdemo.tinker.TinkerApplicationLike",
"com.tencent.tinker.loader.TinkerLoader",
false);
}
@Override
public void onCreate() {
super.onCreate();
//自己的逻辑
}
}
复制代码
通过委托ApplicationLike监听Application的声明周期(代理),使得Tinker的接入更加简单,降低耦合。
最后,记得在清单文件中配置Application以及TINKER_ID:
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.nan.tinkerdemo.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".tinker.TinkerService" />
<service android:name=".tinker.CustomResultService" />
<!--一定要记得设置tinker id-->
<meta-data
android:name="TINKER_ID"
android:value="tinker_id_1234567" />
</application>
复制代码
这个TINKER_ID是十分有用的,当Tinker需要进行Patch加载的时候,如果TINKER_ID不一致,就不会进行加载。
###命令行方式生成Patch
下面先从简单的入手,先到Tinker的官方Github下载patch生成工具。
<?xml version="1.0" encoding="UTF-8"?>
<tinkerPatch>
<issue id="property">
<ignoreWarning value="false"/>
<useSign value="true"/>
<sevenZipPath value="/usr/local/bin/7za"/>
</issue>
<issue id="dex">
<dexMode value="jar"/>
<pattern value="classes*.dex"/>
<pattern value="assets/secondary-dex-?.jar"/>
<loader value="com.tencent.tinker.loader.*"/>
<loader value="com.nan.tinkerdemo.App"/>
</issue>
<issue id="lib">
<pattern value="lib/armeabi/*.so"/>
</issue>
<issue id="resource">
<pattern value="res/*"/>
<pattern value="assets/*"/>
<pattern value="resources.arsc"/>
<pattern value="AndroidManifest.xml"/>
<ignoreChange value="assets/sample_meta.txt"/>
<largeModSize value="100"/>
</issue>
<issue id="packageConfig">
<configField name="platform" value="all"/>
<configField name="patchMessage" value="classes.dex"/>
</issue>
<issue id="sign">
<path value="release.keystore"/>
<storepass value="testres"/>
<keypass value="testres"/>
<alias value="testres"/>
</issue>
</tinkerPatch>
复制代码
主要需要修改的地方有两处:
- value="com.nan.tinkerdemo.App"这里的Application全名需要改为自己项目的名字。
- sign配置里面要改为自己的配置。注意貌似这种方式只支持.keystore格式的签名文件。
然后我们打两个包,一个有BUG的old.apk,一个没有BUG的new.apk,然后通过下面的命令进行补丁文件的生成:
java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out out/
复制代码
最后在指定的out/文件下,找到patch_signed.apk,就是补丁文件。然后拷贝到已经安装好old.apk的手机中,通过下面的代码进行补丁加载即可:
TinkerManager.getInstance().addPatch(path);
复制代码
###Gradle方式生成Patch
上面命令行的方式接入Tinker还是比较麻烦的,而且每次都要手动去执行命令,十分麻烦,最重要的是不支持jks格式的签名文件。因此下面介绍怎么通过Gradle的方式进行引入。
首先,需要在项目的顶层gradle文件中引入Tinker的相关gradle脚本:
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
复制代码
app的Gradle文件如下:
apply plugin: 'com.android.application'
指定基准文件存放位置:在app/build/bakApk
def bakPath = file("${buildDir}/bakApk")
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.nan.tinkerdemo"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
flavorDimensions "versionCode"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
//recommend
dexOptions {
jumboMode = true
}
signingConfigs {
//签名打包配置
release {
storeFile file("../nan.jks")
storePassword "123456"
keyAlias "nan"
keyPassword "123456"
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
//这里要注意混淆规则
proguardFiles getDefaultProguardFile('proguard-android.txt'), '../TinkerTool/tinker_proguard.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:26.1.0'
//tinker的核心库
compile "com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"
//支持Dex分包
compile "com.android.support:multidex:1.0.2"
compile 'com.squareup.okhttp3:okhttp:3.3.0'
}
ext {
tinkerEnable = true
tinkerID = "1.0"
//下面需要注意的是,需要指定为生成基准包的具体位置
tinkerOldApkPath = "${bakPath}/XXX"
tinkerApplyMappingPath = "${bakPath}/XXX"
tinkerApplyResourcePath = "${bakPath}/XXX"
tinkerBuildFlavorDirectory = "${bakPath}/XXX"
}
def buildWithTinker() {
return ext.tinkerEnable
}
def getOldApkPath() {
return ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return ext.tinkerID
}
def getTinkerBuildFlavorDirectory(){
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
//启用tinker
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
//所有tinker相关的参数配置
tinkerPatch {
oldApk = getOldApkPath() //指定old apk的文件路径
ignoreWarning = false //不忽略tinker的警告
useSign = true//patch文件使用签名
tinkerEnable = buildWithTinker()//指定是否启用tinker
buildConfig {
applyMapping = getApplyMappingPath() //指定old apk打包时所使用的混淆文件
applyResourceMapping = getApplyResourceMappingPath() //指定old apk的资源文件
tinkerId = getTinkerIdValue() //指定TinkerID
keepDexApply = false
}
dex {
dexMode = "jar" //jar、raw
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目录
loader = ["com.nan.tinkerdemo.App"] //指定加载patch文件时用到的类
}
lib {
pattern = ["libs/*/*.so"]
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
//指定tinker可以修改的资源路径
ignoreChange = ["assets/sample_meta.txt"] //指定不受影响的资源路径
largeModSize = 100 //资源修改大小默认值
}
packageConfig {
configField("patchMessage", "fix the 1.0 version's bugs")
configField("patchVersion", "1.0")
}
}
//是否有多渠道
List<String> flavors = new ArrayList<>()
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
//拷贝生成的apk文件以及mapping文件
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format("MMdd-HH-mm-ss")
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[0].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")
}
}
}
}
}
}
}
复制代码
在这个文件中,我们主要干了下面的一些事情:
- 在tinkerPatch中对Tinker生成Patch的参数进行配置,配置都比较简单,关于tinker更多的配置请参考官方文档。
- 对签名进行配置
- 拷贝生成的apk文件以及mapping文件到指定的基准目录,这里是参考官方Demo的
需要注意的是,这里配置了tinker ID,那么在清单文件中就不需要重复配置了。
然后通过:
./gradlew assembleRelease
复制代码
进行基准包的生成,然后将上述脚本的基准包的具体信息替换脚本中的“XXX”处,最后执行Tinker的Gradle Task进行Patch文件生成:
./gradlew tinkerPatchRelease
复制代码
生成的Patch文件会在build/output/apk/中找到。
下一篇文章将介绍tinker的一些进阶使用。