(4.2.32.5)android热修复之Andfix方式:Andfix的补丁生成方法分析

在前文中,我们知道,如果需要生成补丁.patch文件需要借助apkpatch ,在本章节我们分析下该工具的内部原理。
apkpatch 是一个jar包,并没有开源出来,但是我们可以用 JD-G UI 或者 procyon 来看下它的 源码 ,版本1.0.3。

重要:

  1. 只提取出了 classes.dex 这个文件,所以源生工具并 不支持multidex ,如果使用了 multidex 方案,并且修复的类不在同一个 dex 文件中,那么补丁就不会生效。所以这里并不像作者在issue中提到的支持 multidex 那样,不过我们可以通过 JavaAssist 工具 修改 apkpatch 这个jar包,来达到支持multidex的目的
  2. AndFix 不支持增加成员变量,但是支持在新增方法中增加的局部变量 。 也不支持修改成员变量
  3. 对比方法过程中对比两个 dex 文件中同时存在的方法,如果方法实现不同则 存储为修改过的方法 ;如果方法名不同, 存储为新增的方法 ,也就是说 AndFix支持增加新的方法 ,这一点已经测试证明

1-首先找到 Main.class

位于 com.euler.patch 包下,找到 Main() 方法

public static void main(String[] args) {
    CommandLineParser parser = new PosixParser();
    CommandLine commandLine = null;
    option();
    try {
      commandLine = parser.parse(allOptions, args);
    } catch (ParseException e) {
      System.err.println(e.getMessage());
      usage(commandLine);
      return;
    }
//*************************1 start******************************
    if ((!commandLine.hasOption('k')) && (!commandLine.hasOption("keystore"))) {
      usage(commandLine);
      return;
    }
    if ((!commandLine.hasOption('p')) && (!commandLine.hasOption("kpassword"))) {
      usage(commandLine);
      return;
    }
    if ((!commandLine.hasOption('a')) && (!commandLine.hasOption("alias"))) {
      usage(commandLine);
      return;
    }
    if ((!commandLine.hasOption('e')) && (!commandLine.hasOption("epassword"))) {
      usage(commandLine);
      return;
    }

    File out = null;
    if ((!commandLine.hasOption('o')) && (!commandLine.hasOption("out")))
      out = new File("");
    else {
      out = new File(commandLine.getOptionValue('o'));
    }
    //***********************1 End********************************

    //***********************2 start********************************
    String keystore = commandLine.getOptionValue('k');
    String password = commandLine.getOptionValue('p');
    String alias = commandLine.getOptionValue('a');
    String entry = commandLine.getOptionValue('e');
    String name = "main";
    if ((commandLine.hasOption('n')) || (commandLine.hasOption("name")))
    {
      name = commandLine.getOptionValue('n');
    }

    if ((commandLine.hasOption('m')) || (commandLine.hasOption("merge"))) {
      String[] merges = commandLine.getOptionValues('m');
      File[] files = new File[merges.length];
      for (int i = 0; i < merges.length; i++) {
        files[i] = new File(merges[i]);
      }
      MergePatch mergePatch = new MergePatch(files, name, out, keystore, 
        password, alias, entry);
      mergePatch.doMerge();
    } else {
      if ((!commandLine.hasOption('f')) && (!commandLine.hasOption("from"))) {
        usage(commandLine);
        return;
      }
      if ((!commandLine.hasOption('t')) && (!commandLine.hasOption("to"))) {
        usage(commandLine);
        return;
      }

      File from = new File(commandLine.getOptionValue("f"));
      File to = new File(commandLine.getOptionValue('t'));
      if ((!commandLine.hasOption('n')) && (!commandLine.hasOption("name"))) {
        name = from.getName().split("\\.")[0];
      }
    //***********************2 End********************************

        //***********************3 start********************************

      ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore, 
        password, alias, entry);
    //***********************3 End********************************
      apkPatch.doPatch();
    }
  }

1.1 第一部分

我们前面介绍如何使用命令行打补丁包的命令,检查命令行是否有那些参数,如果没有要求的参数,就给用户相应的提示

1.2 第二部分

我们在打正式包的时候,会指定keystore,password,alias,entry相关参数。另外name就是最后生成的文件,可以忽略

1.3 第三部分

上面的参数传给ApkPatch进行初始化,调用其构造函数

final ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore, password, alias, entry);

1.4 第四部分

调用ApkPatch的doPatch()方法。

 apkPatch.doPatch();

2-ApkPatch

2.1 构造函数

  public ApkPatch(File from, File to, String name, File out, String keystore, String password, String alias, String entry)
  {
    super(name, out, keystore, password, alias, entry);
    this.from = from;
    this.to = to;
  }

