我理解的热修复中的ART地址错乱问题

1. 序言

  android在5.0开始正式用art虚拟机取代了dalvik虚拟机,不同版本的art虚拟机差别很大,android N开始又引入了混合编译模式。在这里我们只针对android N之前的art版本进行分析,至于art和dalvik的区别,这里就不多说了,最大的区别是art在安装时存在aot过程,用于生成oat文件,这个oat文件既包含转化前的dex文件,又包含机器指令,所以我们的应用在运行的时候可以免去[解释]这一层而直接加载机器指令。最后说一点,art是可以通过开启解释模式进行解释执行代码的,此外,有一些情况,比如动态生成的方法等也是需要解释执行的。文末会有参考文章的链接,建议大家对我略过的内容不太清楚时,可以去参考文章中学习,毕竟侧重点是不同的。

2. 问题的引入

下面开始说重点。
 其实对于classloader热修复方案的地址错乱问题早有耳闻,最早是在腾讯的一篇文章Android_N混合编译与对热补丁影响解析看到的这个说法,但是作者后续没有进行进一步的解释。后来看了看其他文章基本上是这样解释的。假设app中有一个Test类:

public class Test {
  public String showTest1(){
    return "art address error";
  }
  public String showText(){
    return "I am an showText";
  }
}

我们在MainActivity中的OnCreate方法中去调用new Test().showText()

public class MainActivity extends Activity {

  @Override
  protected void onCreate( Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final TextView textView=(TextView) findViewById(R.id.mytv);
    textView.setText(new Test().showText());
  }

 我们反编译生成的oat文件,说明一下,我们接下来的分析过程都以android5.1的源码进行,后面会进行汇编代码的分析,x86指令集比ARM指令集阅读起来复杂的多,如果大家想分析oat文件,最好还是进行ARM的手机进行。android5.x默认情况下生成的oat文件是放在/data/dalvik-cache 目录下,如果是ARM指令集的话,是会在/data/dalvik-cache/arm下的,名字为
data@app@[package name]-1@base.apk@classes.dex(可能名字上略有差异)。使用oatdump我们可以得到反编译后的oat文件dump.txt。

oatdump --file-name=data@app@[package name]-1@base.apk@classes.dex --output=dump.txt

我们看一下结果。


image.png

text方法调用机器码.png

至于这段汇编的含义先不用管,后面会介绍。我们只需要关注标红的字眼,method@21,可以理解为showText()方法在dex中的位置。#96是该方法在ArtMethod中的偏移位置。
接下来我们给Test类添加一个showTest2()方法。

public class Test {

  public String showTest1(){
    Log.i("ljj", "showTest1: ");
    Log.i("ljj", "showTest2: ");
    Log.i("ljj", "showTest3: ");
    return "art address error";
  }

  public String showTest2(){
   return "art address error"; 
  }
  public String showText(){
    return "I am an showText";
  }
}

再次查看oat文件。


image.png

image.png

发现同样调用的showText(),方法id已经变成了method@22,偏移也变成了#100.那问题就来了。我们简单分析下:
假设Test类需要修复,我们打补丁时在Test类中增加了一个showTest2方法,导致showText的方法的偏移由#96变成了#100,而此时#96指向的是showTest2方法,那么当我们调用到showtext方法时会调用到showTest2方法,从而导致了地址错乱,这是网上大家关于地址错乱的解释,不知道读到这里大家有没有发现问题?
 我们仔细来梳理一下,看看有没有问题。首先当我们安装app时,会生成一个oat文件,我们称为host.oat。这个oat文件中有一个Test类,是待修复的,在这个oat文件中,showText()的方法是method@21,调用处的偏移是#96。接着我们下发补丁,通过DexClassLoader加载的patch.dex,在动态加载的过程中,patch.dex也会生成一个oat文件,这里我们为了区分,称为patch.oat。在patch.oat中方法的编号显然是和host.oat没有一点关系的。而我们加载Test类时,毫无疑问加载到的是patch.oat中的Test类,否则热修复就不会成功了,那么怎么可能存在#100的问题,这种说法明显是在同一个dex中进行调用考虑的,和热修复(不考虑全量合成)是不符合的,因为热修复生效的时候,运行起来是跨oat或者说是跨dex的。如果真是按照机器码执行时写入的地址进行跨dex调用,感觉很容易跑飞啊,即使我不插入showTest2方法,两个oat文件的类和方法的偏移都不是同一个基准的,按说机器码应该都不能找到patch.dex中的showText方法。到底怎么回事呢?

3. 探索答案

我们分以下几个步骤来探索。

