逆向--Android DEX 文件格式与 Smali 语言

Android DEX 文件格式与 Smali 语言

最近在探索逆向相关的知识, 总结为一个笔记. 如有大佬路过, 可一起交流.

一、Android 虚拟机概述

1.1 发展历程

阶段 虚拟机 说明
Android 4.4 之前 Dalvik VM 解释执行 + JIT (Just-In-Time) 编译
Android 5.0 - 6.0 ART (预置) 预编译 (AOT, Ahead-Of-Time),安装时编译为机器码
Android 7.0+ ART (混合模式) JIT + AOT 混合,Profile Guided 编译
Android 8.0+ ART (改进) 更优的 Profile 收集与编译策略

1.2 Dalvik vs ART 核心差异

特性 Dalvik ART
执行方式 JIT 即时编译 + 解释 AOT 预编译 + JIT + 解释
安装速度 慢(AOT 模式)
首次启动 快(边解释边编译) 快(已编译为机器码)
占用空间 大(存有机器码)
运行时性能 一般
GC 方式 单代,标记-清除为主 并发 GC + 压缩 GC

1.3 Java 字节码 vs Dalvik 字节码

对比项 Java (JVM) Dalvik/ART (Android)
字节码文件 .class (每个类一个文件) .dex (所有类合并到一个文件)
指令集 基于栈 (Stack-based) 基于寄存器 (Register-based)
指令长度 1 字节 变长 (2/4/6 字节等)
常量池 每个 class 独立 所有类共享常量池
设计目标 跨平台 移动端、低内存

为什么 Android 使用基于寄存器的指令集?

基于栈的字节码(JVM)指令短但指令条数多,解释执行时有大量栈操作开销;

基于寄存器的字节码(Dalvik)指令较长但指令条数少,减少了解释器的分派与栈操作开销,更适合 CPU 资源有限的移动设备。


二、DEX 文件格式详解

2.1 DEX 文件整体结构

复制代码
+---------------------------+
|       DEX Header          |  ← 文件头(固定 0x70 字节)
+---------------------------+
|     String Pool           |  ← 字符串常量池
+---------------------------+
|      Type Pool            |  ← 类型(类名/方法签名)索引池
+---------------------------+
|     Proto Pool            |  ← 方法原型(参数+返回类型)池
+---------------------------+
|     Field Pool            |  ← 字段池
+---------------------------+
|     Method Pool           |  ← 方法池
+---------------------------+
|     Class Defs            |  ← 类定义列表
+---------------------------+
|     Data Section          |  ← 数据区(代码、注解等)
+---------------------------+
|     Map Section           |  ← 映射区(MapItem 列表)
+---------------------------+

2.2 DEX Header 结构 (0x70 字节)

偏移 大小 字段 说明
0x00 8 magic DEX 魔数,值为 dex\n035\0
0x08 4 checksum ADLER32 校验和
0x0C 20 signature SHA-1 签名
0x20 4 file_size 整个 DEX 文件大小
0x24 4 header_size 头部大小(通常 0x70)
0x28 4 endian_tag 字节序标记 (0x12345678)
0x2C 4 link_size 链接段大小
0x30 4 link_off 链接段偏移
0x34 4 map_off Map 段偏移
0x38 4 string_ids_size 字符串 ID 数量
0x3C 4 string_ids_off 字符串 ID 列表偏移
0x40 4 type_ids_size 类型 ID 数量
0x44 4 type_ids_off 类型 ID 列表偏移
0x48 4 proto_ids_size 原型 ID 数量
0x4C 4 proto_ids_off 原型 ID 列表偏移
0x50 4 field_ids_size 字段 ID 数量
0x54 4 field_ids_off 字段 ID 列表偏移
0x58 4 method_ids_size 方法 ID 数量
0x5C 4 method_ids_off 方法 ID 列表偏移
0x60 4 class_defs_size 类定义数量
0x64 4 class_defs_off 类定义列表偏移
0x68 4 data_size 数据区大小
0x6C 4 data_off 数据区偏移

2.3 各数据池详解

(1) String Pool (字符串池)
  • 每个条目是一个 偏移量 (4 字节),指向数据区的 MUTF-8 编码字符串

  • MUTF-8 是修改版 UTF-8,以 0x00 结尾,使用 2 字节编码 \u0000

  • 字符串内容编码格式:[uleb128 长度] [字符数据...] [0x00 结束]

    示例:字符串 "hello"
    实际存储: 05 68 65 6C 6C 6F 00
    ↑ ↑
    | +-- 字符数据 "hello"
    +-- uleb128 长度 = 5

(2) Type Pool (类型池)
  • 每个条目是一个 string_id 索引(4 字节),指向字符串池中对应的类型描述字符串
  • 类型描述格式与 JVM 一致:
类型 Java 表示 DEX 类型描述
int int I
float float F
long long J
double double D
boolean boolean Z
char char C
short short S
byte byte B
void (无) V
引用类型 String Ljava/lang/String;
数组 int[] [I
二维数组 int[][] [[I
(3) Proto Pool (方法原型池)

每个原型条目包含:

  • shorty_idx --- 方法签名简写字符串的偏移(如 "(II)V"
  • return_type_idx --- 返回类型索引
  • param_off --- 参数列表偏移(若为 0 表示无参)
(4) Field Pool (字段池)

每个条目包含:

  • class_idx --- 所属类(type_id 索引)

  • type_idx --- 字段类型(type_id 索引)

  • name_idx --- 字段名(string_id 索引)

    示例:public String name; 所在类 Person
    class_idx → type_id → "Lcom/example/Person;"
    type_idx → type_id → "Ljava/lang/String;"
    name_idx → string_id → "name"

(5) Method Pool (方法池)

每个条目包含:

  • class_idx --- 所属类(type_id 索引)
  • proto_idx --- 方法原型(proto_id 索引)
  • name_idx --- 方法名(string_id 索引)
(6) Class Defs (类定义列表)

每个类定义条目包含:

偏移 大小 字段 说明
0x00 4 class_idx 类 type_id 索引
0x04 4 access_flags 访问标志
0x08 4 superclass_idx 父类 type_id 索引
0x0C 4 interfaces_off 接口列表偏移
0x10 4 source_file_idx 源文件名(string_id 索引)
0x14 4 annotations_off 注解数据偏移
0x18 4 class_data_off 类数据偏移
0x1C 4 static_values_off 静态字段初始值偏移

访问标志 (access_flags):

标志 说明
ACC_PUBLIC 0x1 public
ACC_PRIVATE 0x2 private
ACC_PROTECTED 0x4 protected
ACC_STATIC 0x8 static
ACC_FINAL 0x10 final
ACC_SYNCHRONIZED 0x20 synchronized
ACC_VOLATILE 0x40 volatile
ACC_BRIDGE 0x40 bridge
ACC_TRANSIENT 0x80 transient
ACC_VARARGS 0x80 varargs
ACC_NATIVE 0x100 native
ACC_INTERFACE 0x200 interface
ACC_ABSTRACT 0x400 abstract
ACC_STRICT 0x800 strictfp
ACC_SYNTHETIC 0x1000 synthetic
ACC_ANNOTATION 0x2000 annotation
ACC_ENUM 0x4000 enum
ACC_CONSTRUCTOR 0x10000 构造方法
ACC_DECLARED_SYNCHRONIZED 0x20000 declared synchronized
(7) ULEB128 编码

DEX 文件中大量使用 ULEB128 (Unsigned Little Endian Base 128) 变长编码来压缩整数:

复制代码
编码规则:
- 每个字节的高位(bit 7)表示是否还有后续字节
- 实际数据存储在低 7 位
- 小端序排列

示例:编码 6245(十六进制 0x1865)
二进制:0001 1000 0110 0101

从低位开始每 7 位一组:
组0:110 0101 = 0x65
组1:011 0000 = 0x30
组2:0

需要2组:组0和组1,小端写入:
字节0:0x65 | 0x80 = 0xE5(高位置1表示还有后续字节)
字节1:0x30(高位为0表示结束)

结果:E5 30

验证:((0x30 & 0x7F) << 7) | (0x65 & 0x7F) = 0x1800 | 0x65 = 0x1865 = 6245 ✓

三、Smali 语言详解

3.1 什么是 Smali

Smali 是 Dalvik 字节码的 可读汇编语言

工具 作用
baksmali .dex 文件反汇编为 .smali 文件
smali .smali 文件汇编为 .dex 文件
复制代码
APK 文件结构:
  classes.dex ← 主 DEX
  classes2.dex ← 多 DEX (方法数超过 65535 时)
  classes3.dex ...

反编译流程:
  APK → 解压 → .dex → baksmali → .smali 文件
  .smali 文件 → smali → .dex → 打包 → APK

3.2 基本语法与结构

3.2.1 文件结构与 Java 对照

Java 类:

java 复制代码
package com.example;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    private String message = "Hello";

    public MainActivity() {
        super();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public String getMessage() {
        return this.message;
    }
}

等价的 Smali:

smali 复制代码
.class public Lcom/example/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"

.field private message:Ljava/lang/String;

.method public constructor <init>()V
    .registers 3
    .param p0, "this"

    invoke-direct {p0}, Landroid/app/Activity;-><init>()V

    const-string v0, "Hello"
    iput-object v0, p0, Lcom/example/MainActivity;->message:Ljava/lang/String;

    return-void
.end method

.method protected onCreate(Landroid/os/Bundle;)V
    .registers 3
    .param p0, "this"
    .param p1, "savedInstanceState"

    invoke-direct {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    const v0, 0x7f030001
    invoke-virtual {p0, v0}, Lcom/example/MainActivity;->setContentView(I)V

    return-void
.end method

.method public getMessage()Ljava/lang/String;
    .registers 2

    iget-object v0, p0, Lcom/example/MainActivity;->message:Ljava/lang/String;

    return-object v0
.end method
3.2.2 Smali 文件结构要素
元素 语法 说明
类声明 .class [访问标志] [类名] 声明当前类
父类 .super [父类名] 继承的父类
源文件 .source "[文件名]" 对应的 Java 源文件名
注解 .annotation [属性] [类型] 类、方法或字段上的注解
字段 .field [标志] [名称]:[类型] 声明字段
方法 .method [标志] [名称]([参数])[返回类型] 声明方法
方法结束 .end method 标记方法体结束
寄存器声明 .registers N 该方法需要 N 个寄存器
局部变量数 .locals N N 个局部变量寄存器
参数说明 .param [寄存器], "[名称]" 为方法参数命名
行号 .line N 对应 Java 源码行号
3.2.3 寄存器体系

Dalvik 有两大类寄存器:

  • 基础寄存器: v0 - v65535

  • 参数寄存器: p0 - pn,映射到高位 v 寄存器

    寄存器分配规则:
    .registers N 表示总寄存器数 N
    .locals M 表示局部变量寄存器数为 M

    复制代码
    局部寄存器使用 v0, v1, ..., v{M-1}
    参数寄存器从 v{N-参数个数} 开始,使用 p0, p1, ..., p{n-1}

    示例:非静态方法
    .method public foo(II)V
    .registers 5 ; 共5个寄存器
    .locals 2 ; v0, v1 是局部变量

    复制代码
        ; 参数映射:
        ; p0 = v2 → this 引用
        ; p1 = v3 → 第一个 int 参数
        ; p2 = v4 → 第二个 int 参数
    .end method

关键:p0 = this --- 非 static 方法中,p0 总是 this 引用。

3.2.4 类型表示
类型 Smali 表示 Java 表示 占寄存器数
boolean Z boolean 1
byte B byte 1
char C char 1
short S short 1
int I int 1
long J long 2
float F float 1
double D double 2
void V (无返回值) 0
对象 Ljava/lang/String; String 1
数组 [I int[] 1

3.3 Smali 完整指令集与 Java 对照

3.3.1 数据加载与存储
Java 代码 Smali 指令 说明
x = 5 const/4 v0, 0x5 4 位常量加载
x = 65536 const/16 v0, 0x10000 16 位常量
x = "hello" const-string v0, "hello" 字符串常量
x = null const/4 v0, 0x0 加载 null
obj.field = x iput v0, p0, LClass;->field:LType; 实例字段写入
x = obj.field iget v0, p0, LClass;->field:LType; 实例字段读取
MyClass.f = x sput v0, LMyClass;->f:LType; 静态字段写入
x = MyClass.f sget v0, LMyClass;->f:LType; 静态字段读取
arr[i] = x aput v0, v1, v2 数组写入
x = arr[i] aget v0, v1, v2 数组读取
obj = new Obj() new-instance v0, LClass; 创建对象
arr = new int[10] new-array v0, v1, [I 创建数组
3.3.2 算术运算
Java 代码 Smali 指令 说明
a + b add-int v0, v1, v2 v0 = v1 + v2
a - b sub-int v0, v1, v2 v0 = v1 - v2
a * b mul-int v0, v1, v2 v0 = v1 * v2
a / b div-int v0, v1, v2 v0 = v1 / v2
a % b rem-int v0, v1, v2 v0 = v1 % v2
a & b and-int v0, v1, v2 v0 = v1 & v2
`a b` or-int v0, v1, v2
a ^ b xor-int v0, v1, v2 v0 = v1 ^ v2
a << b shl-int v0, v1, v2 v0 = v1 << v2
a >> b shr-int v0, v1, v2 v0 = v1 >> v2
a >>> b ushr-int v0, v1, v2 v0 = v1 >>> v2
-a neg-int v0, v1 v0 = -v1
~a not-int v0, v1 v0 = ~v1

long/float/double 类型指令将 int 替换为对应类型即可,如 add-long, sub-float, mul-double

完整示例对照:

java 复制代码
// Java
public int calc(int a, int b) {
    return (a + b) * (a - b);
}
smali 复制代码
.method public calc(II)I
    .registers 5
    add-int v0, p1, p2      ; v0 = a + b
    sub-int v1, p1, p2      ; v1 = a - b
    mul-int v2, v0, v1      ; v2 = (a+b)*(a-b)
    return v2
.end method
3.3.3 条件跳转
指令 条件 等价的 Java
if-eq vA, vB, :label vA == vB ==
if-ne vA, vB, :label vA != vB !=
if-lt vA, vB, :label vA < vB <
if-ge vA, vB, :label vA >= vB >=
if-gt vA, vB, :label vA > vB >
if-le vA, vB, :label vA <= vB <=
if-eqz vA, :label vA == 0 == 0
if-nez vA, :label vA != 0 != 0
if-ltz vA, :label vA < 0 < 0
if-gez vA, :label vA >= 0 >= 0
if-gtz vA, :label vA > 0 > 0
if-lez vA, :label vA <= 0 <= 0

if-else 分支对照:

java 复制代码
// Java
public String checkScore(int score) {
    if (score >= 60) return "Pass";
    else return "Fail";
}
smali 复制代码
.method public checkScore(I)Ljava/lang/String;
    .registers 3
    const/16 v0, 0x3c
    if-lt p1, v0, :else
    const-string v1, "Pass"
    goto :end_if
    :else
    const-string v1, "Fail"
    :end_if
    return-object v1
.end method

for 循环对照:

java 复制代码
// Java
public int sum(int n) {
    int total = 0;
    for (int i = 0; i < n; i++) total += i;
    return total;
}
smali 复制代码
.method public sum(I)I
    .registers 4
    const/4 v0, 0x0           ; total = 0
    const/4 v1, 0x0           ; i = 0
    :loop
    if-ge v1, p1, :end        ; if i >= n break
    add-int/2addr v0, v1      ; total += i
    add-int/lit8 v1, v1, 1    ; i++
    goto :loop
    :end
    return v0
.end method
3.3.4 方法调用
调用类型 Smali 指令 Java 对应
虚方法 invoke-virtual obj.method()
直接方法 invoke-direct private / super / <init>
静态方法 invoke-static Class.staticMethod()
接口方法 invoke-interface interface.method()
父类方法 invoke-super super.method()
多参 .../range 4个以上参数时使用

调用格式: invoke-{type} {vC, vD, ...}, Lclass;->method(params)returnType

示例对照:

java 复制代码
// Java
public void test() {
    int sum = Utils.add(3, 5);
    Utils util = new Utils();
    String msg = util.concat("Hello", " World");
    System.out.println(msg);
}
smali 复制代码
.method public test()V
    .registers 5
    const/4 v0, 0x3
    const/4 v1, 0x5
    invoke-static {v0, v1}, Lcom/example/Utils;->add(II)I
    move-result v0

    new-instance v1, Lcom/example/Utils;
    invoke-direct {v1}, Lcom/example/Utils;-><init>()V

    const-string v2, "Hello"
    const-string v3, " World"
    invoke-virtual {v1, v2, v3}, Lcom/example/Utils;->concat(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    move-result-object v4

    sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream;
    invoke-virtual {v2, v4}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

move-result 系列指令(获取返回值):

指令 用途
move-result vA 获取 int/float 返回值
move-result-object vA 获取对象返回值
move-result-wide vA 获取 64位(long/double)返回值
move-exception vA 获取 catch 块中的异常对象
3.3.5 Switch 语句

packed-switch(密集 case):

smali 复制代码
.method public getDayName(I)Ljava/lang/String;
    .registers 3
    packed-switch p1, :p_switch_data
    const-string v0, "Unknown"
    return-object v0
    :pswitch_1
    const-string v0, "Monday"
    return-object v0
    :pswitch_2
    const-string v0, "Tuesday"
    return-object v0
    :p_switch_data
    .packed-switch 1
        :pswitch_1
        :pswitch_2
    .end packed-switch
.end method

sparse-switch(稀疏 case):

smali 复制代码
    sparse-switch p1, :s_switch_data
    const-string v0, "Unknown"
    return-object v0
    :sswitch_403
    const-string v0, "Forbidden"
    return-object v0
    :s_switch_data
    .sparse-switch
        0x193 -> :sswitch_403
        0x194 -> :sswitch_404
    .end sparse-switch

区别: packed-switch 用 O(1) 索引,sparse-switch 用 O(log n) 二分查找。

3.3.6 异常处理
java 复制代码
// Java
public String readFile(String path) {
    try {
        FileInputStream fis = new FileInputStream(path);
        byte[] data = new byte[fis.available()];
        fis.read(data);
        fis.close();
        return new String(data);
    } catch (FileNotFoundException e) {
        return "File not found";
    } catch (IOException e) {
        return "IO Error";
    } finally {
        System.out.println("Done");
    }
}
smali 复制代码
    :try_start_0
    new-instance v0, Ljava/io/FileInputStream;
    invoke-direct {v0, p1}, Ljava/io/FileInputStream;-><init>(Ljava/lang/String;)V
    invoke-virtual {v0}, Ljava/io/FileInputStream;->available()I
    move-result v1
    new-array v1, v1, [B
    invoke-virtual {v0, v1}, Ljava/io/FileInputStream;->read([B)I
    invoke-virtual {v0}, Ljava/io/FileInputStream;->close()V
    new-instance v2, Ljava/lang/String;
    invoke-direct {v2, v1}, Ljava/lang/String;-><init>([B)V
    :try_end_0

    .catch Ljava/io/FileNotFoundException; { :try_start_0 .. :try_end_0 } :catch_fnf
    .catch Ljava/io/IOException; { :try_start_0 .. :try_end_0 } :catch_io
    .catchall { :try_start_0 .. :try_end_0 } :catchall_0

    :goto_finally
    sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v4, "Done"
    invoke-virtual {v3, v4}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-object v2

    :catch_fnf
    move-exception v5
    const-string v2, "File not found"
    goto :goto_finally

    :catchall_0
    move-exception v5
    sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v4, "Done"
    invoke-virtual {v3, v4}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    throw v5
.end method
3.3.7 synchronized 同步
java 复制代码
// Java
public class Counter {
    private int count = 0;
    public synchronized void increment() { count++; }

    public void decrement() {
        synchronized (this) { count--; }
    }
}
smali 复制代码
# 方法级同步------access_flags 含有 ACC_SYNCHRONIZED
.method public synchronized increment()V
    .registers 2
    iget v0, p0, Lcom/example/Counter;->count:I
    add-int/lit8 v0, v0, 1
    iput v0, p0, Lcom/example/Counter;->count:I
    return-void
.end method

# 代码块级同步------显式 monitor-enter / monitor-exit
.method public decrement()V
    .registers 4
    monitor-enter p0
    :try_start
    iget v0, p0, Lcom/example/Counter;->count:I
    add-int/lit8 v0, v0, -1
    iput v0, p0, Lcom/example/Counter;->count:I
    monitor-exit p0
    :try_end
    .catchall { :try_start .. :try_end } :catchall
    return-void
    :catchall
    move-exception v0
    monitor-exit p0
    throw v0
.end method

3.4 注解与内部类

3.4.1 注解 (Annotations)
smali 复制代码
# 在方法上使用注解
.method public doWork()V
    .registers 1
    .annotation runtime Lcom/example/Loggable;
        level = "DEBUG"
        enabled = true
    .end annotation
    return-void
.end method
3.4.2 内部类
java 复制代码
// Java
public class Outer {
    private String name;
    public class Inner {
        public void print() { System.out.println(name); }
    }
}

编译器处理为两个独立的 .smali 文件:

  • Outer.smali --- 包含 MemberClasses 注解
  • Outer$Inner.smali --- 包含 this$0 引用外部类、EnclosingClassInnerClass 注解
smali 复制代码
# Outer$Inner.smali
.class public Lcom/example/Outer$Inner;
.super Ljava/lang/Object;
.field final synthetic this$0:Lcom/example/Outer;

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = "Inner"
.end annotation

.method public <init>(Lcom/example/Outer;)V
    iput-object p1, p0, Lcom/example/Outer$Inner;->this$0:Lcom/example/Outer;
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    return-void
.end method

.method public print()V
    iget-object v0, p0, Lcom/example/Outer$Inner;->this$0:Lcom/example/Outer;
    iget-object v0, v0, Lcom/example/Outer;->name:Ljava/lang/String;
    sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
    invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

3.5 泛型(类型擦除)

Java 泛型在字节码中擦除为 Object,签名信息保留在注解中:

smali 复制代码
# Box<T> 擦除后
.field private item:Ljava/lang/Object;

.annotation system Ldalvik/annotation/Signature;
    value = "Ljava/lang/Object;TT;>"
.end annotation

.method public get()Ljava/lang/Object;
    iget-object v0, p0, Lcom/example/Box;->item:Ljava/lang/Object;
    return-object v0
.end method

# 调用端自动插入 check-cast
invoke-virtual {v0}, Lcom/example/Box;->get()Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/String;

3.6 调试与常用工具

工具 用途 命令
apktool APK 解包/打包 apktool d app.apk -o out
baksmali DEX → Smali java -jar baksmali.jar d classes.dex
smali Smali → DEX java -jar smali.jar a smali_out -o classes.dex
dexdump DEX 十六进制导出 dexdump -d classes.dex

调试日志注入技巧:

smali 复制代码
# 在方法入口处插入
const-string v0, "DEBUG_TAG"
const-string v1, "进入方法 foo"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

四、Smali 指令速查表

4.1 加载常量

指令 范围 说明
const/4 vA, #+B -8~7 小整数
const/16 vA, #+BBBB -32768~32767 中整数
const vA, #+BBBBBBBB 32位 大整数
const-wide/16 vA, #+BBBB 64位低16位 小 long
const-wide/32 vA, #+BBBBBBBB 64位低32位 中 long
const-wide vA, #+BBBBBBBBBBBBBBBB 64位 大 long
const-string vA, string_id 字符串 加载字符串
const-class vA, type_id 类型 加载 Class

4.2 数据移动

指令 说明
move vA, vB vA = vB
move-wide vA, vB 64位值移动
move-object vA, vB 对象引用移动
move-result vAA 获取 invoke 返回值(int)
move-result-object vAA 获取 invoke 返回值(对象)
move-result-wide vAA 获取 invoke 返回值(long/double)
move-exception vAA 获取 catch 异常对象

4.3 返回指令

指令 说明
return-void void 返回
return vAA 返回 int/float
return-wide vAA 返回 long/double
return-object vAA 返回对象

4.4 数组操作

指令 说明
array-length vA, vB vA = vB.length
new-array vA, vB, type 创建数组
aget vAA, vBB, vCC int 数组读
aget-wide vAA, vBB, vCC long 数组读
aget-object vAA, vBB, vCC 对象数组读
aput vAA, vBB, vCC int 数组写
aput-wide vAA, vBB, vCC long 数组写
aput-object vAA, vBB, vCC 对象数组写

4.5 实例操作

指令 说明
new-instance vAA, type 创建新实例
instance-of vA, vB, type 类型检查 (instanceof)
check-cast vAA, type 类型转换
monitor-enter vAA 获取锁
monitor-exit vAA 释放锁
throw vAA 抛出异常

4.6 类型转换

指令 说明
neg-int / not-int 整数取负/取反
int-to-long / int-to-float / int-to-double int 扩展
long-to-int / long-to-float / long-to-double long 转换
float-to-int / float-to-long / float-to-double float 转换
double-to-int / double-to-long / double-to-float double 转换
int-to-byte / int-to-char / int-to-short int 缩窄

五、DEX 文件格式进阶

5.1 Class Data 区详解

类定义中的 class_data_off 指向 Class Data 结构,使用 uleb128 编码:

复制代码
静态字段数量 (uleb128)
实例字段数量 (uleb128)
直接方法数量 (uleb128)   ← private / static / <init>
虚方法数量 (uleb128)     ← public / protected / package

// 每个字段编码(先静态再实例)
[field_idx_diff]  ← 与上一个 field_id 的差值(uleb128)
[access_flags]    ← 访问标志(uleb128)

// 每个方法编码(先 direct 再 virtual)
[method_idx_diff] ← 与上一个 method_id 的差值(uleb128)
[access_flags]    ← 访问标志(uleb128)
[code_off]        ← CodeItem 偏移量(uleb128,0 表示 native/abstract)

差分编码:由于 field_id / method_id 在表中按 class→name→type 排序,相邻条目的索引值相近,差分后用 uleb128 可大幅压缩空间。

5.2 CodeItem 结构

复制代码
┌──────────┬────────┬──────────────────────────────┐
│ 偏移     │ 大小    │ 字段                          │
├──────────┼────────┼──────────────────────────────┤
│ 0x00     │ 2      │ registers_size               │ ← 寄存器总数
│ 0x02     │ 2      │ ins_size                     │ ← 入参寄存器数
│ 0x04     │ 2      │ outs_size                    │ ← 出参寄存器数
│ 0x06     │ 2      │ tries_size                   │ ← try-catch 块数
│ 0x08     │ 4      │ debug_info_off               │ ← 调试信息偏移
│ 0x0C     │ 4      │ insns_size                   │ ← 指令长度(2字节单元)
│ 0x10     │ ...    │ insns[]                      │ ← 指令序列
│ ...      │ padding│ 对齐到4字节                    │
│ ...      │ ...    │ tries[]                      │ ← try-catch 列表
│ ...      │ ...    │ handlers[]                   │ ← catch 处理器列表
└──────────┴────────┴──────────────────────────────┘

5.3 Map Section (映射区)

DEX 文件的 map_off 指向一个 MapItem 列表,用于快速定位各段:

复制代码
map_item 结构:
┌──────────┬────────┬─────────────────────────────┐
│ 偏移     │ 大小    │ 字段                          │
├──────────┼────────┼─────────────────────────────┤
│ 0x00     │ 2      │ type                        │ ← 段类型(见下表)
│ 0x02     │ 2      │ unused                      │
│ 0x04     │ 4      │ size                        │ ← 条目数
│ 0x08     │ 4      │ offset                      │ ← 在该段中的偏移
└──────────┴────────┴─────────────────────────────┘
type 值 段类型 说明
0x0000 kDexTypeHeaderItem DEX 头部
0x0001 kDexTypeStringIdItem 字符串 ID
0x0002 kDexTypeTypeIdItem 类型 ID
0x0003 kDexTypeProtoIdItem 原型 ID
0x0004 kDexTypeFieldIdItem 字段 ID
0x0005 kDexTypeMethodIdItem 方法 ID
0x0006 kDexTypeClassDefItem 类定义
0x1000 kDexTypeCodeItem 代码
0x1001 kDexTypeStringDataItem 字符串数据
0x1002 kDexTypeDebugInfoItem 调试信息
0x1003 kDexTypeAnnotationItem 注解
0x2000 kDexTypeAnnotationSetRefList 注解引用列表
0x2001 kDexTypeAnnotationDirectoryItem 注解目录
0x2002 kDexTypeAnnotationSetItem 注解集合
0x2003 kDexTypeAnnotationOffItem 注解偏移
0x2004 kDexTypeAnnotationSetItem 注解集合(重)
0x2005 kDexTypeAnnotationItem 注解(重)

5.4 多 DEX (Multi-DEX)

概念 说明
65535 限制 DEX 的 method_id 用 16 位索引,单 DEX 最多 65535 个方法引用
解决方案 Android 5.0+ 原生支持多 DEX,低版本需 android.support.multidex
文件命名 classes.dex, classes2.dex, classes3.dex, ...
主 DEX 规则 必须包含 ApplicationActivity 等四大组件及 MultiDex.install()
复制代码
DEX 分包示例:
classes.dex    ← 主 DEX,含 Application/Activity/Service/BroadcastReceiver
classes2.dex   ← 次 DEX,含第三方库代码
classes3.dex   ← 次 DEX,含不常用功能代码

5.5 Compact DEX (CDEX) --- Android 9+

Android 9 (Pie) 引入 Compact DEX 格式,作为 .dex 文件的压缩变体:

特性 说明
文件扩展名 .cdex(打包在 APK 中为 .dex,ART 会在 OTA 时转换)
压缩算法 自定义的 LZ4 变体 + 共享字符串表
优势 APK 安装包体积更小
局限 只能在 ART 上运行,需由 dex2oat 处理

六、Smali 进阶技术与 Java 对照

6.1 算术增强指令(2-in-1 模式)

Dalvik 在标准三寄存器指令外,还提供操作数与目标寄存器相同(就地修改)的增强模式:

/2addr 后缀: vA 同时作为源和目标

指令 等价于 说明
add-int/2addr v0, v1 v0 = v0 + v1 就地加法
sub-int/2addr v0, v1 v0 = v0 - v1 就地减法
mul-int/2addr v0, v1 v0 = v0 * v1 就地乘法
div-int/2addr v0, v1 v0 = v0 / v1 就地除法
rem-int/2addr v0, v1 v0 = v0 % v1 就地取模
and-int/2addr v0, v1 v0 = v0 & v1 就地与
or-int/2addr v0, v1 `v0 = v0 v1`
xor-int/2addr v0, v1 v0 = v0 ^ v1 就地异或
shl-int/2addr v0, v1 v0 = v0 << v1 就地左移
shr-int/2addr v0, v1 v0 = v0 >> v1 就地右移
ushr-int/2addr v0, v1 v0 = v0 >>> v1 就地无符号右移

/lit 后缀: 立即数运算

指令 等价于
add-int/lit8 vA, vB, #C vA = vB + C(C 为 8 位有符号)
add-int/lit16 vA, vB, #C vA = vB + C(C 为 16 位有符号)
rsub-int vA, vB, #C vA = C - vB(反向减法)
mul-int/lit8 vA, vB, #C vA = vB * C
div-int/lit8 vA, vB, #C vA = vB / C
rem-int/lit8 vA, vB, #C vA = vB % C
and-int/lit8 vA, vB, #C vA = vB & C
or-int/lit8 vA, vB, #C `vA = vB
java 复制代码
// Java
public int demo(int x) {
    x += 10;
    x *= 2;
    x -= 5;
    return x / 3;
}
smali 复制代码
.method public demo(I)I
    .registers 3
    add-int/lit8 v0, p1, 0xa      ; v0 = x + 10
    const/4 v1, 0x2
    mul-int/2addr v0, v1          ; v0 = v0 * 2
    add-int/lit8 v0, v0, -5       ; v0 = v0 - 5
    const/4 v1, 0x3
    div-int/2addr v0, v1          ; v0 = v0 / 3
    return v0
.end method

6.2 数组初始化优化

java 复制代码
// Java 方式1:逐个赋值
int[] arr = new int[3];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;

// Java 方式2:带初始值的简写
int[] arr = {1, 2, 3};
// 或 new int[]{4, 5, 6}
smali 复制代码
# 方式1:逐个赋值
const/4 v0, 0x3
new-array v0, v0, [I
const/4 v1, 0x1
const/4 v2, 0x0
aput v1, v0, v2
const/4 v1, 0x2
const/4 v2, 0x1
aput v1, v0, v2

# 方式2:filled-new-array(优化方式,仅用于方法参数中的内联数组)
const/4 v0, 0x1
const/4 v1, 0x2
const/4 v2, 0x3
filled-new-array {v0, v1, v2}, [I
move-result-object v0

# 方式3:fill-array-data(编译器常用优化,通过数据区批量填充)
const/4 v0, 0x3
new-array v0, v0, [I
fill-array-data v0, :array_data    ; 指向数据区的批量数据
...
:array_data
.array-data 4                      ; 每个元素 4 字节
    0x00000001                     ; 1
    0x00000002                     ; 2
    0x00000003                     ; 3
.end array-data

6.3 StringBuilder 与字符串拼接(全展开 vs StringBuilder)

java 复制代码
// Java
public String greet(String name) {
    return "Hello, " + name + "! Welcome!";
}

编译器可能生成两种模式的 Smali:

模式 A:StringBuilder 模式(源码级别)

smali 复制代码
.method public greet(Ljava/lang/String;)Ljava/lang/String;
    .registers 5
    new-instance v0, Ljava/lang/StringBuilder;
    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
    const-string v1, "Hello, "
    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    const-string v1, "! Welcome!"
    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v0
    return-object v0
.end method

模式 B:String.concat 模式(编译器优化后)

smali 复制代码
.method public greet(Ljava/lang/String;)Ljava/lang/String;
    .registers 4
    const-string v0, "Hello, "
    invoke-virtual {v0, p1}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
    move-result-object v0
    const-string v1, "! Welcome!"
    invoke-virtual {v0, v1}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;
    move-result-object v0
    return-object v0
.end method

6.4 compareTo/cmp 比较

java 复制代码
// Java
public int compare(long a, long b) {
    return Long.compare(a, b);
}
smali 复制代码
.method public compare(JJ)I
    .registers 6
    .param p0, "this"
    .param p1, "a"       ; p1(低32位), p2(高32位) = a
    .param p3, "b"       ; p3(低32位), p4(高32位) = b

    cmp-long v0, p1, p3  ; v0 = compare(a, b)
    return v0
.end method

浮点比较的 NaN 处理差异:

指令 NaN 时行为 适用场景
cmpl-float/double 返回 -1 用于 <<= 判断(NaN 被视为小于任何值)
cmpg-float/double 返回 1 用于 >>= 判断(NaN 被视为大于任何值)
java 复制代码
// Java
public int check(float a, float b) {
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}
smali 复制代码
.method public check(FF)I
    .registers 4
    cmpl-float v0, p1, p2      ; cmpl: NaN → -1
    if-ltz v0, :less
    if-gtz v0, :greater
    const/4 v0, 0x0
    return v0
    :less
    const/4 v0, -1
    return v0
    :greater
    const/4 v0, 0x1
    return v0
.end method

6.5 反射的 Smali 实现

java 复制代码
// Java
public Object reflectCall(String className, String methodName) throws Exception {
    Class<?> clazz = Class.forName(className);
    Method method = clazz.getDeclaredMethod(methodName);
    method.setAccessible(true);
    return method.invoke(null);  // 静态方法,第一个参数为 null
}
smali 复制代码
.method public reflectCall(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
    .registers 8
    .param p0, "this"
    .param p1, "className"
    .param p2, "methodName"

    # Class.forName(className)
    invoke-static {p1}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;
    move-result-object v0

    # clazz.getDeclaredMethod(methodName)
    const/4 v1, 0x0
    new-array v1, v1, [Ljava/lang/Class;     ; 无参 → 空 Class 数组
    invoke-virtual {v0, p2, v1}, Ljava/lang/Class;->getDeclaredMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
    move-result-object v2

    # method.setAccessible(true)
    const/4 v3, 0x1
    invoke-virtual {v2, v3}, Ljava/lang/reflect/AccessibleObject;->setAccessible(Z)V

    # method.invoke(null)  --- 静态方法调用
    const/4 v3, 0x0
    const/4 v4, 0x0
    new-array v4, v4, [Ljava/lang/Object;
    invoke-virtual {v2, v3, v4}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
    move-result-object v5

    return-object v5
.end method

6.6 枚举的 Smali 实现

java 复制代码
// Java
public enum Color {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF);

    final int rgb;
    Color(int rgb) { this.rgb = rgb; }
}

编译器将枚举编译为继承 java.lang.Enum 的普通类,同时自动生成 $VALUES 数组和 valueOf/values 方法:

smali 复制代码
.class public final enum Lcom/example/Color;
.super Ljava/lang/Enum;
.source "Color.java"

# 枚举常量作为静态字段
.field public static final enum RED:Lcom/example/Color;
.field public static final enum GREEN:Lcom/example/Color;
.field public static final enum BLUE:Lcom/example/Color;

# 实例字段
.field final rgb:I

# 编译器合成的 $VALUES 数组
.field private static final synthetic $VALUES:[Lcom/example/Color;

# 静态初始化块(<clinit>)
.method static constructor <clinit>()V
    .registers 6

    # new Color("RED", 0, 0xFF0000)
    new-instance v0, Lcom/example/Color;
    const-string v1, "RED"
    const/4 v2, 0x0              ; ordinal = 0
    const v3, 0xFF0000
    invoke-direct {v0, v1, v2, v3}, Lcom/example/Color;-><init>(Ljava/lang/String;II)V
    sput-object v0, Lcom/example/Color;->RED:Lcom/example/Color;

    # new Color("GREEN", 1, 0x00FF00)
    new-instance v0, Lcom/example/Color;
    const-string v1, "GREEN"
    const/4 v2, 0x1
    const v3, 0x00FF00
    invoke-direct {v0, v1, v2, v3}, Lcom/example/Color;-><init>(Ljava/lang/String;II)V
    sput-object v0, Lcom/example/Color;->GREEN:Lcom/example/Color;

    # 初始化 $VALUES 数组
    const/4 v0, 0x3
    new-array v0, v0, [Lcom/example/Color;
    sget-object v1, Lcom/example/Color;->RED:Lcom/example/Color;
    const/4 v2, 0x0
    aput-object v1, v0, v2
    sget-object v1, Lcom/example/Color;->GREEN:Lcom/example/Color;
    const/4 v2, 0x1
    aput-object v1, v0, v2
    sget-object v1, Lcom/example/Color;->BLUE:Lcom/example/Color;
    const/4 v2, 0x2
    aput-object v1, v0, v2
    sput-object v0, Lcom/example/Color;->$VALUES:[Lcom/example/Color;

    return-void
.end method

6.7 Lambda 表达式的 Smali 实现

java 复制代码
// Java
button.setOnClickListener(v -> {
    Log.d("TAG", "Clicked!");
});

编译器将 Lambda 转换为 invoke-dynamic + 辅助方法(或直接生成匿名内部类):

smali 复制代码
# 方式一:invoke-dynamic(Java 8+ 的 Lambda Metafactory)
# 在调用处:
    sget-object v0, Lcom/example/MainActivity$1;->INSTANCE:Lcom/example/MainActivity$1;
    invoke-interface {v0}, Landroid/view/View$OnClickListener;->onClick(Landroid/view/View;)V

# 同时在同一个类中生成合成方法(lambda$开头):
.method private static synthetic lambda$onCreate$0(Landroid/view/View;)V
    .registers 3
    const-string v0, "TAG"
    const-string v1, "Clicked!"
    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    return-void
.end method

# 方式二:匿名内部类辅助
# 也可以编译为类似匿名内部类的形式
.class Lcom/example/MainActivity$$ExternalSyntheticLambda0;
.implements Landroid/view/View$OnClickListener;
    ... 实现 onClick 方法并委托给 lambda$onCreate$0

6.8 try-with-resources 的资源关闭模式

java 复制代码
// Java
public void readFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        System.out.println(br.readLine());
    }
}

编译器为 try-with-resources 生成额外的异常抑制逻辑:

smali 复制代码
.method public readFile(Ljava/lang/String;)V
    .registers 8

    :try_start_open
    new-instance v0, Ljava/io/FileReader;
    invoke-direct {v0, p1}, Ljava/io/FileReader;-><init>(Ljava/lang/String;)V
    new-instance v1, Ljava/io/BufferedReader;
    invoke-direct {v1, v0}, Ljava/io/BufferedReader;-><init>(Ljava/io/Reader;)V
    :try_start_body

    # 使用资源
    invoke-virtual {v1}, Ljava/io/BufferedReader;->readLine()Ljava/lang/String;
    move-result-object v2
    sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;
    invoke-virtual {v3, v2}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

    # 正常关闭
    invoke-virtual {v1}, Ljava/io/BufferedReader;->close()V
    :try_end_body

    .catchall {:try_start_body .. :try_end_body} :catchall_body
    .catch Ljava/lang/Throwable; {:try_start_open .. :try_start_body} :catch_primary

    return-void

    # 主异常处理 + 资源关闭(含抑制异常)
    :catch_primary
    move-exception v4
    :try_start_close
    invoke-virtual {v1}, Ljava/io/BufferedReader;->close()V
    :try_end_close
    .catch Ljava/lang/Throwable; {:try_start_close .. :try_end_close} :catch_suppressed
    throw v4

    :catch_suppressed
    move-exception v5
    invoke-virtual {v4, v5}, Ljava/lang/Throwable;->addSuppressed(Ljava/lang/Throwable;)V
    throw v4

    # 外层 finally:确保资源关闭
    :catchall_body
    move-exception v6
    throw v6
.end method

6.9 多态与方法的 Smali 表示

java 复制代码
// Java
public class Animal {
    public void speak() { System.out.println("..."); }
}

public class Dog extends Animal {
    @Override
    public void speak() { System.out.println("Woof!"); }
}

// 调用处
Animal a = new Dog();
a.speak();      // 输出 "Woof!" --- 运行时多态
smali 复制代码
# Animal 类中
.method public speak()V
    .registers 3
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v1, "..."
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

# Dog 类中------invoke-virtual 用于虚方法分发
.method public speak()V
    .registers 3
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v1, "Woof!"
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

# 调用处------invoke-virtual 实现运行时多态
    new-instance v0, Lcom/example/Dog;
    invoke-direct {v0}, Lcom/example/Dog;-><init>()V
    invoke-virtual {v0}, Lcom/example/Animal;->speak()V    ; invoke-virtual 在运行时查找实际类型

invoke-virtual vs invoke-direct:

  • invoke-virtual:根据对象实际类型(运行时类)查找方法表,支持多态
  • invoke-direct:直接调用指定类的具体方法(private、、super 调用),不进行虚方法分发

6.10 APK 修改实战流程

复制代码
1. apktool d target.apk -o out/          ← 反编译 APK
2. 编辑 out/smali/com/example/.../       ← 修改 Smali 代码
3. apktool b out/ -o modified.apk        ← 重新打包
4. 解压 modified.apk 删除 META-INF/*     ← 删除旧签名
5. jarsigner -keystore my.keystore modified.apk alias  ← 签名
6. adb install -r modified.apk           ← 安装测试

NOP 填充绕过技巧:

smali 复制代码
# 原始指令(检查 root 权限)
invoke-static {}, Lcom/example/RootDetector;->isRooted()Z
move-result v0
if-eqz v0, :continue                    ; 不通过则跳走
const-string v0, "Rooted device!"
invoke-static {v0}, Lcom/example/Utils;->exitApp(Ljava/lang/String;)V
:continue

# NOP 绕过(直接跳到 continue 标签)
goto :continue                          ; 用 goto 无条件跳过检查

七、附录:Java → Smali 快速翻译指南

7.1 常见 Java 结构的 Smali 等价

Java 结构 Smali 等价模式
int x = 0; const/4 v0, 0x0
long x = 0L; const-wide/16 v0, 0x0
String s = "hi"; const-string v0, "hi"
int[] arr = new int[n]; new-array v0, v1, [I
arr.length array-length v0, v1
obj instanceof T instance-of v0, v1, LT;
(T) obj check-cast v0, LT;
return x; return v0
return null; const/4 v0, 0x0 + return-object v0
this.x = x; iput v0, p0, LClass;->x:LType;
ClassName.x = x; sput v0, LClassName;->x:LType;
super.method() invoke-super {p0, ...}, LSuper;->method(...)
new Foo(a, b) new-instance v0, LFoo; + invoke-direct {v0, v1, v2}, LFoo;-><init>(...)V
throw new E(msg) new-instance v0, LE; + invoke-direct {v0, v1}, LE;-><init>(String)V + throw v0
try { ... } catch(E e) { } :try_start + ... + :try_end + .catch LE; { :try_start .. :try_end } :handler + :handler + move-exception v0

7.2 寄存器分配快速参考

方法类型 p0 p1 p2 ...
非 static 方法 this 第1个参数 第2个参数 ...
static 方法 第1个参数 第2个参数 第3个参数 ...
构造方法 <init> this 构造参数1 构造参数2 ...

long/double 占 2 个寄存器:method(long a, int b) 中,p0=this, p1/p2=a(long 占 vN, vN+1), p3=b

7.3 Smali 反编译标志速查

Smali 标志 Java 等价
.class public 0x1 public class
.class final 0x10 final class
.class abstract 0x400 abstract class
.class interface abstract 0x200+0x400 interface
.class enum 0x4000 enum
.class annotation 0x2000 @interface
public static 0x1+0x8 public static
private 0x2 private
protected 0x4 protected
static 0x8 static
final 0x10 final
synchronized 0x20 synchronized
native 0x100 native
abstract 0x400 abstract
constructor 0x10000 <init> 构造方法
declared-synchronized 0x20000 synchronized 方法修饰
相关推荐
zb200641206 小时前
Laravel5.x核心特性全解析
android·spring boot·php·laravel
_李小白6 小时前
【android opencv学习笔记】Day 21: 形态学开运算与闭运算
android·opencv·学习
zhangfeng11336 小时前
ThinkPHP5 事件系统的标准最佳实践 事件系统的完整设计逻辑tags.php tags.php(事件地图)
android·开发语言·php
_李小白6 小时前
【Android车载学习笔记】第四天:AAOS系统架构
android·笔记·学习
圆粥綠6 小时前
【保姆级】国内Windows用户Android Studio下载+安装+配置完整教程(2026最新版,避坑指南)
android·windows·android studio
User_芊芊君子6 小时前
一条命令搞定 mysql_exporter 部署,Shell 脚本把重复配置这件事自动化了
android·mysql·自动化
huaCodeA7 小时前
Android面试-Kotlin作用域函数
android·面试·kotlin
BlueBirdssh7 小时前
fastboot vs adb 的区别
android·adb
imuliuliang7 小时前
Laravel5.x核心特性全解析
android·运维·数据库·nginx