当前位置:首页>安卓APP>KMP编译魔法一套代码搞定安卓和iOS程序员狂喜

KMP编译魔法一套代码搞定安卓和iOS程序员狂喜

  • 2026-10-11 06:08:09
KMP编译魔法一套代码搞定安卓和iOS程序员狂喜

同一份 Kotlin 代码如何在 Android 和 iOS 上运行?揭秘 KMP 背后的编译器魔法

深入探讨“一次编写,多端部署”在编译器层面究竟发生了什么

作为一名 Android 开发新手,当我第一次听说 Kotlin Multiplatform (KMP) 时,一个问题一直困扰着我:

“完全相同的 Kotlin 代码,怎么可能同时在 Android 和 iOS 上原生运行?”

我的意思是,Android 使用 JVM,iOS 不用。Android 运行 Dalvik 字节码,iOS 运行 ARM 机器码。它们是完全不同的架构。那么这到底是如何实现的呢?

答案不是“魔法”——而是卓越的编译器工程。一旦我理解了编译层面发生的事情,一切都豁然开朗了。

让我为你揭示幕后的真实运作机制。

根本区别:编译 vs 解释

这里有一个关键见解,可以回答我们的问题:

KMP 不会创建一个到处都能运行的通用二进制文件。相反,Kotlin 编译器会为每个平台创建不同的原生二进制文件。

React Native 和 Flutter 使用运行时桥接或虚拟机。KMP 在编译器层面做了根本不同的事情。

这些框架创建了一个在运行时解释你代码的“包装器”。KMP 的做法则截然不同:

  1. 你编写一次 Kotlin 代码(你的业务逻辑、网络请求、数据库操作)
  2. Kotlin 编译器为每个平台生成原生代码:
    • Android → Kotlin/JVM 字节码(就是你已经在写的代码!)
    • iOS → 通过 Kotlin/Native 生成的原生 ARM64 机器码
    • JavaScript → 通过 Kotlin/JS 生成的实际 JS 代码
    • 桌面端 → 原生二进制文件

输出不是一个包装器。它是针对每个平台的实际原生代码。

仔细想想。你的 Kotlin 代码变成了真正的原生 iOS 代码,而不是在桥接中运行的 JavaScript。这就是为什么 KMP 应用的性能与完全原生的应用一样好。

🧩 三层架构

KMP 遵循一个清晰的架构模式,分离关注点:

┌───────────────────────────────────────────────┐│   平台特定代码 (UI 层)                         ││   Android: Jetpack Compose/XML                ││   iOS: SwiftUI/UIKit                          │└───────────────────────────────────────────────┘│┌───────────────────────────────────────────────┐│   共享业务逻辑 (Kotlin 共享模块)               ││   网络请求、数据库、业务规则                   │└───────────────────────────────────────────────┘│┌───────────────────────────────────────────────┐│   平台 API 适配器 (expect/actual)              ││   文件系统、网络、传感器等                     │└───────────────────────────────────────────────┘

关键点:

  1. 你编写共享的 Kotlin 代码,放在一个共享模块中。
  2. Kotlin 编译器有多个后端——每个平台一个。
  3. 每个后端将你的代码编译为原生格式:
    • Android → JVM 字节码 (.class 文件 → DEX 字节码)
    • iOS → ARM64/x86_64 机器码 (原生二进制文件)
    • JavaScript → JavaScript 源代码
    • 桌面端 → 原生可执行文件 (通过 LLVM)

核心在于: 这些是相同输入产生的不同输出。

你的 Kotlin 源代码经过不同的编译流水线,每条流水线都为其目标平台生成真正的原生代码。没有解释,没有桥接,没有性能损失。

让我向你展示每条流水线中具体发生了什么。

🔧 编译过程:实际发生了什么

让我们写一些简单的 Kotlin 代码,并精确追踪它为 Android 和 iOS 编译时发生了什么。

你的共享 Kotlin 代码

// 在 commonMain/kotlin/UserRepository.kt
classUserRepository {
    fun getUser(id: String): User {
        return User(id, "John Doe")
    }
}

data classUser(val id: String, val name: String)

很简单。现在看看当你构建项目时会发生什么。

📱 编译流水线 #1:Android (Kotlin/JVM)

逐步过程

  1. Kotlin 源代码 (UserRepository.kt)
  2. Kotlin/JVM 编译器 (kotlinc)
  3. JVM 字节码 (.class 文件)
  4. D8 编译器 (Android 构建工具)
  5. DEX 字节码 (.dex 文件)
  6. 打包进 APK/AAB
  7. 在 Android 运行时 (ART) 上运行

