KMP 实战:Android 开发如何快速统一双端 IM 模块

写在前面

大家好,我是三雒(luo)。

熟悉我的朋友应该知道,我长期深耕 Android 基础技术,虽然做过架构相关的工作,但以宏观的工程架构设计为主,真正落地业务架构重构还是头一次。所以这次 IM 模块跨端重构项目,对我而言完全是一次「新手开荒」------第一次深入 IM 业务、第一次落地 KMP 跨端、第一次开发 iOS 端。

本次改造工期非常紧张,要赶版本上线。所以我全程秉持「先跑通、再优化」的思路,没有在 SDK 架构封装、底层设计上做过度打磨,所有非刚需的精细化工作都预留到了二期迭代。而身为一个「处处都是第一次」的人,是怎么借助 AI------用 6 天完成 Android 侧的代码下沉与 KMP 改造、再用 8 天把 iOS 端接入并提测的,其中有哪些 AI 使用经验、踩了哪些坑,正是本文想分享的重点。

好好的两套代码,为什么要合成一套

IM 是我们 App 的核心链路,会话列表、消息收发、长连接保活、本地数据缓存、Token 刷新、风控校验等全流程能力,此前都是 Android、iOS 各自实现、独立维护,两端团队当初根据各自的开发习惯,选用了完全不同的技术栈:

  • Android 端:Room 本地缓存 + Retrofit 网络请求 + OkHttp WebSocket 长连接

  • iOS 端:WCDB(SQLite C++封装)本地存储 + Alamofire 网络框架 + SRWebSocket 长连接

单独看任意一端的代码实现、技术选型,都没有明显问题,真正的痛点在于「同一套业务,双重维护成本」。随着迭代次数增加,各类问题层出不穷,严重影响开发效率和用户体验:

  • 双端行为漂移:未读数统计、消息去重、风控重试等通用业务规则,两端代码长期独立迭代,逻辑逐渐分化,QA 频繁测出「Android 表现正常、iOS 异常」的差异化问题。

  • 重复劳动翻倍:后端协议新增字段、功能迭代、线上 bug 修复,都需要在双端分别开发、自测、提审,所有开发流程全部重复一遍,人力成本大幅浪费。

核心目标 :将 Android 主工程内的 IM 核心逻辑完全下沉至 libs/im 跨端库,并在iOS端接入,实现双端共用一套核心业务代码。改造完成后,协议升级、Bug 修复、新功能迭代,相关的业务逻辑仅需维护一份代码即可。

同时我明确了一期改造的边界:优先完成底层实现的统一,主要包括数据库,网络和长链接实现和协议,功能跑通并删除 iOS 旧代码。不大动上层的业务逻辑,SDK API更加合理抽象和设计全部预留至二期改造。

一期改造改造工作主要分为两大阶段:

Plain 复制代码
                          ┌─────────────────────┐
                          │   libs/im (KMP)     │
                          │   commonMain        │
                          └──────────┬──────────┘
                                     │
              ┌──────────────────────┼────────────────────────┐
              │ 阶段一:Android 下沉    │     阶段二:iOS 接入     │
              ▼                                                ▼
       Android 主工程                                    iOS Foundation
       把 IM 核心搬出来                                    删掉自写的旧栈
       Java/Kotlin 改 KMP friendly                       接入 KMP API
       框架替换 (Retrofit→Ktorfit 等)                     数据迁移 WCDB → Room
       设计统一的 SDK 对外接口                              实现平台 Config 注入

阶段一是「搬运 + 改造」,最终交付一个真正跨平台、对外可用的 IM SDK;阶段二是「消费 + 替换」,把 iOS 旧栈彻底退役。下面分两个阶段细讲。

阶段一:把 Android 的 IM 核心搬进 KMP

这远不止「搬代码」这么简单,Android 原来的 IM 代码实现里塞满了 JVM/Android 特有的东西------Gson、Handler、LiveData、Retrofit,这些在 commonMain 里一个都用不了。真正费劲的是把它们换成 KMP 能用的对应物,顺带把 SDK 对外的 API 收拾成一个双端都好用的样子。

架构设计:长连接 / HTTP / 数据库三块怎么改