调用了父类Build的构造函数,干的事情其实比较简单,就是给变量进行赋值。可以看到out,我们的输出文件就是这么来的,没有的话,它会自己创建一个。

  protected static final String SUFFIX = ".apatch";
  protected String name;
  private String keystore;
  private String password;
  private String alias;
  private String entry;
  protected File out;
  public Build(String name, File out, String keystore, String password, String alias, String entry)
  {
    this.name = name;
    this.out = out;
    this.keystore = keystore;
    this.password = password;
    this.alias = alias;
    this.entry = entry;
    if (!out.exists())
      out.mkdirs();
    else if (!out.isDirectory())
      throw new RuntimeException("output path must be directory.");
  }

2.2 doPatch 方法

可以简单描述为两步:

  • 对比apk文件,得到需要的信息
  • 将结果打包为apatch文件
public void doPatch() {
        try {
        //生成smali文件夹
            final File smaliDir = new File(this.out, "smali");
            if (!smaliDir.exists()) {
                smaliDir.mkdir();
            }
            //新建diff.dex文件
            final File dexFile = new File(this.out, "diff.dex");
            //新建diff.apatch文件
            final File outFile = new File(this.out, "diff.apatch");
            //第一步,拿到两个apk文件对比,对比信息写入DiffInfo
            final DiffInfo info = new DexDiffer().diff(this.from, this.to);
            //第二步,将对比结果info写入.smali文件中,然后打包成dex文件
            this.classes = buildCode(smaliDir, dexFile, info);
            //第三步,将生成的dex文件写入jar包,并根据输入的签名信息进行签名,生成diff.apatch文件
            this.build(outFile, dexFile);
            //第四步,将diff.apatch文件重命名,结束
            this.release(this.out, dexFile, outFile);
        }
        catch (Exception e2) {
            e2.printStackTrace();
        }
    }

2.2.1 第一步,对比并记录差异:DexDiffer().diff() 方法返回DiffInfo差异对象

public DiffInfo diff(final File newFile, final File oldFile) throws IOException {
  //提取新apk的dex文件
        final DexBackedDexFile newDexFile = DexFileFactory.loadDexFile(newFile, 19, true);
        //提取旧apk的dex文件
        final DexBackedDexFile oldDexFile = DexFileFactory.loadDexFile(oldFile, 19, true);
        final DiffInfo info = DiffInfo.getInstance();
        boolean contains = false;
        for (final DexBackedClassDef newClazz : newDexFile.getClasses()) {//一层for循环,新的所有类
            final Set<? extends DexBackedClassDef> oldclasses = oldDexFile.getClasses();
            for (final DexBackedClassDef oldClazz : oldclasses) {//二层for循环,旧的所有类
              //对比相同名的类,存储为修改的方法
                if (newClazz.equals(oldClazz)) {
                  //对比class文件的变量
                    this.compareField(newClazz, oldClazz, info);
                    //对比class文件的方法
                    this.compareMethod(newClazz, oldClazz, info);
                    contains = true;
                    break;
                }
            }
            if (!contains) {
              //否则是新增的方法
                info.addAddedClasses(newClazz);
            }
        }
        //返回包含diff信息的DiffInfo对象
        return info;
    }
2.2.1.1提取dex

就是提取 dex 文件的地方,在 DexFileFactory 类中

可以看到,只提取出了 classes.dex 这个文件,所以源生工具并 不支持multidex ,如果使用了 multidex 方案,并且修复的类不在同一个 dex 文件中,那么补丁就不会生效。所以这里并不像作者在issue中提到的支持 multidex 那样,不过我们可以通过 JavaAssist 工具 修改 apkpatch 这个jar包,来达到支持multidex的目的

public static DexBackedDexFile loadDexFile(File dexFile, int api, boolean experimental) throws IOException
  {
    return loadDexFile(dexFile, "classes.dex", new Opcodes(api, experimental));
  }
2.2.1.2 对比变量compareField

AndFix 不支持增加成员变量,但是支持在新增方法中增加的局部变量 。 也不支持修改成员变量

public void compareField(DexBackedField object, Iterable<? extends DexBackedField> olds, DiffInfo info)
  {
    for (DexBackedField reference : olds) {
      if (reference.equals(object)) {
        if ((reference.getInitialValue() == null) && 
          (object.getInitialValue() != null)) {
          info.addModifiedFields(object);
          return;
        }
        if ((reference.getInitialValue() != null) && 
          (object.getInitialValue() == null)) {
          info.addModifiedFields(object);
          return;
        }
        if ((reference.getInitialValue() == null) && 
          (object.getInitialValue() == null)) {
          return;
        }
        if (reference.getInitialValue().compareTo(
          object.getInitialValue()) != 0) {
          info.addModifiedFields(object);
          return;
        }
        return;
      }
    }

    info.addAddedFields(object);
  }
2.2.1.3 对比方法compareMethod

