JAndFix

Project Url: qiuba/JAndFix
Introduction: JAndFix is a tool for Android real-time hot fix base on JAVA.
More: Author   ReportBugs   
Tags:

简述

  • JAndFix 是一种基于 Java 实现的 Android 实时热修复方案,它并不需要重新启动就能生效。JAndFix 是在 AndFix 的基础上改进实现,AndFix 主要是通过 jni 实现对 method(ArtMethod)结构题内容的替换。JAndFix 是通过 Unsafe 对象直接操作 Java 虚拟机内存来实现替换。

原理

  • 为何 JAndfix 能够做到即时生效呢? 原因是这样的,在 app 运行到一半的时候,所有需要发生变更的 Class 已经被加载过了,在 Android 上是无法对一个 Class 进行卸载的。而腾讯系的方案,都是让 Classloader 去加载新的类。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会 Resolve 为新的类。从而达到热修复的目的。JAndfix 采用的方法是,在已经加载了的类中直接拿到 Method(ArtMethod)在 JVM 的地址,通过 Unsafe 直接修改 Method(ArtMethod)地址的内容,是在原来类的基础上进行修改的。我们这就来看一下 JAndfix 的具体实现。

虚拟机调用方法的原理

为什么这样替换完就可以实现热修复呢?这需要从虚拟机调用方法的原理说起。在 Android 6.0,art 虚拟机中 ArtMethod 的结构是这个样子的:

@art/runtime/art_method.h

class ArtMethod FINAL {
 ... ...

 protected:
  // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
  // The class we are a part of.
  GcRoot<mirror::Class> declaring_class_;

  // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
  GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;

  // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
  GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;

  // Access flags; low 16 bits are defined by spec.
  uint32_t access_flags_;

  /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */

  // Offset to the CodeItem.
  uint32_t dex_code_item_offset_;

  // Index into method_ids of the dex file associated with this method.
  uint32_t dex_method_index_;

  /* End of dex file fields. */

  // Entry within a dispatch table for this method. For static/direct methods the index is into
  // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
  // ifTable.
  uint32_t method_index_;

  // Fake padding field gets inserted here.

  // Must be the last fields in the method.
  // PACKED(4) is necessary for the correctness of
  // RoundUp(OFFSETOF_MEMBER(ArtMethod, ptr_sized_fields_), pointer_size).
  struct PACKED(4) PtrSizedFields {
    // Method dispatch from the interpreter invokes this pointer which may cause a bridge into
    // compiled code.
    void* entry_point_from_interpreter_;

    // Pointer to JNI function registered to this method, or a function to resolve the JNI function.
    void* entry_point_from_jni_;

    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;

... ...
}

这其中最重要的字段就是 entrypoint_from_interprete和 entrypoint_from_quick_compiled_code了,从名字可以看出来,他们就是方法的执行入口。我们知道,Java 代码在 Android 中会被编译为 Dex Code。 art 中可以采用解释模式或者 AOT 机器码模式执行。

解释模式,就是取出 Dex Code,逐条解释执行就行了。如果方法的调用者是以解释模式运行的,在调用这个方法时,就会取得这个方法的 entrypoint_from_interpreter,然后跳转过去执行。

而如果是 AOT 的方式,就会先预编译好 Dex Code 对应的机器码,然后运行期直接执行机器码就行了,不需要一条条地解释执行 Dex Code。如果方法的调用者是以 AOT 机器码方式执行的,在调用这个方法时,就是跳转到 entrypoint_from_quick_compiled_code执行。

AndFix 的方法替换其本质是 ArtMethod 指针所指内容的替换。 Art Method--Art 6.0

变成了这样的整体替换 Art Method--Art 6.0

由 Unsafe 来实现相当于:

    //src means source ArtMethod Address,dest mean destinction ArtMethod Address
    private void replaceReal(long src, long dest) throws Exception {
        int methodSize = MethodSizeUtils.methodSize();
        int methodIndexOffset = MethodSizeUtils.methodIndexOffset();
        //methodIndex need not replace,becase the process of finding method in vtable need methodIndex
        int methodIndexOffsetIndex = methodIndexOffset / 4;
        //why 1? index 0 is declaring_class, declaring_class need not replace.
        for (int i = 1, size = methodSize / 4; i < size; i++) {
            if (i != methodIndexOffsetIndex) {
                int value = UnsafeProxy.getIntVolatile(dest + i * 4);
                UnsafeProxy.putIntVolatile(src + i * 4, value);
            }
        }
    }

so easy,JAndFix 就这样完成了方法替换。值得一提的是,由于忽略了底层 ArtMethod 结构的差异,对于所有的 Android 版本都不再需要区分,而统一以 Unsafe 实现即可,代码量大大减少。即使以后的 Android 版本不断修改 ArtMethod 的成员,只要保证 ArtMethod 数组仍是以线性结构排列,就能直接适用于将来的 Android 8.0、9.0 等新版本,无需再针对新的系统版本进行适配了。事实也证明确实如此,当我们拿到 Google 刚发不久的 Android O(8.0)开发者预览版的系统时。

对比方案

名字 公司 实现语言 及时生效 方法替换 方法的增加减少 Android 版本 机型 性能损耗 补丁大小 回滚 易用性
JAndFix 阿里天猫 JAVA 支持 支持 不支持 4.0+ ALL 支持
AndFix 阿里支付宝 C 支持 支持 不支持 4.0+ 极少部分不支持 支持
Tinker 腾讯 JAVA 不支持 支持 支持 ALL ALL 不支持
Robust 美团 JAVA 支持 不支持 不支持 ALL ALL 支持 差(需要反射调用,需要打包插件支持)
Dexposed 个人 C 支持 支持 不支持 4.0+ 部分不支持 支持 差(需要反射调用)

如何使用

     try {
            Method method1 = Test1.class.getDeclaredMethod("string");
            Method method2 = Test2.class.getDeclaredMethod("string");
            MethodReplaceProxy.instance().replace(method1, method2);
        } catch (Exception e) {
            e.printStackTrace();
        }

Running DEMO

  • 把整个项目放入你的 IDE 即可(Android Studio)

注意

Proguard

-keep class com.tmall.wireless.jandfix.MethodSizeCase { *;}

解释实现

  • 我以 Android Art 6.0 的实现来解释为什么这样实现就可实现方法替换
package com.tmall.wireless.jandfix;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * Created by jingchaoqinjc on 17/5/15. */

public class MethodReplace6_0 implements IMethodReplace {

    static Field artMethodField;

    static {
        try {
            Class absMethodClass = Class.forName("java.lang.reflect.AbstractMethod");
            artMethodField = absMethodClass.getDeclaredField("artMethod");
            artMethodField.setAccessible(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    public void replace(Method src, Method dest) {
        try {
            long artMethodSrc = (long) artMethodField.get(src);
            long artMethodDest = (long) artMethodField.get(dest);
            replaceReal(artMethodSrc, artMethodDest);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void replaceReal(long src, long dest) throws Exception {
        // ArtMethod struct size
        int methodSize = MethodSizeUtils.methodSize();
        int methodIndexOffset = MethodSizeUtils.methodIndexOffset();
        //methodIndex need not replace,becase the process of finding method in vtable
        int methodIndexOffsetIndex = methodIndexOffset / 4;
        //why 1? index 0 is declaring_class, declaring_class need not replace.
        for (int i = 1, size = methodSize / 4; i < size; i++) {
            if (i != methodIndexOffsetIndex) {
                int value = UnsafeProxy.getIntVolatile(dest + i * 4);
                UnsafeProxy.putIntVolatile(src + i * 4, value);
            }
        }
    }
}

1.declaringclass 不能替换,为什么不能替换,是因为 JVM 去调用方式时很多地方都要对 declaring_class 进行检查。替换 declaring_class 会导致未知的错误。 2.methodIndex 不能替换,因为 public proected 等简介寻址的访问权限,本质在寻找方法的时候会查找 virtual_methods,而 virtualmethods是个 ArtMethod 数组对象,需要通过 methodIndex 来查找,如果你的 methodIndex 不对会导致方法寻址出错。 3.为什么 AbstractMethod 类中对应的 artMethod 属性的值可以作为 c 层 ArtMethod 的地址直接使用?看源码:

@@art/mirror/abstract_method.cc
ArtMethod* AbstractMethod::GetArtMethod() {
  return reinterpret_cast<ArtMethod*>(GetField64(ArtMethodOffset()));
}

@@art/mirror/abstract_method.h
static MemberOffset ArtMethodOffset() {
    return MemberOffset(OFFSETOF_MEMBER(AbstractMethod, art_method_));
  }

从源码可以看出 C 层在获取 ArtMethod 的地址,实际上就是把 AbstractMethod 的 artMethod 强制转换成了 ArtMethod*指针,及我们在 Java 拿到的 artMethod 就是 c 层 ArtMethod 的实际地址。是不是很简单。

参考

License

Apache License, Version 2.0

Copyright (c) 2017, alibaba-inc.com

Support Me
Apps
About Me
Google+: Trinea trinea
GitHub: Trinea