搬代码之前先得想清楚整套 SDK 的架构------哪些是 IM 核心(要进 commonMain)、哪些是平台特异(留给平台 actual)、对外暴露什么 API。先放一张总图:

Plain 复制代码
                          ┌────────────────────────────────────────────┐
                          │              业务调用方                      │
                          │   (Android ViewModel / iOS Manager)        │
                          └─────────────────┬──────────────────────────┘
                                            │
                                            ▼
   ┌──────────────────────────────────────────────────────────────────────┐
   │                        IM SDK 对外 API                                │
   │                                                                      │
   │   IMSDK.init { ... }            ← 启动入口,注入平台 Config              │
   │   IMAPIManager                  ← HTTP 业务接口                        │
   │   IMWebSocketManager            ← 长连接管理 + 消息收发                  │
   │   IMDatabaseManager.{user,conv,msg,session}Repo  ← 本地持久化          │
   │   IMSendMessageManager          ← 发消息流水线 (WS/HTTP 路由+风控重试)    │
   └────────────────────────┬─────────────────────────────────────────────┘
                            │
       ┌────────────────────┼─────────────────────┐
       ▼                    ▼                     ▼
   ┌────────┐          ┌────────┐            ┌────────┐
   │  HTTP  │          │   WS   │            │   DB   │
   │ Ktorfit│          │ Ktor WS│            │  Room  │
   └───┬────┘          └───┬────┘            └───┬────┘
       │                   │                     │
       └─────────── expect / actual 平台引擎 ──────┘
                            │
            ┌───────────────┴───────────────┐
            ▼                               ▼
    Android (androidMain)              iOS (iosMain)
    OkHttp engine                      Darwin engine (HTTP/WS)
    AndroidSQLiteDriver                NativeSQLiteDriver (Apple sqlite)

下面长连接、HTTP、数据库三块分别说。

长连接:OkHttp WebSocket → Ktor WebSocket

旧实现是 OkHttp WebSocket 直连,外面包了个 WsManager 状态机管重连退避、心跳、401,再用 LiveData 把连接状态丢给业务。这套搬到 KMP 一行都过不去:

  • OkHttp 属于 JVM 专属引擎,无法在 KMP 通用层 commonMain 使用

  • HandlerThreadAtomicIntegersynchronized 等 Java 并发原语,无跨端兼容实现

  • LiveData 是 Android 框架专属组件,不支持跨平台调用

换成 Ktor WebSockets------它本身就跨平台:Android 底层还走 OkHttp 引擎(原来的拦截器、鉴权、风控全留着),iOS 走 Darwin 引擎(系统的 NSURLSessionWebSocketTask)。引擎差异用 expect/actual 隔开,commonMain 里是干净的一套逻辑:

Kotlin 复制代码
// commonMain
internal expect object WebSocketClientFactory {
    fun createWebSocketClient(headersProvider: WebSocketHeadersProvider): HttpClient
}

// androidMain
internal actual object WebSocketClientFactory {
    actual fun createWebSocketClient(...) = HttpClient(OkHttp) { ... }
}

// iosMain
internal actual object WebSocketClientFactory {
    actual fun createWebSocketClient(...) = HttpClient(Darwin) { ... }
}

Java 并发原语和 LiveData 全换成协程和 StateFlow。顺手把回调接口也拆了:原来「消息回调」和「连接生命周期回调」挤在一个接口里,其实是两码事------拆成 ChatMessageHandler(管消息路由)和 ConnectionListener(管连接状态),各实现各的,清爽多了。

HTTP 请求:Retrofit → Ktorfit

Retrofit 也是 JVM-only,KMP 用不了。换成 Ktorfit------注解写法几乎和 Retrofit 一样,底层是 Ktor,接口平移过来基本不费劲。

Kotlin 复制代码
// Retrofit (Android only)
interface ChatApiService {
    @POST("/chat/conversations")
    suspend fun fetchConversations(@Body req: FetchRequest): Response<ConversationsData>
}

// Ktorfit (KMP)
interface ChatApiService {
    @POST("chat/conversations")
    suspend fun fetchConversations(@Body req: FetchRequest): BaseResponse<ConversationsData>
}

