第十五板块:Android 系统调试与逆向工程 | 第三十五篇:ART 虚拟机内部机制与 OAT 文件格式

第十五板块:Android 系统调试与逆向工程 | 第三十五篇:ART 虚拟机内部机制与 OAT 文件格式

所属板块:第十五板块 --- Android 系统调试与逆向工程

前置知识:第十四板块中的 HWC/HDCP 硬件交互、Linux ELF 文件格式、内存映射(mmap)、JIT 编译原理

本篇定位 :这是 Android 应用从字节码到机器码的炼金炉 。如果说 Kernel 是地基,那么 ART (Android Runtime) 就是 承载 Java/Kotlin 世界的承重墙 。本篇将彻底拆解 ART 的架构演进(Dalvik -> ART)AOT (Ahead-of-Time) 编译与 Profile-guided JIT 的混合模式OAT 文件的 ELF 化结构Dex 到 Native Code 的翻译过程 。我们将深入 libart.sodex2oat 编译器OAT 文件头,揭示 Android 如何在安装时和运行时平衡启动速度与运行性能。全程无 代码混淆教程、无逆向实操指南,仅保留 ART 虚拟机与文件格式的底层定义与编译规范。


1. 核心结论先行(Thesis Statement)

ART 是一个基于 AOT 的混合运行时环境

  • ART 的本质多层次的编译执行引擎 。它摒弃了 Dalvik 的纯解释执行,采用 AOT 预编译 (安装时生成机器码)为主,JIT 即时编译(运行时优化热点代码)为辅的策略。
  • OAT 文件的本质Android 特有的 ELF 可执行文件 。它不是一个简单的容器,而是一个标准的 ELF 文件,内部包含 OAT HeaderDex 文件镜像编译生成的 Native 机器码。这使得 OAT 文件可以直接被 Linux 内核加载执行。
  • Profile-guided Compilation 的本质基于统计学的热身。系统记录应用运行时的热点函数(Hot Methods),在下一次安装或系统空闲时,仅对这些函数进行深度优化编译,从而在冷启动速度和运行性能之间取得平衡。
  • Image Space 的本质预初始化的堆镜像 。ART 在系统启动时将核心 Framework 的 OAT 代码和堆数据映射到内存中,应用进程通过 COW (Copy-On-Write) 直接共享这些数据,实现"秒开"。

2. ART 架构全景图

2.1 从 APK 到机器码执行