对比方法过程中对比两个 dex 文件中同时存在的方法,如果方法实现不同则 存储为修改过的方法 ;如果方法名不同, 存储为新增的方法 ,也就是说 AndFix支持增加新的方法 ,这一点已经测试证明

  public void compareMethod(DexBackedMethod object, Iterable<? extends DexBackedMethod> olds, DiffInfo info)
  {
    for (DexBackedMethod reference : olds) {
      if (reference.equals(object))
      {
        if ((reference.getImplementation() == null) && 
          (object.getImplementation() != null)) {
          info.addModifiedMethods(object);
          return;
        }
        if ((reference.getImplementation() != null) && 
          (object.getImplementation() == null)) {
          info.addModifiedMethods(object);
          return;
        }
        if ((reference.getImplementation() == null) && 
          (object.getImplementation() == null)) {
          return;
        }

        if (!reference.getImplementation().equals(
          object.getImplementation())) {
          info.addModifiedMethods(object);
          return;
        }
        return;
      }
    }

    info.addAddedMethods(object);
  }

2.2.2 第二步,将对比结果info写入.smali文件中,然后打包成dex文件:buildCode()

将上一步得到的 diff 信息写入 smali 文件,并且生成 diff.dex 文件。 smali 文件的命名以 _CF.smali 结尾,并且在修改的地方用自定义的 Annotation ( MethodReplace )标注,用于在替换之前查找修复的变量或方法

private static Set<String> buildCode(final File smaliDir, final File dexFile, final DiffInfo info) throws IOException, RecognitionException, FileNotFoundException {
        final ClassFileNameHandler outFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");
        final ClassFileNameHandler inFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");
        final DexBuilder dexBuilder = DexBuilder.makeDexBuilder();
        for (final DexBackedClassDef classDef : list) {
            final String className = classDef.getType();
            baksmali.disassembleClass(classDef, outFileNameHandler, options);
            final File smaliFile = inFileNameHandler.getUniqueFilenameForClass(TypeGenUtil.newType(className));
            classes.add(TypeGenUtil.newType(className).substring(1, TypeGenUtil.newType(className).length() - 1).replace('/', '.'));
            SmaliMod.assembleSmaliFile(smaliFile, dexBuilder, true, true);
        }
        dexBuilder.writeTo(new FileDataStore(dexFile));
        return classes;
    }

2.2.3 第三步,将生成的dex文件写入jar包,并根据输入的签名信息进行签名,生成diff.apatch文件:build(outFile, dexFile)

  • 首先从keystone里面获取应用相关签名
  • 实例化PatchBuilder,然后调用writeMeta(getMeta())
    • 将getMeta()中获取的Manifest内容写入”META-INF/PATCH.MF”文件中。
  • sealPatch——从input输入流中读取buffer数据然后写入到entry。然后联系到我上面提到的将dexfile和签名相关信息写入到classes.dex里面
  protected void build(File outFile, File dexFile)
    throws KeyStoreException, FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableEntryException
  {
    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    KeyStore.PrivateKeyEntry privateKeyEntry = null;
    InputStream is = new FileInputStream(this.keystore);
    keyStore.load(is, this.password.toCharArray());
    privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(this.alias, 
      new KeyStore.PasswordProtection(this.entry.toCharArray()));

    PatchBuilder builder = new PatchBuilder(outFile, dexFile, 
      privateKeyEntry, System.out);
    builder.writeMeta(getMeta());
    builder.sealPatch();
  }

  protected abstract Manifest getMeta();
2.2.3.1 getMeta()
  protected Manifest getMeta()
  {
    Manifest manifest = new Manifest();
    Attributes main = manifest.getMainAttributes();
    main.putValue("Manifest-Version", "1.0");
    main.putValue("Created-By", "1.0 (ApkPatch)");
    main.putValue("Created-Time", 
      new Date(System.currentTimeMillis()).toGMTString());
    main.putValue("From-File", this.from.getName());
    main.putValue("To-File", this.to.getName());
    main.putValue("Patch-Name", this.name);
    main.putValue("Patch-Classes", Formater.dotStringList(this.classes));
    return manifest;
  }

2.2.4 第四步,dex转.apatch

将dexFile进行md5加密,把build(outFile, dexFile);函数中生成的outFile重命名。哈哈,看到”.patch”有没有很激动!!我们的补丁包一开始的命名就是一长串。好了,到这里,补丁文件就生成了


  protected void release(File outDir, File dexFile, File outFile)
    throws NoSuchAlgorithmException, FileNotFoundException, IOException
  {
    MessageDigest messageDigest = MessageDigest.getInstance("md5");
    FileInputStream fileInputStream = new FileInputStream(dexFile);
    byte[] buffer = new byte[8192];
    int len = 0;
    while ((len = fileInputStream.read(buffer)) > 0) {
      messageDigest.update(buffer, 0, len);
    }

    String md5 = HexUtil.hex(messageDigest.digest());
    fileInputStream.close();
    outFile.renameTo(new File(outDir, this.name + "-" + md5 + ".apatch"));
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值