Ktor的HttpClient 同样用 expect/actual 来分平台实现:Android端用OkHttp Engine, 直接复用业务方的 OkHttp 实例,鉴权、风控拦截器原样生效;iOS 后面用自定义 Engine 桥接回原生的 Alamofire 网络栈,也不再关心具体的鉴权、风控等逻辑。

数据库:Room 进 commonMain

数据库这块改造量最大。Android 早就用 Room,但初始化跟 Context 绑死,而 commonMain 里没有 Context。好在 Room 2.7+ 官方支持 KMP,初始化路径用 expect/actual 按平台分开就行:

Kotlin 复制代码
// commonMain
internal expect object AppDatabasePlatform {
    fun createBuilder(databaseName: String): RoomDatabase.Builder<AppDatabase>
}

// androidMain
internal actual object AppDatabasePlatform {
    actual fun createBuilder(databaseName: String): RoomDatabase.Builder<AppDatabase> {
        val context = SDKSupportLibrary.get().applicationContext
        val dbFile = context.getDatabasePath(databaseName)
        return Room.databaseBuilder<AppDatabase>(context, name = dbFile.absolutePath)
    }
}

// iosMain
internal actual object AppDatabasePlatform {
    actual fun createBuilder(databaseName: String): RoomDatabase.Builder<AppDatabase> {
        val dbFile = NSFileManager.defaultManager.URLForDirectory(...)
        return Room.databaseBuilder<AppDatabase>(name = dbFile.absoluteString)
            .setDriver(NativeSQLiteDriver())
    }
}

4 张表(user、conversation、message、conversations_property)连同 DAO、Entity 全部搬进 commonMain,两端共用一套表结构和查询。

对外不暴露 DAO,按业务粒度包了一层 Repository:

Kotlin 复制代码
object IMDatabaseManager {
    val userRepo: UserDatabaseRepo
    val conversationRepo: ConversationDatabaseRepo
    val messageRepo: MessageDatabaseRepo
    ...
}

SDK 对外入口:收敛到 IMSDK.init

IM 模块需要外部注入的能力,按业务维度抽成几个配置接口,在初始化时一次性注入:

Kotlin 复制代码
IMSDK.init {
    logger = LoggerImpl                    // 日志输出
    applicationContext = context             // Android-only 资源对象
    okHttpClient = businessOkHttpClient      // 复用业务方的 OkHttp 实例
    
    networkConfig = NetworkConfigImpl        // base URL 提供方
    webSocketConfig = WebSocketConfigImpl    // 401 处理 / 握手 header / 前后台 / 网络监听
    sendMessageConfig = SendMessageConfigImpl
    databaseConfig = DatabaseConfigImpl      // userId / env tag
}

就两条原则:

  1. 平台相关直接当字段塞okHttpClientapplicationContext 这类平台对象,业务方持有、注入即可,SDK 不碰平台差异;
  2. 行为契约反向注入:401 怎么处理、前后台怎么判断、网络监听怎么订阅,SDK 只定接口、让业务方实现,方便 mock 也给业务留了定制空间。 这套结构 iOS 接入时直接复用------同样几个 Config 和 Handler,Swift 端实现一遍就行。

几个值得关注的问题

序列化:Gson 这位老朋友得走了

Gson 是 JVM-only,得换成 kotlinx.serialization。但项目里上百个 Model 用着 Gson 的 @SerializedName,调用也散得到处都是,我们只换IM相关的,但万一还有用Gson解析相关类的也要保证能正常工作,于是走了个双轨过渡

Kotlin 复制代码
// model 同时适配双序列化注解
@Serializable
data class UserModel(
    @SerialName("user_id")
    val userId: String
)

// 全局 Gson 工具适配 KMP 序列化注解
class SerialNameFieldNamingStrategy : FieldNamingStrategy {
    override fun translateName(field: Field): String {
        return field.getAnnotation(SerialName::class.java)?.value
            ?: field.name
    }
}

GsonUtils 全局装上这个策略后,老的 Gson 代码原地不动照样能跑,新代码逐步迁到 Json.decodeFromString / encodeToString,相当于在Android侧这些Model既能被Gson解析也能被KMP的 Json解析。

Java 并发原语换 KMP 等价物

