Tinker是什么
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
它主要包括以下几个部分:
- gradle编译插件:
tinker-patch-gradle-plugin
- 核心sdk库:
tinker-android-lib
- 非gradle编译用户的命令行版本:
tinker-patch-cli.jar
为什么使用Tinker
当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。
Tinker核心原理
- 基于android原生的ClassLoader,开发了自己的ClassLoader
- 基于android原生的aapt,开发了自己的aapt
- 微信团队自己基于Dex文件的格式,研发了DexDiff算法
使用Tinker完成bug修复
在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖
如果Gradle版本是3.0 (com.android.tools.build:gradle:3.0.0' )或者以上是的话配置
//tinker的核心库
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
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.1"
gradle版本是3.0以下的配置
dependencies {
//tinker的核心库
implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
//可选,用于生成application类
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
}
implementation 的作用是编译的时候使用,把库打包到apk中
provided 的作用是只参与编译,不参与把库打包到apk中,减小apk包体积
gradle.properties配置公共参数
android.enableJetifier=true
TINKER_VERSION=1.9.14.5
android.enableR8 = false
public class TinkerManager {
private static boolean isInstalled = false;
private static ApplicationLike mApplike;//委托类
/**
* Tinker的初始化
* @param applicationLike
*/
public static void installTinker(CustomTinkerLike applicationLike) {
mApplike = applicationLike;
if (isInstalled) {
return;
} else {
TinkerInstaller.install(mApplike);//完成Tinker的初始化
isInstalled = true;
}
}
//完成Patch文件的加载
public static void loadPatch(String path) {
if (Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
}
}
//通过ApplicationContext获取Context
private static Context getApplicationContext() {
if (mApplike != null) {
return mApplike.getApplication().getApplicationContext();
}
return null;
}
}
@DefaultLifeCycle(application = ".MyTinkerApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class CustomTinkerLike extends ApplicationLike {
public CustomTinkerLike(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);
//使应用支持分包
MultiDex.install(base);
TinkerManager.installTinker(this);
}
}
Tinker需要监听Application对象的周期,通过ApplicationLike 进行委托,通过这个委托,可以在ApplicationLike完成对Application对象的周期的监听,在不同的生命周期阶段,完成不同的操作。
build一下就会生成MyTinkerApplication,在清单文件里面使用下。
<application
android:name=".MyTinkerApplication"
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=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
TInker的集成和初始化就完成了
public class MainActivity extends AppCompatActivity {
private static final String FILE_END = ".apk";
private String mPatchDir;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.loadPatch).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadPatch();
}
});
mPatchDir = getExternalCacheDir().getAbsolutePath() + "/tpatch/";
//是为了创建我们的文件夹
File file = new File(mPatchDir);
if (file == null || !file.exists()) {
file.mkdir();
}
}
public void loadPatch() {
TinkerManager.loadPatch(getPatchName());
}
private String getPatchName() {
return mPatchDir.concat("patch_signed").concat(FILE_END);
}
}
布局文件
patch生成方式
- 使用 命令行的方式完成Patch包的生成(不介绍了)
- 使用Gradle插件的方式完成Patch包的生成
集成插件
在项目的build.gradle中,添加tinker-patch-gradle-plugin
的依赖
buildscript {
dependencies {
classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
}
}
在Gradle中配置生成patch文件
- 在Gradle中正确配置tinker参数
- 在Android Studio中直接生成patch文件
在Gradle中配置tinker参数
apply plugin: 'com.android.application'
def javaVersion = JavaVersion.VERSION_1_7
android {
signingConfigs {
release {
storeFile file('D:\\MyDownload\\TinkerDemo\\app\\newSign.jks')
storePassword '888888'
keyAlias = 'tinker'
keyPassword '888888'
}
}
compileSdkVersion 29
buildToolsVersion "29.0.3"
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
dexOptions {
jumboMode = true
}
defaultConfig {
applicationId "com.example.tinkerdemo"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
multiDexEnabled true
multiDexKeepProguard file("tinker_multidexkeep.pro")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
}
buildTypes {
release {
minifyEnabled = true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//tinker的核心库
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
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.1"
}
def bakPath = file("${buildDir}/bakApk/")
ext {
tinkerEnanle = true
tinkerOldApkPath = "${bakPath}/app-release-0714-14-29-57.apk"
tinkerId = "1.0"
tinkerApplyMappingPath = "${bakPath}/app-release-0714-14-29-57-mapping.text"
tinkerApplyResourcePath = "${bakPath}/app-release-0714-14-29-57-R.text"
}
def buildWithTinker() {
return ext.tinkerEnanle
}
def getOldApkPath() {
return ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return ext.tinkerId
}
if (buildWithTinker()) {
//启用tinker
apply plugin: 'com.tencent.tinker.patch'
//所有tinker相关的参数配置
tinkerPatch {
oldApk = getOldApkPath() //指定old apk文件
ignoreWarning = false //不忽略tinker的警告,有则中止patch文件的生成
useSign = true//强制patch文件也使用签名
tinkerEnanle = buildWithTinker()//指定是否启用Tinker
buildConfig {
applyMapping = getApplyMappingPath()//指定old apk 打包时所使用的混淆文件
applyResourceMapping = getApplyResourceMappingPath()//指定old apk所使用的资源文件
tinkerId = getTinkerIdValue()//指定TinkerID,每个patch文件的唯一标识符
keepDexApply = false
isProtectedApp = false
supportHotplugComponent = false
}
dex {
dexMode = "jar" //jar、raw
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目录
loader = ["com.example.tinkerdemo.MyTinkerApplication"]//加载patch包所用的类
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
//指定tinker可以修改的所有资源路径
ignoreChange = ["assets/sample_meta.txt"]//修改了也不想patch包里生效
largeModSize = 100 //资源修改大小的默认值
}
packageConfig {
configField("patchMessage", "fix the 1.0 version's bugs")
configField("patchVersion", "1.0")
}
}
}
注意事项1:打开混淆 minifyEnabled true,要不然不能生产mapping文件
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
注意事项2:tinker相关的参数配置的时候要把自动生成的MyTinkerApplication填进去
loader = ["com.example.tinkerdemo.MyTinkerApplication"]//加载patch包所用的类
添加混淆
# tinker混淆规则
-keepattributes SourceFile,LineNumberTable
-dontwarn com.google.**
-dontwarn com.android.**
接下来,ext里面的文件路径还没有指定。可以引入脚本,自动将文件保存到bakPath中
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")
/**
* 复制基准包和其他必须文件到指定目录
*/
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")
}
}
}
}
}
}
这里是可以从官方文档复制
准备阶段
- build一个old apk 并安装到手机
- 修改一些功能后,build一个new apk
打包apk,安装到手机上,保留基础包
修改布局文件,打包新的apk,
新的apk生成以后,我们要将ext里面的文件路径补充完整,这个是刚刚我们基础包的文件路径,不要弄错。
ext {
tinkerEnanle = true
tinkerOldApkPath = "${bakPath}/app-release-0709-15-46-10.apk"
tinkerId ="1.0"
tinkerApplyMappingPath = "${bakPath}/app-release-0709-15-46-10-mapping.txt"
tinkerApplyResourcePath ="${bakPath}/app-release-0709-15-46-10-R.text"
}
点击tinkerPatchRelease就可以生成patch
在studio的terminal面板中输入命令adb push 将补丁文件push到手机
在手机的文件夹中生成了补丁文件
点击加载补丁包,发现新添加的内容显示出来了,完美!!!
tinker自定义行为
主要是自定义patch安装以后的行为,tinker默认实现是杀掉当前应用的进程,app闪退,第二次启动修复了bug,我们进行优化,不要让app闪退
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
if (checkIfNeedKill(result)) {
android.os.Process.killProcess(android.os.Process.myPid());
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
patch加载完以后,会删除手机里的patch包,节省手机的空间
android.os.Process.killProcess(android.os.Process.myPid());
这个是杀掉当前的线程,导致app闪退,app的体验非常不好
我们需要重写这个方法
public class CustomResultSerice extends DefaultTinkerResultService {
private static final String TAG ="Tinker.SampleResultService";
//返回patch文件的安装结果
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
}
}
}
这样自定义以后,app不会闪退,加载完patch包以后,第二次启动的时候生效。
public class TinkerManager {
private static boolean isInstalled = false;
private static ApplicationLike mApplike;//委托类
/**
* Tinker的初始化
* @param applicationLike
*/
public static void installTinker(ApplicationLike applicationLike) {
mApplike = applicationLike;
if (isInstalled) {
return;
} else {
LoadReporter loadReporter = new DefaultLoadReporter(applicationLike.getApplication());//日志上报
PatchReporter patchReporter = new DefaultPatchReporter(applicationLike.getApplication());//日志上报
DefaultPatchListener mPatchListener = new DefaultPatchListener(applicationLike.getApplication());
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(applicationLike, loadReporter, patchReporter, mPatchListener, CustomResultSerice.class, upgradePatchProcessor);
isInstalled = true;
}
}
//完成Patch文件的加载
public static void loadPatch(String path) {
if (Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
}
}
//通过ApplicationContext获取Context
private static Context getApplicationContext() {
if (mApplike != null) {
return mApplike.getApplication().getApplicationContext();
}
return null;
}
}
注意再manifest注册这个service
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.tinkerdemo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".MyTinkerApplication"
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=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".CustomResultSerice"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.tinkerdemo.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</application>
</manifest>
在这个过程中如果发现onPatchResult的result.isSuccess一直返回false,那么在清单文件里面加上provider的代码
provider_paths.xml内容如下
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- /storage/emulated/0/Download/${applicationId}/.beta/apk-->
<external-path name="beta_external_path" path="Download/"/>
<!--/storage/emulated/0/Android/data/${applicationId}/files/apk/-->
<external-path name="beta_external_files_path" path="Android/data/"/>
</paths>
总结,tinker不仅适用于bug修复,也适用于小功能的添加。