  • art下调用者是怎么通过机器码找到我们patch中的方法的(art下跨dex方法调用的实现)
  • art下像上文中增加方法,真的会地址错乱吗?

我们先来看一下第一个问题,这里说明一下,因为对汇编等底层的知识不太了解,以下的解释可能有些地方表述不到位,甚至可能有错误,如果有问题,希望大家一起探讨学习。

3.1 如何找到的Test类(跨dex查找类)

我们首先看MainActivity的调用处

  textView.setText(new Test().showText());
0x000020f8: f8d9e124    ldr.w   lr, [r9, #292]  ; pAllocObject
      0x000020fc: 1c29      mov     r1, r5
      0x000020fe: 2010      movs    r0, #16
      0x00002100: 47f0      blx     lr
      suspend point dex PC: 0x0003
      GC map objects:  v1 (r7), v2 (r8)
      0x00002102: 1c06      mov     r6, r0
      0x00002104: 1c28      mov     r0, r5
      0x00002106: 68c0      ldr     r0, [r0, #12]
      0x00002108: 1c31      mov     r1, r6
      0x0000210a: 6d80      ldr     r0, [r0, #88]
      0x0000210c: f8d0e02c  ldr.w   lr, [r0, #44]
      0x00002110: 47f0      blx     lr
      suspend point dex PC: 0x0005
      GC map objects:  v0 (r6), v1 (r7), v2 (r8)
      0x00002112: 1c28      mov     r0, r5
      0x00002114: 68c0      ldr     r0, [r0, #12]
      0x00002116: 1c31      mov     r1, r6
      0x00002118: 6e40      ldr     r0, [r0, #100]
      0x0000211a: f8d0e02c  ldr.w   lr, [r0, #44]
      0x0000211e: 47f0      blx     lr
      suspend point dex PC: 0x0008

省略了无关的机器码,这段汇编主要是new Test().showText()相关的代码。这里只针对关键的指令进行分析,把握住我们的主线,如果扩展开来,一篇文章肯定是写不下的,大家可以参考文末老罗的相关文章,我也是参照老罗的文章来查看的。
首先看第一行,pAllocObject很明显是在创建对象,在art虚拟机启动进行初始化的过程中,会注册很多的Trampoline,称为跳转表,pAllocObject就是其中的一个,Trampoline指向的是一段汇编代码,在汇编中会去执行相关的方法调用。
代码位置:art/runtime/thread.cc

void InitEntryPoints(InterpreterEntryPoints* ipoints, JniEntryPoints* jpoints,
                     PortableEntryPoints* ppoints, QuickEntryPoints* qpoints) {
    // Interpreter
    ipoints->pInterpreterToInterpreterBridge = artInterpreterToInterpreterBridge;
    ipoints->pInterpreterToCompiledCodeBridge = artInterpreterToCompiledCodeBridge;

    // JNI
    jpoints->pDlsymLookup = art_jni_dlsym_lookup_stub;

    // Portable
    ppoints->pPortableResolutionTrampoline = art_portable_resolution_trampoline;
    ppoints->pPortableToInterpreterBridge = art_portable_to_interpreter_bridge;

    // Alloc
    qpoints->pAllocArray = art_quick_alloc_array;
    qpoints->pAllocArrayWithAccessCheck = art_quick_alloc_array_with_access_check;
    qpoints->pAllocObject = art_quick_alloc_object;
    qpoints->pAllocObjectWithAccessCheck = art_quick_alloc_object_with_access_check;
    qpoints->pCheckAndAllocArray = art_quick_check_and_alloc_array;
    qpoints->pCheckAndAllocArrayWithAccessCheck = art_quick_check_and_alloc_array_with_access_check;
 ....
};

位置:art/runtime/arch/arm/quick_entrypoints_arm.S

    /* 
     * Called by managed code to allocate an object 
     */  
    .extern artAllocObjectFromCode  
ENTRY art_quick_alloc_object  
    SETUP_REF_ONLY_CALLEE_SAVE_FRAME  @ save callee saves in case of GC  
    mov    r2, r9                     @ pass Thread::Current  
    mov    r3, sp                     @ pass SP  
    bl     artAllocObjectFromCode     @ (uint32_t type_idx, Method* method, Thread*, SP)  
    RESTORE_REF_ONLY_CALLEE_SAVE_FRAME  
    RETURN_IF_RESULT_IS_NON_ZERO  
    DELIVER_PENDING_EXCEPTION  
END art_quick_alloc_object  

接着会调用artAllocObjectFromCode方法来创建对象。这里需要知道,每个dex都会生成一个DexCache,会缓存对应dex中加载解析过的所有类和方法,每个类会被分配一个typeId,每个方法会被分配一个methodId。这个方法首先会根据typeId在调用者的DexCache中查看该类是否加载过,如果没加载过,则会调用ClassLinker的ResolveType方法进行类的查找解析。
代码位置:art/runtime/class_linker.cc

mirror::Class* ClassLinker::ResolveType(const DexFile& dex_file, uint16_t type_idx,
                                       mirror::Class* referrer) {
 StackHandleScope<2> hs(Thread::Current());
 Handle<mirror::DexCache> dex_cache(hs.NewHandle(referrer->GetDexCache()));
 Handle<mirror::ClassLoader> class_loader(hs.NewHandle(referrer->GetClassLoader()));
 return ResolveType(dex_file, type_idx, dex_cache, class_loader);
}

mirror::Class* ClassLinker::ResolveType(const DexFile& dex_file, uint16_t type_idx,
                                       Handle<mirror::DexCache> dex_cache,
                                       Handle<mirror::ClassLoader> class_loader) {
 DCHECK(dex_cache.Get() != nullptr);
 mirror::Class* resolved = dex_cache->GetResolvedType(type_idx);
 if (resolved == nullptr) {
   Thread* self = Thread::Current();
   const char* descriptor = dex_file.StringByTypeIdx(type_idx);
   resolved = FindClass(self, descriptor, class_loader);
   if (resolved != nullptr) {
     dex_cache->SetResolvedType(type_idx, resolved);
   } else {
     CHECK(self->IsExceptionPending())
         << "Expected pending exception for failed resolution of: " << descriptor;
     // Convert a ClassNotFoundException to a NoClassDefFoundError.
     StackHandleScope<1> hs(self);
     Handle<mirror::Throwable> cause(hs.NewHandle(self->GetException(nullptr)));
     if (cause->InstanceOf(GetClassRoot(kJavaLangClassNotFoundException))) {
       DCHECK(resolved == nullptr);  // No Handle needed to preserve resolved.
       self->ClearException();
       ThrowNoClassDefFoundError("Failed resolution of: %s", descriptor);
       self->GetException(nullptr)->SetCause(cause.Get());
     }
   }
 }
 DCHECK((resolved == nullptr) || resolved->IsResolved() || resolved->IsErroneous())
         << PrettyDescriptor(resolved) << " " << resolved->GetStatus();
 return resolved;
}

我们针对这段代码进行详细分析,首先dex_cache是通过referrer->GetDexCache()拿到的,这个referrer是caller,也就是调用者所在的类,在我们的例子中就是指的MainActivity,所以这个dex_cache我们可以认为是主dex的cache。dex_file相当于是主dex。在主dex的cache中根据type_id去查找,因为是首次加载,所以

 mirror::Class* resolved = dex_cache->GetResolvedType(type_idx);

返回nullptr。接着会从主dex中根据type_id拿到类的名字,继续调用FindClass去class_loader中查找类,这块就涉及到类的加载机制了,我们就不多说了,会优先找到我们patch.dex中的Test类。类加载及方法调用的调用链为:

FindClass->FindInClassPath->DefineClass->LoadClass->LoadClassMembers->LoadMethod->LinkCode

加载的每个类对应一个oatClass,class的每个field会用一个ArtField表示,每个method都会对应一个ArtMethod对象。loadMethod方法会对创建的ArtMethod进行赋值。这里我们只需要知道ArtMethod的dex_cache_resolved_methods_数组指向的是所在class对应的DexCache中被resolved了的方法。在这里也就是在patch.dex中被resolved的方法,和主dexCache没任何关系。


image.png

LinkCode过程会对ArtMethod的执行入口进行设置,是compiled_code方式还是interpreter解释执行,不想扯的太远。回到ResolveType方法中,注意这一句很关键,当我们通过FindClass方法找到了对应的class后,此时的dex_cache是主dex的,也就是从patch.dex中拿到了class后,同时填充到了主dex的dexcache中对应的位置上了。

  dex_cache->SetResolvedType(type_idx, resolved);

好了至此我们分析完了pAllocObject的过程,完成了Test类的加载解析。到此我们了解了,这段汇编执行过后是通过类的签名在patch.dex中拿到的Test类,同时缓存到了自己的dexCache中。

0x000020f8: f8d9e124    ldr.w   lr, [r9, #292]  ; pAllocObject
      0x000020fc: 1c29      mov     r1, r5
      0x000020fe: 2010      movs    r0, #16
      0x00002100: 47f0      blx     lr
3.2 如何找到的showText方法(跨dex查找类方法)

接下来更关键,我们已经找到了Test类,我们如何查找showText方法呢?

 0x0003: new-instance v0, com.ljj.fixtest.Test // type@16
 0x0005: invoke-direct {v0}, void com.ljj.fixtest.Test.<init>() // method@19
 0x0008: invoke-virtual {v0}, java.lang.String com.ljj.fixtest.Test.showText() // method@22
0x000020f8: f8d9e124    ldr.w   lr, [r9, #292]  ; pAllocObject
      0x000020fc: 1c29      mov     r1, r5
      0x000020fe: 2010      movs    r0, #16
      0x00002100: 47f0      blx     lr
      suspend point dex PC: 0x0003
      GC map objects:  v1 (r7), v2 (r8)
      0x00002102: 1c06      mov     r6, r0
      0x00002104: 1c28      mov     r0, r5
      0x00002106: 68c0      ldr     r0, [r0, #12]
      0x00002108: 1c31      mov     r1, r6
      0x0000210a: 6d80      ldr     r0, [r0, #88]
      0x0000210c: f8d0e02c  ldr.w   lr, [r0, #44]
      0x00002110: 47f0      blx     lr
      suspend point dex PC: 0x0005
      GC map objects:  v0 (r6), v1 (r7), v2 (r8)
      0x00002112: 1c28      mov     r0, r5
      0x00002114: 68c0      ldr     r0, [r0, #12]
      0x00002116: 1c31      mov     r1, r6
      0x00002118: 6e40      ldr     r0, [r0, #100]
      0x0000211a: f8d0e02c  ldr.w   lr, [r0, #44]
      0x0000211e: 47f0      blx     lr
      suspend point dex PC: 0x0008

我们直接来分析最后一段汇编代码,也就是对应的showText调用部分。r

0x00002112: 1c28        mov     r0, r5

5寄存器一开始是由r0寄存器赋值过来的,而调用时r0指向的就是调用者的ArtMethod地址,也就是MainActivity 的onCreate方法对应的ArtMethod。所以下面代码就是将r0指向调用者的ArtMethod。

 0x00002114: 68c0      ldr     r0, [r0, #12]

这个#12是什么意思呢?在/art/runtime/asm_support.h中定义了ArtMethod的相关结构地址跳转的常量。不同版本这个值是不一样的,android 5.1对应的是12,6.0对应的4。那么这条指令的意思就是将r0指向ArtMethod的dex_cache_resolved_methods_位置。

#define METHOD_DEX_CACHE_METHODS_OFFSET 12

r6寄存器的值是由 movs r0, #16赋值给r0寄存器,然后由r0赋值给r6的,Test类的type是16,这行的意思就是将this参数赋值给r1。

 0x00002116: 1c31      mov     r1, r6

这行不太好分析。首先要知道此时r0指向的artMethod的dex_cache_resolved_methods_,那么#100是什么呢,通过不断的观察,发现每个方法调用都是从dex_cache_resolved_methods_的第三个位置开始计算的,阿里的深入探索android热修复技术中写到查找都是从数组的0x2开始的,这点我比较疑惑,我编译出来的机器码都是从0x3开始的,可能不同的版本不一样吧,我在源码中也没有找到相关的定义。showText的methodId是22,这里的oat文件是在32位的机器上编译的,每个指针占4个字节,(22+3)*4=100。所以#100指向的就是showText所对应的ArtMethod。

 0x00002118: 6e40      ldr     r0, [r0, #100]

44是什么意思呢?这个同样是是在/art/runtime/asm_support.h中定义的。对应于ArtMethod的entry_point_from_quick_compiled_code_字段。

#define METHOD_QUICK_CODE_OFFSET_32 44

这行代码的意思就是找到showText的机器码执行入口。准备执行。

0x0000211a: f8d0e02c  ldr.w   lr, [r0, #44]

好了,到此我们分析完了这段汇编代码的含义,可能有人会有疑问,#100上放的是showText对应的ArtMethod的吗?什么时候放上去的呢?
这里大致说一下Art下方法调用的过程。

  • 当一个dex的Dex_cache被初始化的时候,resolved_methods数组里面的ArtMethod都是指向同一个名为Resolution Method,这个ArtMethod的特点是index为kDexNoIndex,表明它不代表任何的类方法。
  • 启动OAT文件的OAT头部包含有一个quick_resolution_trampoline_offset_字段。这个quick_resolution_trampoline_offset_字段指向一小段Trampoline代码。这一小段Trampoline代码的作用是找到当前线程类型为Quick的函数跳转表中的pQuickResolutionTrampoline项,并且跳到这个pQuickResolutionTrampoline项指向的函数art_quick_resolution_trampoline去执行。
  • 当方法首次加载时,如果判断出来方法的index是kDexNoIndex,则表明是一个运行时方法,就会执行蹦床函数执行去查找真正的方法,找到后会填充到DexCache中对应的ArtMethod中。下次运行时就可以直接从DexCache中找到该方法了。

蹦床函数art_quick_resolution_trampoline会调用到artQuickResolutionTrampoline,此时如果发现是Runtime方法,会触发classLinker的resolveMethod方法去查找。ok,终于绕出来了,我们去看看resolveMethod的实现:

mirror::ArtMethod* ClassLinker::ResolveMethod(const DexFile& dex_file, uint32_t method_idx,
                                              Handle<mirror::DexCache> dex_cache,
                                              Handle<mirror::ClassLoader> class_loader,
                                              Handle<mirror::ArtMethod> referrer,
                                              InvokeType type) {
  //1. 第一步
  mirror::ArtMethod* resolved = dex_cache->GetResolvedMethod(method_idx);
  if (resolved != nullptr && !resolved->IsRuntimeMethod()) {
    return resolved;
  }
 
  const DexFile::MethodId& method_id = dex_file.GetMethodId(method_idx);
  //2. 第二步
  mirror::Class* klass = ResolveType(dex_file, method_id.class_idx_, dex_cache, class_loader);
  if (klass == nullptr) {
    DCHECK(Thread::Current()->IsExceptionPending());
    return nullptr;
  }
  switch (type) {
    case kDirect:  // Fall-through.
    case kStatic:
     // 第三步
      resolved = klass->FindDirectMethod(dex_cache.Get(), method_idx);
      break;
    case kInterface:
      resolved = klass->FindInterfaceMethod(dex_cache.Get(), method_idx);
      DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
      break;
    case kSuper:  // Fall-through.
    case kVirtual:
      resolved = klass->FindVirtualMethod(dex_cache.Get(), method_idx);
      break;
    default:
      LOG(FATAL) << "Unreachable - invocation type: " << type;
  }
  if (resolved == nullptr) {
    // Search by name, which works across dex files.
   //第四步
    const char* name = dex_file.StringDataByIdx(method_id.name_idx_);
    const Signature signature = dex_file.GetMethodSignature(method_id);
    switch (type) {
      case kDirect:  // Fall-through.
      case kStatic:
        resolved = klass->FindDirectMethod(name, signature);
        break;
      case kInterface:
        resolved = klass->FindInterfaceMethod(name, signature);
        DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
        break;
      case kSuper:  // Fall-through.
      case kVirtual:
        resolved = klass->FindVirtualMethod(name, signature);
        break;
    }
  }
 ....
  }
}

按照代码中步骤标示来解释:

  • 第一步:注意此时dex_cache是主dex,先去dex_cache中去查找,很显然,查找不到,因为resolveType时将方法保存在了patch.dex的dex_cache中了,主dex_cache是找不到的。
  • 第二步:会根据method_id.class_idx去主dex_cache中查找class,很明显是可以找到的,还记得上面加载类时在resolveType时将class也放到了主dex中了。
  • 第三步:找到了class后,会在class的directMethods数组中查找,按道理是可以找到的,但是在class.cc中会进行dexCache验证,在/art/runtime/mirror/class.cc的FindDeclaredDirectMethod方法。而此时GetDexCache其实获取的是patch.dex的dexCache,而传入的dex_cache是主dex_cache,所以依然获取不到,进入第四步。
if (GetDexCache() == dex_cache) {
        .... 
}
  • 第四步我们也看到了源码中注释,“Search by name, which works across dex files.” 到此,彻底明白了,跨dex是通过name和签名来调用方法的。既然跨dex是通过name和签名来进行查找的,那么在patch.dex中增加一个showTest2()
    按道理来讲是不会找错方法的。
4.结论验证

这里先说一下结论:当我们在app中调用patch中的方法,是通过name和签名去查找的,如果patch增加方法改变了类结构,是不会出现地址错乱的

下面进行验证:在android5.1,android6.0上进行验证,发现果然没问题,随意增加方法,都能打patch成功,然而android7.0却会找错方法,继续研究一下。
dump出android7.0的oat文件查看一下:


image.png

由于android7.0引入了混合编译模式,oat文件中默认并不会生成机器码,但是进行了指令优化,invoke-virtual已经被优化成了invoke-virtual-quick指令。后面直接跟上了vtable索引号,两个指令有什么区别呢?具体源码定义在
/art/runtime/interpreter/interpreter_switch_impl.cc中,具体实现在/art/runtime/interpreter/interpreter_common.h中

  • 对于invoke-virtual指令
static inline bool DoInvoke(Thread* self, ShadowFrame& shadow_frame, const Instruction* inst,
                            uint16_t inst_data, JValue* result) {
  ArtMethod* const method = FindMethodFromCode<type, do_access_check>(
      method_idx, &receiver, &sf_method, self);
}

会进行方法的查找,FindMethodFromCode会进入resolveMethod方法去查找真正的方法。

  • invoke-virtual-quick指令
static inline bool DoInvokeVirtualQuick(Thread* self, ShadowFrame& shadow_frame,
                                        const Instruction* inst, uint16_t inst_data,
                                        JValue* result) {

  ArtMethod* const method = receiver->GetClass()->GetEmbeddedVTableEntry(vtable_idx);
 
  }
}

可以看到,quick指令是直接在class的vtable中按照index查找,那问题就来了,安装时方法的index是针对class的,即在class的vtable中是写死的,此时我下发patch时增加了showTest2方法,那么很明显showTest2会占用原来的showText的位置,从而出现地址错乱。

至此,我们彻底分析完了所有的问题,可能内容有点多,这里简单的总结下:

结论一:

art(android N之前)下本地机器码跨dex进行方法调用是通过方法的name和签名进行的,在patch中通过增加方法改变类的结构并不会导致地址错乱。需要说明两点:

  • 第一,art生成oat文件可以指定多种方式。
  • 第二,我只是针对在修复方法前增加方法等改变,注意其他情形没有研究和验证。
  • 第三,只分析了主dex跨dex访问补丁dex的情形,至于补丁中访问到主dex情形没有深入研究,但是从patch.dexde的机器码中可以看出是通过pAllocObjectWithAccessCheck等Alloc类蹦床函数来回调的。
结论二:
  • android N增加方法后导致地址错乱是因为解释执行时进行指令优化导致的,和本地机器码没什么大的关系。其实在dalvik上应该也存在此问题,只是我们禁止了进行dexopt,所以指令没有优化,从而屏蔽了该问题而已。

 PS:我只是利用热修复来进行学习,并没有深入的做热修复的相关工作,只是出于对art虚拟机下地址错乱问题的好奇而进行的,可能有很多地方解释的不到位或者有误,欢迎指正。感觉这篇文章需要点基础,如果直接看本文的话,不一定看得懂,可以参考我之前写的热修复相关的文章结合文末的参考文章来看,参考文章都是比较有价值的,但是针对热修复下art的跨dex调用问题都没有阐述的很清楚,有问题欢迎留言讨论。

参考文章:
1.老罗的Android运行时ART加载OAT文件的过程分析
2.老罗的Android运行时ART加载类和方法的过程分析
3.老罗的Android运行时ART执行类方法的过程分析
4.老罗的ART运行时为新创建对象分配内存的过程分析
5.滴滴Android热修复探索
6.蘑菇街Android热修复探索之路
7.Android中的类加载-查找和在hotpatch上的问题

目前本人在公司负责热修复相关的工作,主要是基于robust的热修复相关工作。感兴趣的同学欢迎进群交流。


image.png