Java/Android 那套并发原语和系统 API,commonMain 里一个都没有,得一个个换。这活最机械、也最容易漏,我索性整理成一张对照表:

Java/Android 原生 KMP commonMain 跨端等价物(统一复用)
HandlerThread + Handler.postDelayed CoroutineScope + delay 协程延时任务,全平台通用
AtomicInteger / AtomicLong / AtomicBoolean atomicfu.atomic 原子属性,适配双端并发安全
synchronized (lock) {...} atomicfu.locks.synchronized / Mutex 跨端锁机制
ConcurrentHashMap 自定义 ConcurrentMap 抽象:Android typealiasConcurrentHashMap、iOS 用 mutableMapOf + NSRecursiveLock 加锁
ConcurrentLinkedQueue SynchronizedObject 包装 ArrayDeque,替代并发队列
System.currentTimeMillis() Clock.System.now().toEpochMilliseconds() 跨端时间戳
@Volatile @kotlin.concurrent.Volatile 跨端 volatile 关键字
java.util.concurrent.CancellationException kotlinx.coroutines.CancellationException 协程专属取消异常
Dispatchers.Main.immediate Dispatchers.Main(K/N 无 immediate 变体,统一兼容)
LiveData<T> MutableStateFlow<T>/StateFlow<T> 跨端状态监听
Looper.getMainLooper() / runOnUiThread withContext(Dispatchers.Main) 跨端主线程调度

其中 LiveData → Flow 影响最大,靠 asLiveData() 扩展兼容了老 ViewModel,其余都是机械替换。

Ktor层 Websocket 默认缓冲太小,连续发丢消息

这是整个下沉里唯一在回归测试才发现的坑,Ktor 3.3.3 的 OkHttp 引擎下,WebSocket 出站通道是硬编码的小缓冲,全局缓冲配置还不生效。弱网下连发几条消息就容易把缓冲装满、发送失败,降级到HTTP。只能升级到 Ktor 3.4.1 ------这个版本支持引擎级通道配置,用 WebSocket 的 channels DSL 把出站缓冲显式调大;

Kotlin 复制代码
@OptIn(InternalAPI::class)
internal fun HttpClientConfig<*>.installImWebSockets() {
    install(WebSockets) {
        pingIntervalMillis = 0L  // 业务层自主实现心跳,规避引擎心跳兼容问题
        channels {
            outgoing = bounded(capacity = 256, onOverflow = ChannelOverflow.SUSPEND)
        }
    }
}

阶段二:iOS接入KMP,删掉旧栈

Android 这边的 KMP 模块跑稳之后,就到了iOS这边接入了。为了让AI接入的过程可控,我把它拆成初始化、数据库、HTTP、WebSocket 四个子阶段,一步步来。

接入子阶段

Plain 复制代码
┌───────────────────────────────────────────────────────┐
│ Phase 1 初始化                                           │
│   IMBootstrap.install() 注入 iOS 平台能力                   │
│   - Swift 实现 4 个 Config + 2 个 Handler                 │
│   - 启动 KMP IM SDK                                     │
├───────────────────────────────────────────────────────┤
│ Phase 2 数据库                                           │
│   WCDB (SQLite C++ wrapper)  →  KMP Room              │
│   - 5 张表整体接入                                          │
│   - WCDB → Room 一次性数据迁移                               │
├───────────────────────────────────────────────────────┤
│ Phase 3 HTTP                                          │
│   Alamofire + 自家 APIClient  →  KMP Ktorfit            │
│   - 通过 NativeBridgeEngine 把 KMP 网络请求                  │
│     桥回 iOS 原生网络栈(保留鉴权/风控 cookie)                      │
├───────────────────────────────────────────────────────┤
│ Phase 4 WebSocket                                     │
│   SRWebSocket (OC)  →  KMP Ktor WS (Darwin engine)    │
│   - 删除旧 OC WebSocketManager 主体 + 6 个 Category         │
│   - 业务切到 ChatMessageHandler / ConnectionListener      │
└───────────────────────────────────────────────────────┘

iOS 接入的关键技术问题

SKIE:抹平 Kotlin → Swift 的翻译腔

