编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

    • 专栏博客

      • 系统启动Zygote
      • Binder通信原理
      • Handler消息机制
      • Activity启动原理
      • 四大组件原理分析
      • AMS与组件管理
      • View绑制与渲染
      • 事件分发机制
      • Surface渲染原理
      • 自定义View设计
      • WMS窗口管理
      • PMS与APK安装
      • 虚拟机与类加载
        • 内存管理与GC
        • 线程与并发编程
        • 性能优化与监控
        • 序列化与数据存储
        • 组件化与路由设计
        • 插件化与热修复
        • NDK开发实践
        • WebView核心设计
        • ADB常见使用操作
      • 智能硬件

    • iOS开发和进阶

    • Web开发和进阶

    • Linux应用开发

    • Apps
    • Android提升进阶
    • 专栏博客
    杨充
    2026-04-14
    目录

    虚拟机与类加载

    # 13.虚拟机与类加载

    # 目录介绍

    • 01.为什么Android需要虚拟机
      • 1.1 虚拟机的必要性
      • 1.2 Android虚拟机的特殊性
    • 02.Dalvik与ART的演进
      • 2.1 Dalvik时代(Android 1.0 ~ 4.4)
      • 2.2 ART时代(Android 5.0+)
      • 2.3 Dalvik vs ART对比
    • 03.DEX字节码与Java字节码的区别
      • 3.1 指令集差异
      • 3.2 DEX指令格式
      • 3.3 文件格式差异
    • 04.ART虚拟机架构详解
      • 4.1 ART的核心组件
      • 4.2 执行引擎的三种模式
      • 4.3 方法执行入口选择
    • 05.JIT与AOT编译原理
      • 5.1 JIT编译器工作原理
      • 5.2 AOT编译与dex2oat
      • 5.3 编译产物的关系
    • 06.Profile-Guided编译
      • 6.1 Profile收集
      • 6.2 后台dex优化
      • 6.3 Cloud Profiles(Android 12+)
    • 07.类加载器体系
      • 7.1 Android类加载器层级
      • 7.2 ClassLoader源码
    • 08.类加载的双亲委派模型
      • 8.1 双亲委派流程
      • 8.2 为什么需要双亲委派
      • 8.3 打破双亲委派
    • 09.PathClassLoader与DexClassLoader
      • 9.1 DexPathList核心结构
      • 9.2 DexFile的加载
    • 10.MultiDex的原理与实现
      • 10.1 为什么需要MultiDex
      • 10.2 MultiDex在ART中的实现
      • 10.3 MultiDex Support Library(Dalvik时代)
    • 11.类校验与优化
      • 11.1 类验证的必要性
      • 11.2 类初始化
    • 12.对象内存布局与分配
      • 12.1 对象的内存布局
      • 12.2 对象分配策略
    • 13.方法调用与虚方法分派
      • 13.1 方法调用类型
      • 13.2 虚方法表(vtable)
      • 13.3 接口方法表(itable)
    • 14.ART中的内联缓存
      • 14.1 什么是内联缓存
      • 14.2 JIT中的内联缓存
    • 15.类加载在热修复中的应用
      • 15.1 基于dexElements的类替换
      • 15.2 CLASS_ISPREVERIFIED问题
      • 15.3 Instant Run与热修复对比
    • 16.总结与技术思考
      • 16.1 核心要点回顾
      • 16.2 面试高频问题
      • 16.3 学习建议

    # 01.为什么Android需要虚拟机

    # 1.1 虚拟机的必要性

    疑惑:为什么Android不直接运行原生C/C++代码,而要通过虚拟机?

    答疑:有三个核心原因:

    第一,跨平台兼容性。Android设备有ARM、x86、MIPS等不同的CPU架构。如果开发者直接编写特定架构的机器码,一个应用需要为每种架构单独编译。虚拟机提供了一个抽象层,开发者只需编写一次字节码,虚拟机负责在不同平台上执行。

    第二,安全性。虚拟机可以对字节码进行验证,确保代码不会执行非法操作(如数组越界、类型不安全的转换)。虚拟机还提供了内存管理和垃圾回收,防止内存泄漏和悬垂指针等问题。

    第三,可管理性。虚拟机可以监控应用的资源使用,在必要时进行限制和清理。这对移动设备的资源管理至关重要。

    # 1.2 Android虚拟机的特殊性

    Android没有使用标准的JVM,而是设计了专用的虚拟机,原因:

    Android虚拟机 vs 标准JVM:
    
    内存限制:
      JVM:运行在服务器/PC上,内存充裕(GB级别)
      Android:运行在移动设备上,内存受限(早期设备仅256MB)
    
    电池限制:
      JVM:不需要考虑功耗
      Android:需要极度省电,CPU频率和使用时间都受限
    
    启动速度:
      JVM:长期运行的服务,启动速度不敏感
      Android:用户期望应用秒开,启动速度至关重要
    
    存储限制:
      JVM:存储空间充裕
      Android:早期设备内部存储仅1-2GB
    
    结果:Android设计了Dalvik/ART虚拟机
      └── 基于寄存器而非栈的指令集(更少指令数)
      └── DEX格式(共享常量池,比JAR更紧凑)
      └── 针对低内存优化的GC策略
      └── Zygote fork预热启动
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # 02.Dalvik与ART的演进

    # 2.1 Dalvik时代(Android 1.0 ~ 4.4)

    Dalvik虚拟机特点:
    
    架构:基于寄存器的虚拟机
      └── 相比JVM的栈式架构,指令更少但更复杂
      └── 一条Dalvik指令 ≈ 2-3条JVM指令
    
    执行方式:解释执行 + JIT
      ├── Android 2.2之前:纯解释执行
      │   └── 逐条解释字节码,速度慢
      └── Android 2.2+:加入JIT编译器
          └── 运行时将热点方法编译为机器码
          └── 编译结果存在内存中,进程退出即丢失
    
    进程模型:
      └── 每个应用运行在独立的Dalvik VM实例中
      └── 通过Zygote fork创建,共享只读内存(类库等)
    
    缺点:
      ├── JIT每次启动都要重新编译
      ├── GC效率低(Stop-The-World时间长)
      └── 解释执行性能差
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 2.2 ART时代(Android 5.0+)

    ART虚拟机的重大改进:
    
    Android 5.0~6.0(ART初版):
      ├── 安装时AOT编译(dex2oat将全部DEX编译为机器码)
      ├── 新的GC算法(并发标记清除)
      ├── 更好的内存管理
      └── 缺点:安装时间长,占用存储大
    
    Android 7.0+(ART改进版):
      ├── 混合编译:解释执行 + JIT + AOT
      ├── Profile-guided compilation
      ├── Concurrent Copying GC(更低的暂停时间)
      └── 安装时不编译,空闲时根据Profile编译
    
    Android 10+(ART持续优化):
      ├── Generational GC(分代垃圾回收)
      ├── Bionic Allocator优化
      └── 更好的启动优化
    
    Android 12+:
      ├── Cloud Profiles
      ├── ART模块化(Mainline更新)
      └── 可通过Google Play更新ART运行时
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # 2.3 Dalvik vs ART对比

    Dalvik与ART关键对比:
    
                     Dalvik              ART
    字节码格式    DEX                 DEX
    指令集        基于寄存器           基于寄存器
    编译方式      解释+JIT            解释+JIT+AOT
    安装速度      快                  快(7.0+) / 慢(5.0)
    启动速度      慢(每次JIT)         快(有AOT代码)
    运行性能      较差                好(接近原生)
    存储占用      小                  大(有编译产物)
    GC暂停        10-50ms             <1ms(CC GC)
    内存使用      较高                较低(Compact GC)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    # 03.DEX字节码与Java字节码的区别

    # 3.1 指令集差异

    Java字节码(基于栈)vs DEX字节码(基于寄存器):
    
    计算 a = b + c 的字节码对比:
    
    Java字节码(JVM,基于栈):
      iload_1        // 将b压入操作数栈
      iload_2        // 将c压入操作数栈  
      iadd           // 弹出栈顶两个值相加,结果压回栈
      istore_0       // 弹出栈顶值存入局部变量a
      共4条指令
    
    DEX字节码(Dalvik/ART,基于寄存器):
      add-int v0, v1, v2   // 将v1+v2的结果存入v0
      共1条指令
    
    寄存器架构的优势:
      更少的指令数 → 更少的指令分发开销
      直接操作寄存器 → 减少内存访问
      缺点:指令更长(需要编码寄存器号)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    # 3.2 DEX指令格式

    DEX指令的编码格式:
    
    格式       大小    描述
    ─────────────────────────────────
    10x        1 unit  操作码,无操作数
    12x        1 unit  操作码 + 两个4bit寄存器
    11n        1 unit  操作码 + 寄存器 + 4bit立即数
    22x        2 unit  操作码 + 8bit寄存器 + 16bit寄存器
    23x        2 unit  操作码 + 3个8bit寄存器
    31i        3 unit  操作码 + 8bit寄存器 + 32bit立即数
    35c        3 unit  操作码 + 方法调用(最多5个参数)
    
    示例:invoke-virtual指令
    35c格式: B|A|op CCCC G|F|E|D
    
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(I)V
      op = 0x6e (invoke-virtual)
      参数寄存器: v0 (this), v1 (参数)
      方法引用: CCCC = method_id索引
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    # 3.3 文件格式差异

    .class文件 vs .dex文件:
    
    JAR包中的.class文件:
    ├── ClassA.class (有自己的常量池)
    ├── ClassB.class (有自己的常量池)
    └── ClassC.class (有自己的常量池)
    → 如果三个类都用了String "hello",该字符串存储3次
    
    DEX文件:
    ├── 共享字符串常量池 (所有类共用)
    ├── 共享类型表
    ├── 共享方法表
    ├── ClassA的代码
    ├── ClassB的代码
    └── ClassC的代码
    → "hello"只存储1次,三个类引用同一个索引
    
    DEX优势:
      文件更小(消除冗余)
      加载更快(一次性映射到内存)
      更适合移动设备的存储和内存限制
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 04.ART虚拟机架构详解

    # 4.1 ART的核心组件

    ART虚拟机架构:
    ┌──────────────────────────────────────────┐
    │                 ART Runtime               │
    │  ┌────────────────────────────────────┐  │
    │  │        Execution Engine             │  │
    │  │  ├── Interpreter (解释器)           │  │
    │  │  ├── JIT Compiler (即时编译器)      │  │
    │  │  └── AOT Code (预编译代码)          │  │
    │  └────────────────────────────────────┘  │
    │  ┌────────────────────────────────────┐  │
    │  │        Garbage Collector            │  │
    │  │  ├── Concurrent Copying GC (CC)    │  │
    │  │  ├── Generational Mode             │  │
    │  │  └── Compacting (内存压缩)          │  │
    │  └────────────────────────────────────┘  │
    │  ┌────────────────────────────────────┐  │
    │  │        Class Linker                 │  │
    │  │  ├── 类加载                         │  │
    │  │  ├── 类验证                         │  │
    │  │  ├── 类解析                         │  │
    │  │  └── 类初始化                       │  │
    │  └────────────────────────────────────┘  │
    │  ┌────────────────────────────────────┐  │
    │  │        Memory Management            │  │
    │  │  ├── Heap (Java堆)                 │  │
    │  │  ├── Rosalloc (小对象分配器)        │  │
    │  │  ├── DlMalloc (大对象分配器)        │  │
    │  │  └── TLAB (线程本地分配缓冲)       │  │
    │  └────────────────────────────────────┘  │
    │  ┌────────────────────────────────────┐  │
    │  │        Thread Management            │  │
    │  │  ├── Thread (ART线程)              │  │
    │  │  ├── Monitor (对象锁)              │  │
    │  │  └── SafePoint (安全点)             │  │
    │  └────────────────────────────────────┘  │
    └──────────────────────────────────────────┘
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    # 4.2 执行引擎的三种模式

    // ART执行引擎的三种执行方式
    
    // 1.解释执行
    // 逐条解读DEX字节码,最慢但最灵活
    void ExecuteInterpretedMethod(ArtMethod* method, ...) {
        const uint16_t* dex_pc = method->GetCodeItem()->insns_;
        while (true) {
            uint16_t inst = *dex_pc;
            switch (inst & 0xFF) {
                case Instruction::MOVE:
                    // 处理MOVE指令
                    break;
                case Instruction::INVOKE_VIRTUAL:
                    // 处理虚方法调用
                    break;
                // ... 处理所有指令
            }
            dex_pc += inst_size;
        }
    }
    
    // 2.JIT编译执行
    // 运行时将热点方法编译为机器码
    // JIT编译后的代码存储在JIT Code Cache中
    void JitCompile(ArtMethod* method) {
        // 构建中间表示
        HGraph* graph = BuildGraph(method);
        // 优化
        RunOptimizationPasses(graph);
        // 生成机器码
        EmitCode(graph, jit_code_cache);
    }
    
    // 3.AOT编译代码
    // 安装时由dex2oat预编译,直接执行机器码
    // 性能最好,与原生代码接近
    void ExecuteCompiledCode(ArtMethod* method) {
        // 直接跳转到编译后的机器码入口
        method->GetEntryPointFromQuickCompiledCode()();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    # 4.3 方法执行入口选择

    ART为每个方法维护入口指针,决定使用哪种执行方式:

    方法入口选择逻辑:
    
    ArtMethod
    ├── entry_point_from_quick_compiled_code_
    │   ├── 如果有AOT编译代码 → 指向OAT文件中的机器码
    │   ├── 如果有JIT编译代码 → 指向JIT Code Cache
    │   └── 如果都没有 → 指向解释器桥接函数
    │       └── art_quick_to_interpreter_bridge
    │
    └── entry_point_from_interpreter_
        └── 指向解释器入口
    
    热度计数器:
    每个方法有一个hotness_count_
    当计数达到阈值(默认10000)时:
      └── JIT编译器在后台编译该方法
      └── 编译完成后更新entry_point指向JIT代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    # 05.JIT与AOT编译原理

    # 5.1 JIT编译器工作原理

    JIT编译流程(Android 7.0+ ART):
    
    1. 热度检测
       └── 方法被调用或循环回边执行时计数+1
       └── 达到阈值 → 加入JIT编译队列
    
    2. OSR (On-Stack Replacement)
       └── 对于长时间运行的循环
       └── 无需等待方法退出,直接替换运行中的代码
    
    3. 编译过程
       ├── 构建HGraph(高层中间表示)
       │   └── 将DEX字节码转换为SSA形式的图
       ├── 优化Pass
       │   ├── Inlining (方法内联)
       │   ├── Constant Folding (常量折叠)
       │   ├── Dead Code Elimination (死代码消除)
       │   ├── Bounds Check Elimination (边界检查消除)
       │   ├── Load Store Elimination (加载存储消除)
       │   └── Register Allocation (寄存器分配)
       └── 代码生成
           └── 生成目标平台的机器码(ARM64/ARM/x86)
    
    4. 代码安装
       └── 将编译后的代码写入JIT Code Cache
       └── 更新方法的入口指针
       └── 下次调用直接执行编译后的代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    # 5.2 AOT编译与dex2oat

    dex2oat编译管线:
    
    输入:classes.dex
      │
      ├── 前端:DEX → HGraph
      │   └── 每个方法转换为HGraph(SSA中间表示)
      │
      ├── 中端:优化
      │   ├── InstructionSimplifier
      │   ├── ConstantFolding
      │   ├── InstructionCombiner
      │   ├── GVN (Global Value Numbering)
      │   ├── Inlining
      │   ├── BoundsCheckElimination
      │   ├── LoopOptimization
      │   └── SideEffectsAnalysis
      │
      ├── 后端:代码生成
      │   ├── RegisterAllocator (线性扫描/图着色)
      │   ├── InstructionScheduling
      │   └── CodeGenerator (ARM64/ARM/x86)
      │
      └── 输出:
          ├── .oat文件 (ELF格式,包含编译后的机器码)
          │   ├── oat_header (版本信息)
          │   ├── .rodata (只读数据)
          │   ├── .text (编译后的机器码)
          │   └── .bss (GC根引用)
          ├── .vdex文件 (验证后的DEX)
          │   └── 包含原始DEX + quickening信息
          └── .art文件 (预初始化的堆)
              └── 预创建的Class对象和String对象
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    # 5.3 编译产物的关系

    编译产物文件关系:
    
    base.apk (原始APK,包含classes.dex)
        │
        ├── dex2oat编译
        │
        ├── base.odex (.oat格式)
        │   └── 编译后的机器码
        │   └── 运行时ART加载执行
        │
        ├── base.vdex
        │   └── 验证后的DEX + 优化信息
        │   └── 用途1:回退到解释执行时使用
        │   └── 用途2:加速重编译
        │
        └── base.art (boot image的扩展)
            └── 预初始化的ART堆数据
            └── 包含Class对象、String常量等
            └── 进程启动时直接映射到内存
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    # 06.Profile-Guided编译

    # 6.1 Profile收集

    Android 7.0+的混合编译核心是Profile-Guided Optimization:

    Profile收集过程:
    
    应用运行时:
    ├── JIT编译器记录热点方法
    ├── 运行时记录"热"类(使用频率高的类)
    └── Profile信息写入文件
    
    Profile文件位置:
    /data/misc/profiles/cur/0/com.example.app/primary.prof
      └── 当前运行的Profile
    
    /data/misc/profiles/ref/com.example.app/primary.prof
      └── 参考Profile(用于AOT编译)
    
    Profile包含的信息:
    ├── 热点方法列表 (Hot Methods)
    │   └── 调用次数超过阈值的方法
    ├── 启动方法列表 (Startup Methods)
    │   └── 应用启动阶段调用的方法
    ├── 热类列表 (Hot Classes)
    │   └── 使用频率高的类
    └── 内联缓存数据 (Inline Caches)
        └── 虚方法调用的实际接收者类型
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # 6.2 后台dex优化

    后台dex优化流程(BackgroundDexOptService):
    
    1. 触发时机
       ├── 设备充电 + 空闲
       └── 通过JobScheduler调度
    
    2. 检查是否需要优化
       ├── 是否有新的Profile数据
       ├── Profile是否足够大(有足够的热点信息)
       └── 距离上次优化是否超过一定时间
    
    3. 执行编译
       └── dex2oat --compiler-filter=speed-profile
           ├── 只编译Profile中的热点方法
           ├── 其他方法保持DEX形式
           └── 编译结果替换旧的OAT文件
    
    4. 效果
       ├── 热点方法:AOT编译,运行最快
       ├── 非热点方法:解释执行或JIT
       └── 编译产物大小 ≈ 全量编译的20-30%
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 6.3 Cloud Profiles(Android 12+)

    Cloud Profiles工作流程:
    
    1. Google Play聚合Profile
       ├── 收集大量用户设备上的Profile数据
       ├── 聚合得到"通用"的热点方法集
       └── 上传到Play Store服务器
    
    2. 安装时下载Profile
       └── 用户安装应用时从Play Store下载Cloud Profile
    
    3. 安装时编译
       └── 使用Cloud Profile进行AOT编译
       └── 首次运行就有近乎完全编译的性能
    
    优势:
      └── 解决了"首次运行冷启动慢"的问题
      └── 不需要等待本地Profile积累
      └── 覆盖了大多数用户的热点代码路径
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 07.类加载器体系

    # 7.1 Android类加载器层级

    Android类加载器层级:
    
    BootClassLoader (引导类加载器)
    ├── 加载Android框架核心类
    ├── java.lang.*, java.util.*, android.* 等
    └── 对应boot image(/system/framework/boot-xxx.art)
        │
        ├── PathClassLoader (路径类加载器)
        │   ├── 加载已安装的APK中的类
        │   ├── 系统在创建应用进程时自动创建
        │   ├── dexPath = APK路径
        │   └── librarySearchPath = Native库路径
        │
        ├── DexClassLoader (DEX类加载器)
        │   ├── 可以加载任意路径的DEX/JAR/APK
        │   ├── 插件化框架常用
        │   └── 与PathClassLoader的区别在Android 8.0+已消失
        │
        └── InMemoryDexClassLoader (内存DEX加载器)
            ├── Android 8.0+新增
            └── 从内存中的ByteBuffer加载DEX
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 7.2 ClassLoader源码

    // BaseDexClassLoader.java - PathClassLoader和DexClassLoader的父类
    public class BaseDexClassLoader extends ClassLoader {
        // 核心:DexPathList管理所有DEX文件
        private final DexPathList pathList;
        
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, 
                    librarySearchPath, null, false);
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // 在DexPathList中查找类
            Class<?> c = pathList.findClass(name, suppressedExceptions);
            if (c == null) {
                throw new ClassNotFoundException(name);
            }
            return c;
        }
    }
    
    // PathClassLoader.java
    public class PathClassLoader extends BaseDexClassLoader {
        // Android 8.0+: optimizedDirectory参数被忽略
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }
    }
    
    // DexClassLoader.java  
    public class DexClassLoader extends BaseDexClassLoader {
        // Android 8.0+: optimizedDirectory参数被忽略
        // 实际上和PathClassLoader已经没有区别
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    # 08.类加载的双亲委派模型

    # 8.1 双亲委派流程

    // ClassLoader.java
    protected Class<?> loadClass(String name, boolean resolve) 
            throws ClassNotFoundException {
        // 1.检查是否已经加载过
        Class<?> c = findLoadedClass(name);
        
        if (c == null) {
            try {
                // 2.委派给父类加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 如果没有父加载器,使用BootClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器无法加载,不做处理
            }
            
            // 3.父加载器无法加载,自己尝试加载
            if (c == null) {
                c = findClass(name);
            }
        }
        
        return c;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    双亲委派的调用链:
    
    加载 com.example.MyActivity:
    
    PathClassLoader.loadClass("com.example.MyActivity")
      │
      ├── 1.findLoadedClass() → null (未加载过)
      │
      ├── 2.parent.loadClass() → BootClassLoader
      │     ├── findLoadedClass() → null
      │     └── findClass() → ClassNotFoundException
      │         (BootClassLoader中没有com.example开头的类)
      │
      └── 3.findClass("com.example.MyActivity")
            └── pathList.findClass()
                  └── 遍历dexElements[]
                        └── 在APK的DEX中找到该类
                              └── defineClass() → 返回Class对象
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 8.2 为什么需要双亲委派

    双亲委派的好处:
    
    1. 安全性
       └── 防止用户自定义java.lang.String等核心类
       └── 核心类始终由BootClassLoader加载
       └── 即使APK中包含同名类也不会被加载
    
    2. 避免重复加载
       └── 父加载器已加载的类不会被子加载器重复加载
       └── 减少内存使用
    
    3. 类的唯一性
       └── 同一个类被同一个ClassLoader加载才是同一个类
       └── 不同ClassLoader加载的同名类被认为是不同类
       └── 这是插件化框架中需要注意的重要问题
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    # 8.3 打破双亲委派

    在某些场景下需要打破双亲委派:

    // 自定义ClassLoader打破双亲委派
    class PluginClassLoader extends DexClassLoader {
        
        @Override
        protected Class<?> loadClass(String name, boolean resolve) 
                throws ClassNotFoundException {
            // 对于插件自己的类,不委派给父加载器
            // 直接在自己的DEX中查找
            
            // 1.已加载的类直接返回
            Class<?> c = findLoadedClass(name);
            if (c != null) return c;
            
            // 2.系统类仍然走双亲委派
            if (name.startsWith("java.") || name.startsWith("android.")) {
                return getParent().loadClass(name);
            }
            
            // 3.插件类优先从自己的DEX加载(打破双亲委派)
            try {
                c = findClass(name);
                if (c != null) return c;
            } catch (ClassNotFoundException e) {
                // 找不到,回退到父加载器
            }
            
            // 4.回退到双亲委派
            return getParent().loadClass(name);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

    # 09.PathClassLoader与DexClassLoader

    # 9.1 DexPathList核心结构

    两者的核心都是DexPathList中的dexElements数组:

    // DexPathList.java
    class DexPathList {
        // 核心数据结构:Element数组
        private Element[] dexElements;
        
        // 每个Element对应一个DEX文件
        static class Element {
            private final File path;        // 文件路径
            private final DexFile dexFile;  // DEX文件对象
            
            public Class<?> findClass(String name) {
                return dexFile != null ? dexFile.loadClassBinaryName(name, 
                        definingContext, suppressedExceptions) : null;
            }
        }
        
        // 查找类:遍历dexElements数组
        public Class<?> findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                Class<?> clazz = element.findClass(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
            // 遍历所有Element都没找到
            return null;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    dexElements数组的查找顺序:
    
    dexElements[0] → classes.dex  (优先查找)
    dexElements[1] → classes2.dex
    dexElements[2] → classes3.dex
    ...
    dexElements[N] → classesN+1.dex
    
    关键点:类在第一个匹配的DEX中被找到后就停止搜索。
    这个特性是MultiDex和热修复的基础。
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    # 9.2 DexFile的加载

    // DexFile.java
    public final class DexFile {
        // 打开DEX文件(Native实现)
        private static Object openDexFile(String sourceName, String outputName,
                int flags, ClassLoader loader, DexPathList.Element[] elements) {
            // 通过JNI调用到ART Runtime
            return openDexFileNative(sourceName, outputName, flags, loader, elements);
        }
        
        // Native层的实现
        // art/runtime/native/dalvik_system_DexFile.cc
        static jobject DexFile_openDexFileNative(JNIEnv* env, ...) {
            // 1.打开文件
            std::unique_ptr<const DexFile> dex_file = DexFileLoader::Open(
                    sourceName, ...);
            
            // 2.如果有OAT编译产物,加载OAT
            OatFile* oat_file = OatFileManager::OpenOatFile(sourceName);
            
            // 3.验证DEX
            if (!dex_file->IsVerified()) {
                VerifyDexFile(dex_file);
            }
            
            return CreateCookieFromDexFiles(env, dex_files, oat_file);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    # 10.MultiDex的原理与实现

    # 10.1 为什么需要MultiDex

    疑惑:为什么一个DEX文件不够用?

    答疑:DEX文件中方法引用使用16位索引,最多引用65536个方法。当应用引入大量第三方库后很容易超过这个限制:

    65536方法数限制:
    
    DEX文件的method_ids区域使用uint16_t索引
    最大值 = 2^16 = 65536
    
    一个中型应用的方法数分布:
    ├── Android SDK:    ~15000个方法
    ├── AppCompat:      ~12000个方法
    ├── Google Services: ~20000个方法
    ├── 其他第三方库:    ~15000个方法
    └── 应用自身代码:    ~8000个方法
    合计: ~70000 > 65536 → 溢出!
    
    解决方案:
    Android 5.0+ (ART): 原生支持MultiDex
    Android 4.4- (Dalvik): 需要MultiDex Support Library
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # 10.2 MultiDex在ART中的实现

    ART原生MultiDex支持:
    
    APK中包含多个DEX文件:
    ├── classes.dex     (主DEX)
    ├── classes2.dex    (第二个DEX)
    ├── classes3.dex    (第三个DEX)
    └── ...
    
    ART安装时处理:
    dex2oat会将所有DEX文件合并编译:
      classes.dex + classes2.dex + classes3.dex
        → 统一编译为一个OAT文件
        → base.odex
    
    运行时加载:
    PathClassLoader的DexPathList中
      dexElements[] 包含所有DEX文件的Element
      查找类时顺序遍历所有Element
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 10.3 MultiDex Support Library(Dalvik时代)

    // MultiDex.java(Support Library)
    public class MultiDex {
        public static void install(Context context) {
            // 1.获取应用的ClassLoader
            ClassLoader loader = context.getClassLoader();
            
            // 2.获取APK文件路径
            ApplicationInfo appInfo = context.getApplicationInfo();
            File sourceApk = new File(appInfo.sourceDir);
            
            // 3.提取secondary DEX文件
            File dexDir = getDexDir(context, appInfo);
            List<File> files = MultiDexExtractor.load(context, appInfo, dexDir);
            
            // 4.关键:将secondary DEX注入到dexElements数组中
            installSecondaryDexes(loader, dexDir, files);
        }
        
        private static void installSecondaryDexes(ClassLoader loader,
                File dexDir, List<File> files) {
            // 反射获取PathClassLoader的pathList字段
            Field pathListField = findField(loader, "pathList");
            Object pathList = pathListField.get(loader);
            
            // 反射获取dexElements字段
            Field dexElementsField = findField(pathList, "dexElements");
            Object[] originElements = (Object[]) dexElementsField.get(pathList);
            
            // 为secondary DEX创建新的Element
            Object[] newElements = makeDexElements(files, ...);
            
            // 合并:原有Elements + 新Elements
            Object[] combined = new Object[originElements.length + newElements.length];
            System.arraycopy(originElements, 0, combined, 0, originElements.length);
            System.arraycopy(newElements, 0, combined, originElements.length, newElements.length);
            
            // 设置回去
            dexElementsField.set(pathList, combined);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    # 11.类校验与优化

    # 11.1 类验证的必要性

    ART在加载类时会进行严格的验证,确保字节码的合法性:

    类验证检查的内容:
    
    1. 结构验证
       ├── DEX文件格式正确性
       ├── 字符串编码合法性
       └── 索引范围合法性
    
    2. 语义验证
       ├── 类继承关系合法
       ├── 方法重写签名正确
       └── 接口实现完整
    
    3. 字节码验证
       ├── 指令编码合法
       ├── 寄存器使用不越界
       ├── 类型安全(不会把int当作Object用)
       ├── 控制流合法(不会跳转到指令中间)
       └── 异常处理范围合法
    
    验证结果:
    ├── 验证通过 → 标记为verified,可以优化
    ├── 验证失败 → 标记为errored,不能加载
    └── 软失败 → 运行时再次验证
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # 11.2 类初始化

    // 类初始化的触发时机(与JVM规范一致)
    // 以下操作触发类初始化:
    // 1. new 创建类的实例
    // 2. 访问类的静态字段(非final常量)
    // 3. 调用类的静态方法
    // 4. 反射访问
    // 5. 子类初始化时先初始化父类
    
    // ART中的类状态
    enum ClassStatus {
        kNotReady,          // 未准备
        kIdx,               // 索引已解析
        kLoaded,            // 已加载
        kResolving,         // 正在解析
        kResolved,          // 已解析(字段和方法已链接)
        kVerifying,         // 正在验证
        kVerified,          // 已验证
        kInitializing,      // 正在初始化(执行<clinit>)
        kInitialized,       // 已初始化(可以使用)
        kVisiblyInitialized // 初始化对所有线程可见
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 12.对象内存布局与分配

    # 12.1 对象的内存布局

    ART中Java对象的内存布局:
    
    普通对象:
    ┌───────────────────────┐
    │  Object Header (8字节) │
    │  ├── klass_ (4字节)    │  → 指向Class对象
    │  └── monitor_ (4字节)  │  → 锁状态/HashCode/GC标记
    ├───────────────────────┤
    │  字段1 (按对齐排列)    │
    │  字段2                 │
    │  ...                   │
    │  字段N                 │
    └───────────────────────┘
    
    数组对象:
    ┌───────────────────────┐
    │  Object Header (8字节) │
    ├───────────────────────┤
    │  length (4字节)        │  → 数组长度
    ├───────────────────────┤
    │  element[0]            │
    │  element[1]            │
    │  ...                   │
    │  element[N-1]          │
    └───────────────────────┘
    
    String对象(Android 9+ Compact Strings):
    ┌───────────────────────┐
    │  Object Header (8字节) │
    ├───────────────────────┤
    │  count (4字节)         │  → 字符数
    │  hash (4字节)          │  → 缓存的HashCode
    ├───────────────────────┤
    │  字符数据               │  → 直接存储(不再是char[]引用)
    └───────────────────────┘
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    # 12.2 对象分配策略

    ART的对象分配策略:
    
    小对象 (< 128KB):
    ├── TLAB分配 (Thread Local Allocation Buffer)
    │   └── 每个线程有私有的分配缓冲区
    │   └── 无锁分配,速度极快(几条指令)
    │   └── TLAB用完后从RegionSpace分配新的
    │
    ├── Rosalloc分配器(旧版)
    │   └── 按大小分桶(16B/24B/32B/...)
    │   └── 每个桶有线程本地缓存
    │   └── 类似TCMalloc的设计
    │
    └── BumpPointer分配
        └── 简单地移动分配指针
        └── 在CC GC的to-space中使用
    
    大对象 (>= 128KB):
    └── Large Object Space
        └── 直接mmap分配
        └── 独立管理,不参与Compact
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 伪代码:TLAB分配流程
    Object allocate(int size) {
        // 1.尝试TLAB分配(最快路径)
        Thread* self = Thread::Current();
        byte* top = self->tlabTop;
        byte* end = self->tlabEnd;
        
        if (top + size <= end) {
            self->tlabTop = top + size;
            return (Object*)top;  // 成功,几条指令完成
        }
        
        // 2.TLAB空间不足,申请新的TLAB
        if (AllocateNewTLAB(self, size)) {
            return AllocateInTLAB(self, size);
        }
        
        // 3.无法分配新TLAB,触发GC
        CollectGarbage();
        return RetryAllocate(size);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 13.方法调用与虚方法分派

    # 13.1 方法调用类型

    DEX中的五种方法调用指令:
    
    invoke-virtual:虚方法调用(最常见)
      └── 需要运行时根据对象实际类型查找方法
      └── 通过vtable(虚方法表)分派
    
    invoke-interface:接口方法调用
      └── 比虚方法调用更慢
      └── 通过itable(接口方法表)分派
    
    invoke-direct:直接调用
      └── 构造方法、private方法
      └── 编译时就确定目标,无需查表
    
    invoke-static:静态方法调用
      └── 静态方法,无需this引用
      └── 编译时确定目标
    
    invoke-super:调用父类方法
      └── super.xxx()
      └── 编译时确定为父类的vtable条目
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 13.2 虚方法表(vtable)

    vtable的结构和查找过程:
    
    class Animal {
        void eat() { ... }      // vtable[0]
        void sleep() { ... }    // vtable[1]
    }
    
    class Dog extends Animal {
        void eat() { ... }      // 重写,vtable[0]指向Dog.eat
        void bark() { ... }     // 新方法,vtable[2]
    }
    
    Dog对象的vtable:
    ┌──────────────────┐
    │ [0] Dog.eat()    │  ← 重写了Animal.eat
    │ [1] Animal.sleep │  ← 继承,未重写
    │ [2] Dog.bark()   │  ← 新增方法
    └──────────────────┘
    
    虚方法调用过程:
    animal.eat()  // animal实际类型是Dog
      1. 获取对象的Class指针
      2. 从Class中获取vtable
      3. 用方法的vtable索引(0)查找
      4. 调用vtable[0] → Dog.eat()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    # 13.3 接口方法表(itable)

    itable的查找比vtable多一次遍历:
    
    interface Runnable { void run(); }  
    interface Comparable { int compareTo(Object o); }
    
    class MyClass implements Runnable, Comparable {
        void run() { ... }
        int compareTo(Object o) { ... }
    }
    
    MyClass的itable:
    ┌────────────────────────────────────┐
    │ Interface  │ Offset │ Methods      │
    ├────────────────────────────────────┤
    │ Runnable   │   0    │ run()        │
    │ Comparable │   1    │ compareTo()  │
    └────────────────────────────────────┘
    
    接口方法调用过程:
    Runnable r = new MyClass();
    r.run();
      1. 获取对象的Class
      2. 在itable中查找Runnable接口
      3. 获取run()方法的偏移量
      4. 调用对应的实现方法
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    # 14.ART中的内联缓存

    # 14.1 什么是内联缓存

    虚方法调用每次都要查vtable/itable,开销不小。内联缓存是一种优化,记录上次调用的实际类型:

    内联缓存原理:
    
    未优化的虚方法调用:
      animal.eat()
        → 每次查vtable → 调用方法
    
    单态内联缓存(Monomorphic):
      记录:上次调用时animal的类型是Dog
      if (animal.class == Dog) {
          直接调用Dog.eat()  ← 命中缓存,无需查表
      } else {
          查vtable,更新缓存
      }
    
    多态内联缓存(Polymorphic):
      记录最近N个不同类型
      if (animal.class == Dog)     → Dog.eat()
      elif (animal.class == Cat)   → Cat.eat()
      elif (animal.class == Bird)  → Bird.eat()
      else → 查vtable
    
    超多态(Megamorphic):
      类型太多,退化为普通vtable查找
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # 14.2 JIT中的内联缓存

    JIT编译器利用内联缓存进行去虚拟化:
    
    // 原始代码
    void feedAnimal(Animal animal) {
        animal.eat();  // 虚方法调用
    }
    
    // 如果Profile显示animal总是Dog类型
    // JIT编译为:
    void feedAnimal(Animal animal) {
        if (animal instanceof Dog) {  // 类型守卫
            // 去虚拟化:直接调用Dog.eat()
            // 可能进一步内联Dog.eat()的代码
            ((Dog)animal).inlinedEat();
        } else {
            // 慢路径:走vtable
            animal.eat();
        }
    }
    
    优化效果:
      消除了vtable查找开销
      方法内联后可以进一步优化(常量传播、死代码消除等)
      对于单态调用点,性能提升显著
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # 15.类加载在热修复中的应用

    # 15.1 基于dexElements的类替换

    热修复的核心思路是利用dexElements的顺序查找特性:

    热修复原理(dexElements插入法):
    
    修复前的dexElements:
    [Element(classes.dex)]  → 包含BugClass(有Bug)
    
    修复操作:
    将包含修复后BugClass的patch.dex插入到数组前面
    
    修复后的dexElements:
    [Element(patch.dex), Element(classes.dex)]
         ↑ 包含修复后的BugClass   ↑ 包含有Bug的BugClass
    
    类加载时:
    loadClass("BugClass")
      → 遍历dexElements
      → 先在patch.dex中找到BugClass → 返回修复后的版本
      → classes.dex中的旧版本永远不会被加载
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 热修复:注入patch dex到dexElements最前面
    void hotFix(Context context, File patchDex) {
        // 1.获取应用的ClassLoader
        PathClassLoader classLoader = (PathClassLoader) context.getClassLoader();
        
        // 2.反射获取pathList
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader);
        
        // 3.反射获取dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        Object[] originElements = (Object[]) dexElementsField.get(pathList);
        
        // 4.为patch dex创建新的Element
        Object[] patchElements = makeDexElements(patchDex);
        
        // 5.关键:patch放在前面
        Object[] combined = new Object[patchElements.length + originElements.length];
        System.arraycopy(patchElements, 0, combined, 0, patchElements.length);
        System.arraycopy(originElements, 0, combined, patchElements.length, 
                originElements.length);
        
        // 6.设置回去
        dexElementsField.set(pathList, combined);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    # 15.2 CLASS_ISPREVERIFIED问题

    在Dalvik虚拟机上,热修复会遇到CLASS_ISPREVERIFIED问题:

    CLASS_ISPREVERIFIED问题:
    
    Dalvik在安装时会对DEX进行验证优化(dexopt):
    如果一个类的所有引用都在同一个DEX文件中
      → 该类被标记为CLASS_ISPREVERIFIED
    
    运行时:
    如果被标记的类引用了其他DEX文件中的类
      → 抛出 IllegalAccessError
    
    热修复的影响:
    BugClass在classes.dex中被标记为ISPREVERIFIED
    patch.dex中的修复类引用了classes.dex中的其他类
      → 报错!
    
    解决方案(QQ空间方案):
    在每个类的构造方法中插入一行代码:
      System.out.println(AntiLazyLoad.class);
    AntiLazyLoad.class放在独立的hack.dex中
    → 所有类都引用了不同DEX中的类
    → 不会被标记为ISPREVERIFIED
    
    ART不存在此问题,因为ART没有CLASS_ISPREVERIFIED机制。
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # 15.3 Instant Run与热修复对比

    热修复方案对比:
    
    方案              原理                      重启要求    粒度
    ─────────────────────────────────────────────────────────────
    dexElements插入    在ClassLoader中注入新DEX   冷启动     类级别
    Tinker            合成全量DEX替换             冷启动     类级别
    AndFix            Native层替换ArtMethod      不需要     方法级别
    Robust            字节码插桩+方法路由         不需要     方法级别
    Instant Run       拆分多个split APK          温启动     类/资源
    
    各方案优缺点:
    AndFix:不需要重启,但兼容性差(ART不同版本ArtMethod结构不同)
    Tinker:稳定可靠,但需要冷启动
    Robust:兼容性好,但包体积增加(每个方法都插入了路由逻辑)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    # 16.总结与技术思考

    # 16.1 核心要点回顾

    Android虚拟机与类加载核心要点:
    
    1. ART是Android当前的运行时
       └── 混合编译:解释执行 + JIT + AOT
       └── Profile-guided编译是最佳实践
    
    2. DEX是Android专用的字节码格式
       └── 基于寄存器的指令集
       └── 共享常量池,比JAR更紧凑
    
    3. 类加载采用双亲委派模型
       └── BootClassLoader → PathClassLoader
       └── DexPathList.dexElements[]是核心数据结构
    
    4. dexElements的顺序决定类加载优先级
       └── 这是MultiDex和热修复的基础
    
    5. ART的执行方式决定性能
       └── AOT代码 > JIT编译 > 解释执行
       └── 内联缓存优化虚方法调用
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    # 16.2 面试高频问题

    问题:PathClassLoader和DexClassLoader的区别?

    • Android 8.0之前,DexClassLoader可以指定optimizedDirectory,PathClassLoader不行
    • Android 8.0+两者已无本质区别,optimizedDirectory参数被忽略

    问题:MultiDex的原理?

    • 将多个DEX文件打包到APK中
    • 运行时通过反射将所有DEX的Element添加到dexElements数组
    • ART原生支持,Dalvik需要Support Library

    问题:热修复为什么把patch.dex放在前面?

    • dexElements按顺序查找类
    • 放在前面的DEX优先被搜索到
    • 修复后的类会覆盖有Bug的类

    问题:ART的混合编译是怎么工作的?

    • 安装时只验证DEX,不编译
    • 运行时使用解释器+JIT
    • JIT编译热点代码并记录Profile
    • 空闲时根据Profile进行选择性AOT编译

    # 16.3 学习建议

    理解虚拟机和类加载需要分层学习:

    1. 应用层:理解ClassLoader的使用、双亲委派、热修复原理
    2. 框架层:理解DexPathList、DexFile的实现
    3. 运行时层:理解ART的编译策略、GC机制、对象模型
    4. 编译器层:理解dex2oat的优化Pipeline

    建议从ClassLoader出发,先理解类如何被加载,再深入理解DEX如何被编译和执行。

    上次更新: 2026/06/10, 11:13:41
    PMS与APK安装
    内存管理与GC

    ← PMS与APK安装 内存管理与GC→

    最近更新
    01
    信号崩溃快速排查
    06-15
    02
    CoreDump破案
    06-15
    03
    perf火焰图实战
    06-15
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式