热修复原理1:java代码

无需通过升级APK来实现BUG修复,有人选择插件化来解决,但是对于已经开发好的APP,移植成本非常高,既要学习插件化工具,又要对老代码进行改造。

热修复更加轻量、灵活,直接把补丁上传到云端,下拉补丁后立即生效。热修复主要有两种方案,底层替换和类加载,一般配合使用。

  • 底层替换方案:限制颇多,但时效性最好,加载轻快,立即见效。
  • 类加载方案:时效性差,需要重新冷启动才能见效,但修复范围广,限制少。

1.底层替换原理

直接替换ART虚拟机中的ArtMethod结构可以达到即时生效。
直接替换的难点在于获取ArtMethod结构的大小,由于ArtMethod可能被厂商修改,不能直接使用AOSP原始的ArtMethod,因此要想办法兼容。

memcpy(oldmeth, newmeth, sizeof(ArtMethod));

通过ART虚拟机源码,发现类的method空间是线性的,一个接一个紧密new出来的排列在数组中的。

android9.0/art/runtime/class_linker.cc:
LengthPrefixedArray<ArtMethod>* ClassLinker::AllocArtMethodArray(Thread* self,
                                                                 LinearAlloc* allocator,
                                                                 size_t length) {
  if (length == 0) {
    return nullptr;
  }
  const size_t method_alignment = ArtMethod::Alignment(image_pointer_size_);
  const size_t method_size = ArtMethod::Size(image_pointer_size_);
  const size_t storage_size =
      LengthPrefixedArray<ArtMethod>::ComputeSize(length, method_size, method_alignment);
  void* array_storage = allocator->Alloc(self, storage_size);
  auto* ret = new (array_storage) LengthPrefixedArray<ArtMethod>(length);
  CHECK(ret != nullptr);
  for (size_t i = 0; i < length; ++i) {
    new(reinterpret_cast<void*>(&ret->At(i, method_size, method_alignment))) ArtMethod;
  }
  return ret;
}

根据这个特性可以看出,两个相邻ArtMethod的差值就是ArtMethod的大小,我们可以自己构造一个类来巧妙获取。

public class NativeMethodModel {
  public static void f1(){}
  public static void f2(){}
}

可以在JNI层获取它们的地址差值:

size_t firMid = (size_t) env->GetStaticMethodID(nativeMethodModelClazz, "f1", "()V");
size_t secMid = (size_t) env->GetStaticMethodID(nativeMethodModelClazz, "f2", "()V");

size_t methSize = secMid - firMid;

这个methSize就可以作为sizeof(ArtMethod)的值了。

memcpy(oldmeth, newmeth, methSize);

访问权限问题:

  • 方法调用时的权限检查
    新替换的方法的所属类,和原先方法的所属类,是不同的类,被替换的方法有权限访问这个类的其他private方法吗?
    通过oat code观察,调用同一个类的私有方法,没有任何权限检查,可以推测是编译时的优化,确认了两个方法同属一个类,所以机器码不做任何权限检查。

  • 同包名下的权限问题
    补丁中的类在访问同包名下的类时,会报异常,是由于补丁类是从补丁包的Classloader加载的,与原来的base包不是同一个Classloader。可以使用反射修改ClassLoader规避:

Field classLoaderField = Class.class.getDeclaredField("classLoader");
classLoaderField.setAccessible(true);
classLoaderField.set(newClass, oldClass.getClassLoader());
  • 反射调用非静态方法产生的问题
    当一个非静态方法被热替换后,在反射调用这个方法时,会抛异常:
Caused: java.lang.IllegalArgumentException:
  Excepted receiver of type com.patch.demo.BaseBug
, but got com.patch.demo.BaseBug

com.patch.demo.BaseBug是两个不同的类,前者是被热替换方法所属的类,由于我们替换了ArtMethod的declaring_class_,因此就是新的补丁类。后者是被调用的实例对象所属类,是原有的BaseBug。
静态方法是类级别直接调用的,不需要接收对象实例作为参数。
这种反射调用非静态方法产生的问题可以通过冷启动对付。

新增方法、字段的影响:
除了反射问题,补丁类里面存在方法、字段的新增或者减少,都是不适用的。
方法、字段数量的变化,会导致dex中的方法索引、字段索引发生变化,所以无法正常替换。

2.你所不知的Java

3.冷启动类加载原理

4.多态对冷启动类加载的影响

5.Dalvik下完整DEX方案的新探索

参考

阿里Sophix《深入探索Android热修复技术原理》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章