SKIE 是 KMP 生态里很实用的跨端桥接插件,核心能力是优化 Kotlin 与 Swift 的语法兼容------支持 Flow 转 AsyncSequence、密封类透明映射、挂起函数适配 Swift 异步语法,把 KMP 桥接常见的「翻译腔」抹平,让 iOS 调 KMP 接口的体验和调原生 Swift 基本一致。

iOS 走原生网络栈

iOS 这边让 KMP 发网络请求,其实有两种实现方式:

  • Darwin Engine:Ktor 官方提供的 iOS 引擎,底层走系统的 NSURLSession,开箱即用;
  • 自定义 Engine:自己实现一个 Ktor 引擎,把 KMP 的请求转回 iOS 原生的网络库。 我们选了后者。原因很直接:iOS 原生网络栈里沉淀了多年的鉴权、Cookie 管理、风控校验、Token 自动刷新等逻辑,如果走 Darwin 引擎,等于要在 KMP 层把这一整套重新实现一遍,实现成本和风险高,用自定义 Engine 把请求桥接回原生网络库之后,这些逻辑直接复用原来的。

数据迁移:字段映射 + 一次性扫表

iOS 用户在 WCDB 里已经攒了大量历史数据,接入 Room 时要做一次性迁移,不能丢也不能让用户感知。这里最麻烦的不是搬数据,而是两端的表结构和字段命名根本对不齐 ,当年是两拨人各自设计的,同一个业务字段,两边的命名、类型、甚至拆分方式都不一样。靠人逐字段比对4张表,既慢又容易出错。这块我基本交给AI分析,让它逐表逐字段拉一张映射表(WCDB 字段 → Room 字段、类型怎么转、要不要拆分或合并),生成迁移的映射关系写入设计方案中,人工Review确认完再据此生成迁移代码。

AI 协作

这次重构不管是 Android 侧的核心下沉,还是 iOS 侧的接入,AI 都深度参与了。整套协作模式可以概括成 Spec 先行 + 方案固化 + AI 落地 + 人工兜底------先把规则和方案写成文档,再让 AI 照着落地,人只盯边界和关键决策,下面分 Android、iOS 两块说。

Android 下沉:先划清IM 模块能力边界,再分步实施

Android 下沉的工作量主要在框架替换------Gson、Retrofit、Room、OkHttp WebSocket,还有一堆 Java 并发原语,全要换成 KMP 通用实现。我用 AI 的方式分两步走。

先划清这一期的能力边界: 一期时间紧,不可能什么都下沉。我让 AI 先把 IM 核心的依赖关系、调用方全梳理出来,再一起圈范围:哪些核心逻辑这期下沉、哪些上层业务先留在主工程不动、哪些精细化设计留到二期。边界由人拍板,AI 负责把全貌摆清楚,省掉自己一条条翻调用链的功夫。

再让 AI 分步骤落地: 边界定了,就按数据库 → HTTP → WebSocket 的顺序拆成一步步实施下沉。AI 按这个分解逐块改、逐块跑编译,我只在每步收尾时 review修改,再拍板下一步。

一个没写过 iOS 的人,是怎么把 iOS 接下来的

iOS 接入是整个项目里我最没底的部分------我一直写 Android,几乎没碰过 iOS,连开发环境也没有。按传统路子光入门 Swift / Xcode 就得一两个月,但靠一套标准化的 AI 协作流程,最后 8 天就完成了iOS接入、适配、提测。

关键是这套 Spec → Design → 实施 → 验证 的流程:

Plain 复制代码
       ┌──────────────┐
       │   spec.md    │  ← 人话定义业务需求、行为契约,无技术实现绑定
       │ (行为契约)    │
       └──────┬───────┘
              ↓
       ┌──────────────┐
       │ design-ios.md│  ← 技术方案落地:模块映射、适配规则、风险点
       │ (技术方案)    │
       └──────┬───────┘
              ↓
       ┌──────────────┐
       │   实施        │  ← AI 按方案落地编码,AI + 半人工Review
       │  (Claude)    │
       └──────┬───────┘
              ↓
       ┌──────────────┐
       │  test-plan   │  ← 输出标准化测试用例,保障业务内涉及的Case都能覆盖到
       └──────────────┘

