Android热修复6、Tinker详解及两种方式接入

2021-03-31  本文已影响0人  flynnny

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工具。

48.png

Tinker核心原理总结(每一个都很复杂)

1基于android原生ClassLoader,开发了自己的ClassLoader(去加载patch文件的字节码);
2基于android原生的aapt,开发了自己的aapt(定义自己的AssetManager完成patch中资源的加载);
3基于Dex文件的格式,研发了DexDiff算法(比较两个apk区别生成patch文件)

使用Tinker完成线上bug修复

集成阶段

gradle中添加Tinker依赖(官网省略了步骤)

49.png

*provided:只在编译时用,不打包到apk;
compile:不仅在编译时使用并打包到apk中

50.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

51.png 52.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:

53.png

数字一般是version name/code。一致才会安装patch文件。

生成old apk 后,修改一些功能后,build一个new apk,比如布局中添加一个button。

同样用 ./gradlew assembleRelease 创建release版本

patch文件生成

Tinker有两种patch生成方式(andfix只有一种)
1使用命令行方式完成patch包的生成
各文件作用讲解及命令参数讲解

54.png

修改xml中两个地方

55.png 56.png

使用tinker-patch命令生成patch文件

57.png 58.png

将这个文件push到指定目录中即可:

59.png

2使用gradle插件方式完成patch包的生成

在gradle中正确配置tinker参数(非常重要)尽量一次配好
有些必须的,有些非必须的(会默认)

1、记得去掉上面<manifest>里<application>里加的<meta-data>,这里不需要。

2、在gradle.propertes里新建一个变量,表明tinker版本号(后面用到就知道什么用)

60.png

3、工程gradle中新加了一个classpath

61.png

4、将导入包改为TINKER_VERSION方式

62.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

63.png

同时生成 bakApk下三个文件(脚本生效了)

64.png

有了bakApk后。就可以修改上面代码里三个 文件的路径(一定要写正确):

65.png

同步一下,之后打开gradle选项卡:

66.png

点击后就可生成patch文件

67.png

与andfix一样需要下载安装 封装成组件。

将tinker组件化

组件化讲解

最早在服务端,逐渐应用在客户端
将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,以减少耦合。

68.png

每个页面冗余

69.png

类似mvc结构
所有代码在一个工程

70.png

每个团队独立负责一个业务。

组件化优势:

1每个组件都可以有独立的版本,可以独立的编译测试;
2更加精细的分工,每个小团队专注于自己的功能;
3最高的代码可复用性,极大提高整个团队的代码可复用性。

将tinker组件化

71.png

新建TinkerService 与之前文章andfix基本一样

72.png 73.png 74.png 75.png

详情查看andfix那章

tinker高级功能

tinker如何支持多渠道打包

命令行方式只能一个渠道一个渠道打包
gradle方式只需要简单修改一下gradle脚本即可

76.png 77.png 78.png

准备完成,改动如下:

79.png 80.png

再添加一个多渠道脚本,从tinker官方demo中copy

81.png 86.png

生成release 会和之前不一样

82.png

然后修改开始的地方

83.png

同步后生成patch文件 (多了很多选项)

84.png 85.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。

88.png

AbstractResultService 在 onHandleIntent方法中(工作线程中调用安装结果方法onPatchResult,他执行在非ui线程,所以不能有ui操作)

DefaultTinkerResultService onPatchResult方法默认实现

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

其他方法并不需要复写和修改。

在初始化中修改代码:

87.png

LoadReport 和PatchReport

LoadReport 加载阶段所有事件监听
PatchReport 合成阶段所有异常监听

tinker使用中要注意哪些问题

Tinker源码讲解

从入口api开始(TinkerInstaller):
外观模式 都是调用tinker类完成功能

90.png 91.png

单例+构建者模式 创建。

加载patch方法:

92.png

走到了DefaultPatchListener--onPatchReceiver(String path)

93.png

通过tinkerpatch service中完成加载 和合并
到onHandleIntent()

94.png

tryPatch最终到了UpgradePatch 实现

95.png

里面也有签名等异常逻辑处理

96.png

dex 、library、res都可以处理了。

进入dex处理里

97.png 98.png 99.png 100.png

DexPatchApplier真正处理dex文件的类:
标记dex文件类型、找到偏移量,然后execute()逐个修改,内存写入本地。

同理查看资源的修改流程:

首先处理manifext

101.png

查看ResUtil的更新方法

102.png

tinker要注意的问题

目前不支持加固,以后肯定会支持(目前1.7.7)
tinker不支持修改AndroidManifest.xml
tinker不支持新增四大组件、transition动画,桌面图标等
部分三星android-21机型不兼容(rom改动大又不开源)

上一篇 下一篇

猜你喜欢

热点阅读