Android热修复6、Tinker详解及两种方式接入
Tinker的基本介绍
微信android的热补丁方案,支持动态下发代码,so库以及资源,让应用在不需要重新安装的情况下实现更新。
由三个部分组成:
1gradle编译插件:tinker-patch-gradle-plugin,AS中直接生成patch文件;
2核心sdk库:tinker-android-lib,tinker提供的api;
3非gradle编译用户的执行命令版本:tinker-patch-cli.jar,玩确实为了eclipse用户生成patch工具。
![](https://img.haomeiwen.com/i14355128/c9ae0d33e6983c55.png)
Tinker核心原理总结(每一个都很复杂)
1基于android原生ClassLoader,开发了自己的ClassLoader(去加载patch文件的字节码);
2基于android原生的aapt,开发了自己的aapt(定义自己的AssetManager完成patch中资源的加载);
3基于Dex文件的格式,研发了DexDiff算法(比较两个apk区别生成patch文件)
使用Tinker完成线上bug修复
集成阶段
gradle中添加Tinker依赖(官网省略了步骤)
![](https://img.haomeiwen.com/i14355128/0d62badfe7c603fc.png)
*provided:只在编译时用,不打包到apk;
compile:不仅在编译时使用并打包到apk中
![](https://img.haomeiwen.com/i14355128/f1f706fe32f83945.png)
添加多dex支持,不支持则无需调用
在代码中完成对Tinker的初始化:(定义TinkerManager封装API)
创建tinker目录,创建TinkerManager
/*
*封装Tinker API
*/
public class TinkerManager{
private static boolean isInstalled = false;
private static ApplicationLike mAppLike;//tinker提供的对象
//由外部调用完成tinker初始化
public static void installTinker(ApplicationLike applicationLike){
mAppLike = applicationLike;
if(isInstalled){
return;
}
TinkerInstaller.install(mAppLike);//完成tinker初始化
isInstalled = true;
}
//完成patch文件加载
public static void loadPatch(String path){
if(Tinker.isTinkerInstalled()){
TinkerInstaller.onReceiverUpgradePatch(getApplicationContext(),path);
}
}
//通过ApplicationLike获取Context
private static Context getApplicationContext(){
if(mAppLike!=null){
return mAppLike.getApplication().getApplicationContext();
}
return null;
}
}
tinkerManager调用需要在ApplicationLike中去初始化(官方推荐,而不是Application):
tinker目录下创建CustomTinkerLike:
//添加注解,通过like对象生成application对象
@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 applicationStartMillsTime,
Intent tinkerResultIntent){
super(application,tinkerFlags,tinkerLoadVerifyFlag,
applicationStartElapsedTime,applicationStartMillsTime,
tinkerResultIntent);
}
//在这里完成tinker初始化
@Override
public void onBaseContextAttached(Context base){
super.onBaseContextAttached(base);
//应用支持分包
MultiDex.install(base);
TinkerManager.installTinker(this);
}
}
为什么不在application中初始化:
因为tinker要监听application生命周期,通过like对象进行委托,完成监听,在不同生命周期阶段完成不同工作。
这里的MyTinkerApplication 就相当于我们定义的Application,需要声明中定义。先build一下,就会帮我们生成Application
![](https://img.haomeiwen.com/i14355128/1584480390c31fca.png)
![](https://img.haomeiwen.com/i14355128/fc9fd7e641771869.png)
TinkerApplication 继承自Application。
准备阶段
build一个old apk 并安装到手机(不需要一定有bug)与Andfix基本一样。
public class MainActivity extends AppCompatActivity{
private static final String FILE_END=".apk";//andfix是apatch
private String mPatchDir;
@Override
protected void onCreate(Budle savedInsatnceState){
super.onCreate(savedInsatanceState);
setContentView(R.layout.activity_main);
//小米三下是/storage/emulated/0/Android/data/com.imocc/cache/tpatch/
mPatchDir = getExternalCatcheDir().getAbsolutePath()+"/tpatch";//andfix 是apatch
//创建文件夹
File file = new File(mPatchDir );
if(file==null||!file.exists()){
file.mkdir();
}
}
//ui是一个按钮loadPatch
public void loadPatch(View view){
TinkerManager.loadPatch(getPatchName());
}
//构造patch文件名
private String getPatchName(){
return mPatchDir.concat("imooc").concat(FILE_END);
}
}
打包前一定还要在清单文件application中添加meta-data:
![](https://img.haomeiwen.com/i14355128/2c7c59d053df2880.png)
数字一般是version name/code。一致才会安装patch文件。
生成old apk 后,修改一些功能后,build一个new apk,比如布局中添加一个button。
同样用 ./gradlew assembleRelease 创建release版本
patch文件生成
Tinker有两种patch生成方式(andfix只有一种)
1使用命令行方式完成patch包的生成
各文件作用讲解及命令参数讲解
![](https://img.haomeiwen.com/i14355128/177ca8dafe323a36.png)
修改xml中两个地方
![](https://img.haomeiwen.com/i14355128/d057de3b28d07e2b.png)
![](https://img.haomeiwen.com/i14355128/eac785709a9312f3.png)
使用tinker-patch命令生成patch文件
![](https://img.haomeiwen.com/i14355128/6cc214aae33c5d05.png)
![](https://img.haomeiwen.com/i14355128/6d762e6fd652b391.png)
将这个文件push到指定目录中即可:
![](https://img.haomeiwen.com/i14355128/aad58f5c2e4b3dd2.png)
2使用gradle插件方式完成patch包的生成
在gradle中正确配置tinker参数(非常重要)尽量一次配好
有些必须的,有些非必须的(会默认)
1、记得去掉上面<manifest>里<application>里加的<meta-data>,这里不需要。
2、在gradle.propertes里新建一个变量,表明tinker版本号(后面用到就知道什么用)
![](https://img.haomeiwen.com/i14355128/0dedd2c02591f2c6.png)
3、工程gradle中新加了一个classpath
![](https://img.haomeiwen.com/i14355128/21abe2935040f3b3.png)
4、将导入包改为TINKER_VERSION方式
![](https://img.haomeiwen.com/i14355128/f461fad6e37d4b69.png)
在开关语句块中配置tinker开关:
apply plugin:'com.adroid.application'
def javaVersion = JavaVersion.VERSION_1_7
//这里的buildDir代表app下的build文件夹,成功后会创建bakApk文件夹,存放oldapk
def bakPath=file("${buildDir}/bakApk")
android{...}
dependencies{...}
ext{
tinkerEnable = true
tinkerOldApkPath="${bakPath}/"
tinkerApplyMappingPath="${bakPath}/"
tinkerResourceMappingPath="${bakPath}/"
tinkerID = "1.0"//和应用的version name一样
}
def buildWithTinker(){
return ext.tinkerEnable
}
def getOldApkPath(){
return ext.tinkerOldApkPath
}
def getApplyMappingPath(){
return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath(){
return ext.tinkerResourceMappingPath
}
def getTinkerIdValue(){
return ext.tinkerID
}
if(buildWhthTinker()){
// 启用tinker
//引入tinkerpatch包才有下面的tinkerpatch配置
apply plugin:'com.tencent.tinker.patch'
//所有tinker相关参数配置
tinkerPatch{
//基本配置
oldApk=getOldApkPath()//指定old apk文件路径
ignoreWarning = false //是否忽略tinker警告,有会终止patch生成(具体条件查询官网)
useSign = true //patch文件启用签名,防止patch篡改
tinkerEnable = buildWithTinker();//指定是否启用tinker
//以下要一块一块的配置
buildConfig{
applyMapping = getApplyMappingPath()//指定oldapk打包时用的混淆文件,因为patch也要混淆,需要一样
applyResourceMapping = getApplyResourceMappingPath()//指定oldApk资源文件
tinkerId = getTinkerIdValue()//指定tinkerID
keepDexApply =false//通常为false即可
}
dex{
dexMode = "jar"//tinker支持jar和raw。jar支持api14以下,raw只能在14以上;jar模式是tinker处理dex文件时会将dex重新压缩成jar文件,然后对jar处理;raw模式直接对dex文件处理。jar模式生成会相对小一些。
pattern =["classes*.dex","assets/secondary-dex-?.jar"]//指定tinker要处理的dex文件都位于哪些目录,官方的,写上即可
loader=["com.imooc.tinker.MyTinkerApplication"]//指定加载patch文件时用到的类
}
lib{
pattern = ["libs/*/*.so"]//指定tinker可以修改的所有so存放位置
}
res{
pattern=["res/*","assets/*","resources.arcs","AndroidManifest.xml"]//指定tinker可以修改的所有资源存放位置
// ignoreChange = ["assets/sample_mata.txt"]//随便指定一个文件即使修改了也不再patch生效
largeModSize =100 //资源修改大小默认值100k,超过后会用Dexdiffer算法减少patch体积
}
//以上必须项配置完毕
packageConfig{//用来说明本次压缩相关信息
configField("patchMessage","fix the 1.0 version's bugs")
configField("patchVersion","1.0")
}
}
//通过脚本,每次把生成的放入相应的地方
//hasFlavors 判断配置是否用到多渠道
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each{ flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size()>0
/**
*bak apk and mapping
*/
android.applicationVariants.all{variant ->
/*
*tast type,you wang 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}":"${fileName}"
def destPath = hasFlavors?file("${bakPath}/${project.name}-${date}/${variant.flavorName}"):bakPath
//找到apk copy到bak中
from variant.outputs.outputFile
into destPath
rename{String fileName ->
fileName.replace("${fileNamePrefix}.apk","${newFileNamePrefix}")
}
//找到mapping copy到bak中
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename{String fileName->
fileName.replace("mapping.txt","${newFileNamePrefix}-mapping.txt")
}
//找到R.txt copy到bak中
from "{buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
rename{String fileName->
fileName.replace("R.txt","${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
}
在AS中直接生成Patch文件
删掉build目录,生成release apk
![](https://img.haomeiwen.com/i14355128/d04acfe5cd387c3f.png)
同时生成 bakApk下三个文件(脚本生效了)
![](https://img.haomeiwen.com/i14355128/38dccd55adb1857e.png)
有了bakApk后。就可以修改上面代码里三个 文件的路径(一定要写正确):
![](https://img.haomeiwen.com/i14355128/7579bf607f0526c2.png)
同步一下,之后打开gradle选项卡:
![](https://img.haomeiwen.com/i14355128/60af51ad34d6a9e3.png)
点击后就可生成patch文件
![](https://img.haomeiwen.com/i14355128/b172e75137a26d5d.png)
与andfix一样需要下载安装 封装成组件。
将tinker组件化
组件化讲解
最早在服务端,逐渐应用在客户端
将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,以减少耦合。
![](https://img.haomeiwen.com/i14355128/0044d4c4f9add72b.png)
每个页面冗余
![](https://img.haomeiwen.com/i14355128/a7f9ca7be283bb4e.png)
类似mvc结构
所有代码在一个工程
![](https://img.haomeiwen.com/i14355128/81e5b40f2593a201.png)
每个团队独立负责一个业务。
组件化优势:
1每个组件都可以有独立的版本,可以独立的编译测试;
2更加精细的分工,每个小团队专注于自己的功能;
3最高的代码可复用性,极大提高整个团队的代码可复用性。
将tinker组件化
![](https://img.haomeiwen.com/i14355128/95c577fa0fc49b56.png)
新建TinkerService 与之前文章andfix基本一样
![](https://img.haomeiwen.com/i14355128/54d4e74a88364bbd.png)
![](https://img.haomeiwen.com/i14355128/e206c0d38db1e621.png)
![](https://img.haomeiwen.com/i14355128/a8e9841844440b42.png)
![](https://img.haomeiwen.com/i14355128/56f697edcfe03a33.png)
详情查看andfix那章
tinker高级功能
tinker如何支持多渠道打包
命令行方式只能一个渠道一个渠道打包
gradle方式只需要简单修改一下gradle脚本即可
![](https://img.haomeiwen.com/i14355128/41cb39e973b8b9d2.png)
![](https://img.haomeiwen.com/i14355128/d032ccd3eaa73bb5.png)
![](https://img.haomeiwen.com/i14355128/ade3c39525798a41.png)
准备完成,改动如下:
![](https://img.haomeiwen.com/i14355128/47fdab0df71ac177.png)
![](https://img.haomeiwen.com/i14355128/9ba46bc39f94f8ae.png)
再添加一个多渠道脚本,从tinker官方demo中copy
![](https://img.haomeiwen.com/i14355128/5284540f705de3ac.png)
![](https://img.haomeiwen.com/i14355128/7da4249e3c235fb6.png)
生成release 会和之前不一样
![](https://img.haomeiwen.com/i14355128/a4dd20ededf7a49b.png)
然后修改开始的地方
![](https://img.haomeiwen.com/i14355128/fa8ac6019cc2ac20.png)
同步后生成patch文件 (多了很多选项)
![](https://img.haomeiwen.com/i14355128/a60e2072002c008a.png)
![](https://img.haomeiwen.com/i14355128/51fe3b0c62a7a63f.png)
如何自定义tinker行为(重点)
自定义PatchLiatener监听Pathc receiver事件:
defaultPatchLiatener patch()方法对patch文件进行校验,通常会扩展这部分(比如md5校验)
package com.imooc.tinker;
import android.content.Context;
import com.imooc.util.Utils;
import com.tencent.tinker.lib.listener.DefaultPatchListener;
import com.tencent.tinker.loader.shareutil.ShareConstants;
/**
* @author: vision
* @function: 1.较验patch文件是否合法 2.启动Service去安装patch文件
* @date: 17/5/11
*/
public class CustomPatchListener extends DefaultPatchListener {
private String currentMD5;
public void setCurrentMD5(String md5Value) {
this.currentMD5 = md5Value;
}
public CustomPatchListener(Context context) {
super(context);
}
@Override
protected int patchCheck(String path) {
//patch文件ms5较验,需要服务器返回在PatchInfo类中定义。
if (!Utils.isFileMD5Matched(path, currentMD5)) {
return ShareConstants.ERROR_PATCH_DISABLE;
}
return super.patchCheck(path);
}
}
如果想弹一个对话框,需要重写onPatchReceived
方法(不过建议patch安装越隐蔽越好)
自定义TinkerResultService改变patch安装成功后行为:
DefaultTinkerResultService继承自AbstractResultService 他又继承自IntentService,内部有一个handler。
![](https://img.haomeiwen.com/i14355128/a43525a96737029d.png)
AbstractResultService 在 onHandleIntent方法中(工作线程中调用安装结果方法onPatchResult,他执行在非ui线程,所以不能有ui操作)
DefaultTinkerResultService onPatchResult方法默认实现
![](https://img.haomeiwen.com/i14355128/1128398ec89b7822.png)
所以重写onPatchResult方法:
/**
* 本类的作用:决定在patch安装完以后的后续操作,默认实现是杀进程
*/
public class CustomResultService 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));
}
}
}
其他方法并不需要复写和修改。
在初始化中修改代码:
![](https://img.haomeiwen.com/i14355128/758746ea68da1b05.png)
LoadReport 和PatchReport
LoadReport 加载阶段所有事件监听
PatchReport 合成阶段所有异常监听
tinker使用中要注意哪些问题
Tinker源码讲解
从入口api开始(TinkerInstaller):
外观模式 都是调用tinker类完成功能
![](https://img.haomeiwen.com/i14355128/5bb01bc9795c611c.png)
![](https://img.haomeiwen.com/i14355128/22710df68153675a.png)
单例+构建者模式 创建。
加载patch方法:
![](https://img.haomeiwen.com/i14355128/cfc7c2c543a98a76.png)
走到了DefaultPatchListener--onPatchReceiver(String path)
![](https://img.haomeiwen.com/i14355128/edcd109225dd10b7.png)
通过tinkerpatch service中完成加载 和合并
到onHandleIntent()
![](https://img.haomeiwen.com/i14355128/0eb3833b071302d1.png)
tryPatch最终到了UpgradePatch 实现
![](https://img.haomeiwen.com/i14355128/e7baf54703005af4.png)
里面也有签名等异常逻辑处理
![](https://img.haomeiwen.com/i14355128/88c4b2accdf48fd9.png)
dex 、library、res都可以处理了。
进入dex处理里
![](https://img.haomeiwen.com/i14355128/ab50a99b0203ce57.png)
![](https://img.haomeiwen.com/i14355128/cb98109623d7dabb.png)
![](https://img.haomeiwen.com/i14355128/225c4f59ca3a6426.png)
![](https://img.haomeiwen.com/i14355128/f464a5d7b11505a7.png)
DexPatchApplier真正处理dex文件的类:
标记dex文件类型、找到偏移量,然后execute()逐个修改,内存写入本地。
同理查看资源的修改流程:
首先处理manifext
![](https://img.haomeiwen.com/i14355128/9d2ba1eb44e6fd3f.png)
查看ResUtil的更新方法
![](https://img.haomeiwen.com/i14355128/9521f57b8d2d6ba8.png)
tinker要注意的问题
目前不支持加固,以后肯定会支持(目前1.7.7)
tinker不支持修改AndroidManifest.xml
tinker不支持新增四大组件、transition动画,桌面图标等
部分三星android-21机型不兼容(rom改动大又不开源)