核心是先定规则、再做落地 :我们先和AI把需求描述清楚让它生成spec文档,AI再根据spec生成具体的设计文档,最后我们再经过Review设计确保没有遗漏关键点。最终的效果就是用文档把「做什么」和「怎么做」前置写清楚,AI 编码就从「随机输出」变成「精准落地」,不会自由发挥跑偏。项目根目录的 AGENTS.md 还钉了几条硬约束,全程严格执行:

  1. 编码前必须读完对应的 Spec 与 Design;
  2. 方案没写明的逻辑不许猜,统一标记待补充;
  3. 不许私自加优化 / 冗余逻辑,严格按需;
  4. 双端实现严格对齐同一条业务规则。

整体的实施还是要分子阶段,按一块可独立的逻辑进行改造,独立编译和验证,以防止整体的代码量改动太大而失控,每个子阶段实施过程中AI可能还会有一些之前没考虑到的问题让我进行决策。最后改造完成之后可以再用AI进行风险Review,有没有实现语义和重构前不对齐的地方,根据代码改动生成适合QA阅读的测试用例,给QA参考测试范围。

AI 的能力边界

这次项目重构AI帮我干的活非常多,可以说我基本没有手写一行代码。从IM模块的业务逻辑摸底,方案设计调研,具体实现落地,双Agent Code Review、Bug排查修复、自动加日志抓日志、分析 crash 和 watchdog堆栈,自动编译和修复编译问题、Git相关的commit push、创建MR、解决代码冲突,基本都能完成。 我们目前真正比较缺失的一环是我们工程缺一套能让 AI 自动操作 UI 的 harness,AI 能写代码、能编译和自动安装、能加日志和读日志,但它没法自己把 App 跑起来、点进页面、看UI到底对不对。所以现在的循环还是:AI改完 → 我手动把UI点一遍 → 把现象、截图喂回去 → AI 根据现象和日志接着改。这个人工断点,是目前AI自动化过程中最大的瓶颈。等把这套 UI harness 补上,AI 才能「改完自己验、有问题自己接着改」,整个流程才算真正闭环------这也是我觉得下一步最值得投入的方向。

写在最后

整个重构做下来,回头看收获主要在IM本身、KMP工程,以及AI协作这三块。

IM 这块在AI的帮助下我把消息收发、长连接、数据设计存储的相关逻辑都捋了一遍,大概掌握了IM底层实现的核心知识。KMP 这块对于KMP常用的库包括Json解析、网络访问、数据库、并发原语、协程和Flow这些都有了解,第一次感受到双端代码复用的魅力。 AI协作 这块是我感触最深的,一个没写过 iOS 的人能这么快熟悉代码逻辑并接入IM SDK,这是以前想都不敢想的。这除了AI本身的强大之外,需要的是一套驾驭AI这个野马的能力和规范。

最后还想再感叹一下AI 的发展,2023年,在朋友圈发过一张用AI生成的动画版居庸关列车,感觉离写代码还很遥远。2024年底那会第一次上手Cursor,感觉它还挺笨,2025年中也就只是用它去进行局部的代码重构和优化。而到现在Claude已经强到能独立扛下一整块大的模块重构。短短两年多,AI 工具从「帮我生成张图」进化到了「替我扛下整块工程」,属于程序员的AI时代是真的来了。"风云再度变换,是时候做出选择了"。

相关推荐
码云骑士2 小时前
Android SWT重启问题
android
恋猫de小郭2 小时前
GSY 史上最全跨平台/架构/语言的项目,七大项目召唤「神龙」
android·前端·flutter
shuaiqinke2 小时前
【分享】一刻日记 富文本日记+图文混排+导出分享
android·craiyon
秋雨梧桐叶落莳2 小时前
iOS——抽屉视图详解
开发语言·macos·ui·ios·objective-c·cocoa
__Witheart__2 小时前
Android RK SDK只编译和烧录kernel(boot.img)
android
黄林晴2 小时前
Compose 键盘焦点别乱写!正确姿势只有这一种
android
刮风那天2 小时前
Android ActivityStarter 完整解析
android
liyunlong-java2 小时前
Android 跳转系统相册选取图片/视频/音频/文档(适配全版本权限)
android·gitee·音视频
q20609517102 小时前
文件上传漏洞攻防全解析
android