#mermaid-svg-fWAiYB9xFKEolNLn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fWAiYB9xFKEolNLn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fWAiYB9xFKEolNLn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fWAiYB9xFKEolNLn .error-icon{fill:#552222;}#mermaid-svg-fWAiYB9xFKEolNLn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fWAiYB9xFKEolNLn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fWAiYB9xFKEolNLn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fWAiYB9xFKEolNLn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fWAiYB9xFKEolNLn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fWAiYB9xFKEolNLn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fWAiYB9xFKEolNLn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fWAiYB9xFKEolNLn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fWAiYB9xFKEolNLn .marker.cross{stroke:#333333;}#mermaid-svg-fWAiYB9xFKEolNLn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fWAiYB9xFKEolNLn p{margin:0;}#mermaid-svg-fWAiYB9xFKEolNLn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fWAiYB9xFKEolNLn .cluster-label text{fill:#333;}#mermaid-svg-fWAiYB9xFKEolNLn .cluster-label span{color:#333;}#mermaid-svg-fWAiYB9xFKEolNLn .cluster-label span p{background-color:transparent;}#mermaid-svg-fWAiYB9xFKEolNLn .label text,#mermaid-svg-fWAiYB9xFKEolNLn span{fill:#333;color:#333;}#mermaid-svg-fWAiYB9xFKEolNLn .node rect,#mermaid-svg-fWAiYB9xFKEolNLn .node circle,#mermaid-svg-fWAiYB9xFKEolNLn .node ellipse,#mermaid-svg-fWAiYB9xFKEolNLn .node polygon,#mermaid-svg-fWAiYB9xFKEolNLn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fWAiYB9xFKEolNLn .rough-node .label text,#mermaid-svg-fWAiYB9xFKEolNLn .node .label text,#mermaid-svg-fWAiYB9xFKEolNLn .image-shape .label,#mermaid-svg-fWAiYB9xFKEolNLn .icon-shape .label{text-anchor:middle;}#mermaid-svg-fWAiYB9xFKEolNLn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fWAiYB9xFKEolNLn .rough-node .label,#mermaid-svg-fWAiYB9xFKEolNLn .node .label,#mermaid-svg-fWAiYB9xFKEolNLn .image-shape .label,#mermaid-svg-fWAiYB9xFKEolNLn .icon-shape .label{text-align:center;}#mermaid-svg-fWAiYB9xFKEolNLn .node.clickable{cursor:pointer;}#mermaid-svg-fWAiYB9xFKEolNLn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fWAiYB9xFKEolNLn .arrowheadPath{fill:#333333;}#mermaid-svg-fWAiYB9xFKEolNLn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fWAiYB9xFKEolNLn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fWAiYB9xFKEolNLn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fWAiYB9xFKEolNLn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fWAiYB9xFKEolNLn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fWAiYB9xFKEolNLn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fWAiYB9xFKEolNLn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fWAiYB9xFKEolNLn .cluster text{fill:#333;}#mermaid-svg-fWAiYB9xFKEolNLn .cluster span{color:#333;}#mermaid-svg-fWAiYB9xFKEolNLn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fWAiYB9xFKEolNLn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fWAiYB9xFKEolNLn rect.text{fill:none;stroke-width:0;}#mermaid-svg-fWAiYB9xFKEolNLn .icon-shape,#mermaid-svg-fWAiYB9xFKEolNLn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fWAiYB9xFKEolNLn .icon-shape p,#mermaid-svg-fWAiYB9xFKEolNLn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fWAiYB9xFKEolNLn .icon-shape .label rect,#mermaid-svg-fWAiYB9xFKEolNLn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fWAiYB9xFKEolNLn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fWAiYB9xFKEolNLn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fWAiYB9xFKEolNLn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 执行
ART 运行时
安装/首次启动
应用包 (APK)
输入
输出
共享
加载
优先执行
热点优化
兜底
classes.dex (字节码)
lib/*.so
dex2oat 编译器
base.oat (ELF)
boot.oat (系统镜像)
libart.so
GC Heap
JIT Compiler
Interpreter
AOT 机器码 (Fast)
JIT 机器码 (Optimized)
解释执行 (Slow)

2.2 核心组件职责表

组件 层级 职责 学术定义
dex2oat Compiler AOT 编译器 将 Dex 字节码翻译成本地机器码(ARM/x86),生成 OAT 文件。
libart.so Runtime 虚拟机核心 负责类加载、内存管理(GC)、线程调度、解释执行和 JIT 调用。
OAT File Format 可执行文件 基于 ELF 格式,包含代码段(.text)、数据段(.rodata)和 Dex 镜像。
Image Space Memory 预加载空间 存放 Framework 预编译代码和堆数据的只读内存区域。
Profile Saver Tool 数据分析 记录应用运行时的方法调用频率和特征,用于指导 JIT/AOT。

3. OAT 文件格式深度解析

OAT 文件是 ART 的核心产物。它本质上是 ELF + Dex + Code

3.1 OAT 文件结构布局

#mermaid-svg-icwLt8Zdf6eLuyIF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-icwLt8Zdf6eLuyIF .error-icon{fill:#552222;}#mermaid-svg-icwLt8Zdf6eLuyIF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-icwLt8Zdf6eLuyIF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-icwLt8Zdf6eLuyIF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-icwLt8Zdf6eLuyIF .marker.cross{stroke:#333333;}#mermaid-svg-icwLt8Zdf6eLuyIF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-icwLt8Zdf6eLuyIF p{margin:0;}#mermaid-svg-icwLt8Zdf6eLuyIF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-icwLt8Zdf6eLuyIF .cluster-label text{fill:#333;}#mermaid-svg-icwLt8Zdf6eLuyIF .cluster-label span{color:#333;}#mermaid-svg-icwLt8Zdf6eLuyIF .cluster-label span p{background-color:transparent;}#mermaid-svg-icwLt8Zdf6eLuyIF .label text,#mermaid-svg-icwLt8Zdf6eLuyIF span{fill:#333;color:#333;}#mermaid-svg-icwLt8Zdf6eLuyIF .node rect,#mermaid-svg-icwLt8Zdf6eLuyIF .node circle,#mermaid-svg-icwLt8Zdf6eLuyIF .node ellipse,#mermaid-svg-icwLt8Zdf6eLuyIF .node polygon,#mermaid-svg-icwLt8Zdf6eLuyIF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-icwLt8Zdf6eLuyIF .rough-node .label text,#mermaid-svg-icwLt8Zdf6eLuyIF .node .label text,#mermaid-svg-icwLt8Zdf6eLuyIF .image-shape .label,#mermaid-svg-icwLt8Zdf6eLuyIF .icon-shape .label{text-anchor:middle;}#mermaid-svg-icwLt8Zdf6eLuyIF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-icwLt8Zdf6eLuyIF .rough-node .label,#mermaid-svg-icwLt8Zdf6eLuyIF .node .label,#mermaid-svg-icwLt8Zdf6eLuyIF .image-shape .label,#mermaid-svg-icwLt8Zdf6eLuyIF .icon-shape .label{text-align:center;}#mermaid-svg-icwLt8Zdf6eLuyIF .node.clickable{cursor:pointer;}#mermaid-svg-icwLt8Zdf6eLuyIF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-icwLt8Zdf6eLuyIF .arrowheadPath{fill:#333333;}#mermaid-svg-icwLt8Zdf6eLuyIF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-icwLt8Zdf6eLuyIF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-icwLt8Zdf6eLuyIF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-icwLt8Zdf6eLuyIF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-icwLt8Zdf6eLuyIF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-icwLt8Zdf6eLuyIF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-icwLt8Zdf6eLuyIF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-icwLt8Zdf6eLuyIF .cluster text{fill:#333;}#mermaid-svg-icwLt8Zdf6eLuyIF .cluster span{color:#333;}#mermaid-svg-icwLt8Zdf6eLuyIF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-icwLt8Zdf6eLuyIF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-icwLt8Zdf6eLuyIF rect.text{fill:none;stroke-width:0;}#mermaid-svg-icwLt8Zdf6eLuyIF .icon-shape,#mermaid-svg-icwLt8Zdf6eLuyIF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-icwLt8Zdf6eLuyIF .icon-shape p,#mermaid-svg-icwLt8Zdf6eLuyIF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-icwLt8Zdf6eLuyIF .icon-shape .label rect,#mermaid-svg-icwLt8Zdf6eLuyIF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-icwLt8Zdf6eLuyIF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-icwLt8Zdf6eLuyIF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-icwLt8Zdf6eLuyIF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} base.oat (ELF Format)
.symtab Section
Symbol Table
ELF Header
Program Headers
Section Headers
.text Section
OAT Header
Compiled Native Code
.rodata Section
Dex File Mirror
VTable Array
ITable Array

3.2 OAT Header 关键字段

OATHeader 位于 .text 段的起始位置,是解析 OAT 的入口。

字段 学术定义 作用
magic "oat\n037\0" 魔数,标识 OAT 文件。
checksum uint32_t 文件校验和,用于验证完整性。
instruction_set enum ARM64, ARM32, x86_64 等。
dex_file_count uint32_t 包含的 Dex 文件数量。
executable_offset uint32_t 可执行代码段在文件中的偏移。
image_file_location_oat_checksum uint32_t 关联的 Image Space 校验和。

3.3 Dex 到 OAT 的映射关系

OAT 文件内部维护了 Dex 方法与 Native 代码的映射表。

学术定义

  • Method Index: Dex 文件中方法的索引。
  • Code Offset: 该方法对应的 Native 代码在 OAT 文件中的位置。

当应用调用 MainActivity.onCreate() 时:

  1. ART 在 Dex 中找到 onCreate 的 Method Index。
  2. 查 OAT 映射表,找到对应的 Code Offset。
  3. 直接跳转到该地址执行机器码(无需解释)。

4. AOT 与 JIT 的混合编译模式

4.1 编译策略对比

模式 触发时机 编译速度 执行速度 存储空间
AOT (dex2oat) 安装时/系统启动 最快 占用大
JIT 运行时 占用小
Interpreter 运行时 无编译 最慢

4.2 Profile-guided JIT

这是 Android 8.0 (Oreo) 引入的精髓。

  1. 首次启动:应用主要通过解释器运行,同时 JIT 开始工作。
  2. 热点识别 :JIT 记录哪些方法被频繁调用(如 onCreate, onDraw)。
  3. Profile 保存 :系统空闲时(如夜间充电),dex2oat 根据 Profile 数据,仅编译热点方法,更新 OAT 文件。
  4. 二次启动:应用再次启动,直接执行优化后的 AOT 代码,速度大幅提升。

学术定义

  • Warmup (热身): 应用从解释执行到 JIT 编译的过程。
  • Full Compilation (全量编译): 系统更新或应用重装时,对所有方法进行 AOT 编译。

5. Image Space 与 Zygote 的协同

5.1 系统启动时的预加载

第十五篇(Zygote) 中提到,Zygote 预加载了 Framework 类。在 ART 时代,这具体表现为:

  1. Boot OAT : boot.oat 包含所有 Framework 代码(android.jar)的机器码。
  2. Boot Image : boot.art 包含这些类的堆镜像(已初始化的对象、字符串常量池)。
  3. 内存映射 : Zygote 启动时,将这些文件 mmap 到内存的 Image Space。

5.2 COW 与应用启动

当应用进程从 Zygote fork 出来时:

  • 应用继承了 Image Space 的映射。
  • 由于是 COW (Copy-On-Write),应用可以直接使用 Framework 的代码和数据,无需重新加载。
  • 只有当应用修改了 Framework 的某个静态变量时,内核才会为该应用复制一份新的物理页。

这就是为什么 Android 应用能秒开的原因。


6. 关键源码深度解析

6.1 dex2oat 编译流程

cpp 复制代码
// art/dex2oat/dex2oat.cc
int Dex2Oat(int argc, char** argv) {
    // 1. 解析参数 (输入 Dex, 输出 OAT, 指令集)
    ParsedOptions options;
    options.Parse(argc, argv);

    // 2. 创建 OAT 文件
    OatWriter oat_writer(options);

    // 3. 遍历 Dex 文件中的每个类和方法
    for (const DexFile* dex_file : dex_files) {
        for (ClassAccessor accessor : dex_file->GetClasses()) {
            // 4. 编译方法
            CompileMethod(accessor.GetMethodCode(), oat_writer);
        }
    }

    // 5. 写入 ELF 文件
    oat_writer.Write();
}

6.2 ART 执行方法

cpp 复制代码
// art/runtime/mirror/method.cc
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty) {
    // 1. 检查是否有 AOT 代码
    const void* oat_code = GetOatQuickCode();
    if (oat_code != nullptr) {
        // 2. 直接跳转到 AOT 机器码
        QuickCall(self, oat_code, args, result, shorty);
    } else {
        // 3. 解释执行
        Interpreter::Interpret(self, this, args, result);
    }
}

7. 常见误区

误区 学术解释
OAT 就是 Dex 换个名字 错误。OAT 是二进制机器码,Dex 是字节码。
安装慢是因为解压 部分正确。主要原因是 dex2oat 的 AOT 编译耗时。
ART 没有解释器 错误。ART 保留了解释器,用于执行冷代码和非热点代码。
OAT 文件越大越好 错误。过大的 OAT 会占用过多存储空间,且 mmap 开销大。Profile-guided 编译解决了这个问题。

8. 本篇总结(Knowledge Closure)

关键点 纯学术定义
ART 的本质 基于 AOT 的混合运行时,平衡启动速度与运行性能。
OAT 文件的本质 包含 Dex 镜像和 Native 代码的 ELF 可执行文件。
Profile-guided Compilation 基于运行时统计数据的选择性优化编译。
Image Space 预加载的 Framework 代码与堆镜像,通过 COW 实现快速共享。
dex2oat 将字节码翻译为机器码的核心编译器。

9. 第十五板块结语

至此,第十五板块:Android 系统调试与逆向工程 的第一篇已完成。

我们从 ART 的架构演进 出发,深入 OAT 文件的 ELF 结构 ,探索 AOT 与 JIT 的混合策略 ,最终抵达 Image Space 与 Zygote 的协同

我们揭示了 Android 应用执行的核心逻辑:用 AOT 换取速度,用 JIT 换取空间,用 Image Space 换取秒开。

下一篇预告第十五板块:Android 系统调试与逆向工程 | 第三十六篇:Smali 字节码语义与 Dalvik 指令集

相关推荐
alexhilton10 小时前
Android的Agent优先时代:构建时vs运行时
android·kotlin·android jetpack
Cutecat_11 小时前
视频字幕处理工具横向:提取模式 vs 编辑模式,该如何选择
android·前端·ios·语音识别
2601_9617652912 小时前
【分享】PlayerPro媒体音乐播放器 完整专业版
android·媒体
JohnnyDeng9414 小时前
【Android】Android 包体积优化:R8/ProGuard 深度配置全攻略
android·性能优化·kotlin·jetpack
故渊at14 小时前
第九板块:Android 多媒体体系 | 第二十四篇:Camera Service 与 HAL3 成像流水线
android·camera·多媒体体系·hal3
其实防守也摸鱼17 小时前
无线网络安全--10 规避WLAN验证之挫败MAC地址限制
网络·智能路由器·php·教程·虚拟机·wlan·无线网络安全
Jinkxs18 小时前
Python基础 - 初识内置函数 Python自带的便捷工具
android·java·python
私人珍藏库18 小时前
【Android】VLLO-韩国热门手机剪辑APP
android·app·工具·软件·多功能
Cloud_Shy61819 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第六章 Item 40 - 43)
android·开发语言·人工智能·笔记·python·学习方法