字节码的样子

编译后,如果你反编译 Android 字节码,你会看到类似这样的内容:

// 反编译的 JVM 字节码 (简化版)
public final classUserRepository {
    public final User getUser(String id) {
        return new User(id, "John Doe");
    }
}

public final classUser {
    private final String id;
    private final String name;

    public User(String id, String name) {
        this.id =id;
        this.name = name;
    }
    //... getters
}

这就是你的 Android 应用一直运行的代码! KMP 只是复用你已经拥有的编译流水线。

🍎 编译流水线 #2:iOS (Kotlin/Native)

逐步过程

  1. 相同的 Kotlin 源代码 (UserRepository.kt)
  2. Kotlin/Native 编译器 (使用 LLVM)
  3. ARM64 机器码 (原生二进制文件)
  4. 打包为 .framework 包
  5. Swift/Objective-C 可以直接导入和使用
  6. 直接在 iOS 硬件上运行——没有虚拟机!

这是关键见解:Kotlin 编译器不会创建一个通用二进制文件。它会创建针对每个目标平台优化的平台特定原生代码。

💡 expect/actual 模式

但是平台特定的功能怎么办?当相机或文件系统在每个平台上的工作方式不同时,你如何访问它们?

答案是:expect/actual 声明。

公共代码 (你需要什么)

// 在 commonMain - 共享代码
expect classPlatformLogger {
    fun log(message: String)
}

// 你的共享代码现在可以使用它:classAuthService {
    private val logger = PlatformLogger()

    fun login(email: String) {
        logger.log("User logging in:  $email")
        //... 其余的登录逻辑
    }
}

平台实现 (每个平台如何实现)

// 在 androidMain
actual classPlatformLogger {
    actual fun log(message: String) {
        android.util.Log.d("MyApp", message)
    }
}

// 在 iosMain
actual classPlatformLogger {
    actual fun log(message: String) {
        NSLog("MyApp", message)
    }
}

编译时, 每个平台获得其自己的 PlatformLogger 实现。共享代码调用 log(),但实际执行的是平台特定的代码。

🤝 互操作性:iOS 如何调用你的 Kotlin 代码

编译后,你得到一个 .framework 包。但是 Swift 需要调用你的 Kotlin 代码。这是如何工作的?

Kotlin/Native 编译器生成 Objective-C 头文件

对于我们的 UserRepository 例子,编译器会自动生成:

// 自动生成的头文件 (UserRepository.h)
@interface SharedUserRepository : NSObject
- (instancetype)init;
- (SharedUser *)getUserWithId:(NSString *)id;
@end@interface SharedUser : NSObject
@property (readonly) NSString *id;
@property (readonly) NSString *name;
- (instancetype)initWithId:(NSString *)id name:(NSString *)name;
@end

在 Swift 中使用

Swift 现在可以自然地使用你的 Kotlin 代码:

importShared// 你的 KMP 模块

let repository = UserRepository()
let user = repository.getUser(id: "123")
print("User name: \(user.name)")

这里发生了什么?

  1. Swift 调用 Objective-C 互操作层
  2. Objective-C 桥接到你的 Kotlin/Native 代码
  3. 你的 Kotlin 代码作为原生 ARM64 机器码运行
  4. 结果通过互操作层传回
  5. Swift 接收到原生的 Objective-C/Swift 对象

互操作层具有接近零的开销——它只是函数调用转发,没有序列化或消息传递。

追踪一个函数在两个平台上的执行

让我们追踪一个真实的函数通过两个编译流水线,看看其中的差异:

共享的 Kotlin 代码

// 在 commonMain
classCalculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }
}

Android 编译后 (JVM 字节码)

// 反编译的 JVM 字节码
public final classCalculator {
    public final int add(int a, int b) {
        return a + b;
    }
}

当它在 Android 上运行时:

JVM 栈:
1. 将 'a' 压入栈
2. 将 'b' 压入栈
3. 执行 IADD 指令 (整数加法)
4. 从栈返回结果

iOS 编译后 (ARM64 汇编)

; ARM64 汇编 (简化版)
_Calculator_add:
    add w0, w0, w1    ; w0 = w0 + w1 (32 位加法)
    ret               ; 结果返回到 w0

当它在 iOS 上运行时:

ARM64 寄存器:
1. 参数 'a' 在寄存器 w0 中
2. 参数 'b' 在寄存器 w1 中
3. 执行 ADD 指令 (直接 CPU 操作)
4. 结果保留在 w0 中 (返回寄存器)

注意区别:

  • Android:使用基于栈的 JVM 指令
  • iOS:使用基于寄存器的 ARM 指令

两者都是各自平台的原生代码——没有解释发生!

🧠 内存管理:平台间的差异

编译器层面的另一个关键区别是内存管理方式。

Android (JVM 垃圾回收)

// 你的代码
val user = User("123", "John")
//...// user 超出作用域

发生了什么:

  1. 对象在 JVM 堆上分配
  2. GC 跟踪引用
  3. 当没有引用存在时,GC 标记为待回收
  4. GC 定期运行以释放内存

iOS (自动引用计数)

相同的 Kotlin 代码编译为:

  1. 对象在原生堆上分配
  2. 引用计数器 = 1
  3. 当传递/存储时:计数器++
  4. 当引用丢弃时:计数器--
  5. 当计数器 = 0 时:立即释放

编译器生成不同的内存管理代码!

对于我们的 User 对象,iOS 编译包含:

// 生成的原生代码伪代码
User* user = User_create("123", "John"); // ref_count =1
retain(user);                             // ref_count =2//... 使用 user ...
release(user);                            // ref_count =1
release(user);                            // ref_count =0→ 释放

你编写相同的 Kotlin 代码,但编译器为每个平台以不同的方式处理内存。

❌ 常见误解澄清

一个常见问题:“如果相同的代码在不同的平台上运行,那不就意味着在运行时发生了一些翻译吗?”

不!原因如下:

React Native/Flutter (运行时桥接): 你的代码 → JavaScript → 桥接 → 原生 API (在运行时解释,有序列化开销)

KMP (编译时转换): 你的代码 → 平台特定的原生代码 (没有运行时翻译,直接执行)

项目结构

myapp/├── commonMain/kotlin/│└── Calculator.kt
  ├── androidMain/kotlin/│└── MainActivity.kt
  └── iosMain/kotlin/└── iOS.kt

构建输出

运行 ./gradlew build 后:

build/├── android/│└── classes/│└── Calculator.class← JVM 字节码
  │└── ios/└── MyApp.framework/├── MyApp              ← ARM64 二进制文件 (原生机器码)
          ├── Headers/│└── MyApp.h       ← Objective-C 互操作头文件
          └── Info.plist

KMP 不是:

  • ❌ 运行时桥接 (如 React Native)
  • ❌ 虚拟机 (如桌面端的 Java)
  • ❌ 解释器
  • ❌ WebView 包装器

KMP 是:

  • ✅ 一个具有多个后端的编译器
  • ✅ 每个后端为其平台生成原生代码
  • ✅ 零运行时开销——没有解释
  • ✅ 编译时代码转换,而非运行时翻译

心智模型:

你的 Kotlin 源代码
                           ││┌──────────────┼──────────────┐│││▼▼▼
       JVM 后端     原生后端       JS 后端
            │││▼▼▼
     JVM 字节码     ARM64 代码    JavaScript
            │││▼▼▼
      Android ART      iOS CPU       浏览器

相同的源代码 → 不同的编译路径 → 不同的原生输出。

这就是为什么它的性能像原生代码:它本来就是原生代码!

🎯 结论

Kotlin Multiplatform 的魔力其实不是魔法——而是卓越的编译器工程。

通过使用多个后端,每个后端都生成真正的原生代码,KMP 让你鱼与熊掌兼得:

  • ✅ 代码共享 (一次编写)
  • ✅ 原生性能 (无开销)
  • ✅ 平台特定优化 (为每个目标量身定制)

当有人问你 “相同的 Kotlin 代码如何在 Android 和 iOS 上运行?” 时,你现在可以解释:

“它并没有。相同的 Kotlin 源代码被编译成针对每个平台的不同原生代码。在 Android 上,它变成 JVM 字节码。在 iOS 上,它通过 LLVM 变成 ARM64 机器码。编译器在构建时完成所有繁重的工作,因此运行时开销为零。”

这才是真正的答案。这也是 KMP 如此强大的原因。

原文链接:https://proandroiddev.com/how-does-the-same-kotlin-code-run-on-both-android-and-ios-the-compiler-magic-behind-kmp-b4450f87f7a8

最新文章

随机文章