
说出来你可能不信我第一次在简历上写“熟悉Kotlin Multiplatform”的时候心里其实挺虚的。当时只是拿一个内部工具类项目练过手连expect/actual的坑都没踩全。但正是那段“半懂不懂”的阶段让我在后续的面试里被追问最多、也收获最多。后来我陆续在几个跨平台项目里把共享逻辑层真正落地又作为面试官看过不少候选人对KMP的理解越来越觉得Kotlin Multiplatform以下简称KMP不是那种“会写几行代码”就能糊弄过去的技术它背后牵扯到编译模型、平台差异、构建体系、团队协作等一系列问题。这篇文章我不打算给你念官方文档而是结合我自己的实操经历和面试别人的经验把KMP的技术实践拆开揉碎再附上一份可以直接用的安卓开发面试应答思路。不管你是刚接触KMP、准备在项目里落地还是正在准备面试想把这个亮点讲清楚这篇内容应该都能帮到你。1. 先搞明白KMP到底解决了什么问题1.1 它不是“又一个跨平台框架”而是一套代码共享策略很多初学者容易把KMP和Flutter、React Native放一起比较这个方向一开始就偏了。Flutter和RN是UI跨平台重点在界面层KMP解决的是业务逻辑层的共享UI部分你仍然可以用原生写法也可以用Compose Multiplatform。换句话说KMP更像是一种代码组织方式而不是一个“应用壳”。我自己的理解是KMP把“共享”的边界划在了平台无关的逻辑层。比如网络请求、数据解析、本地存储、状态管理、权限校验的纯逻辑部分这些都可以写一份。而凡是涉及到平台API的地方比如获取设备信息、调用系统相册、读写剪贴板就用expect声明一个“期待接口”在Android/iOS各自用actual实现。这种设计的好处是你不用像Flutter那样整个UI都被框架绑死原生能力和性能不受影响也不需要像RN那样引入一个巨大的JS运行时包体积和启动性能都可控。坏处也很明显你得懂至少两端的平台差异否则actual实现会写得很痛苦。1.2 面试官问KMP到底在问什么我在面试候选人的时候如果简历上写了KMP我通常会先问一句“你觉得KMP和写纯Android相比最大的心智负担在哪里”这个问题能筛掉很大一批人。只背过概念的人会回答“多了一套actual实现”或者“构建配置复杂”。但真正实践过的人会告诉你最大的心智负担是你在写共享代码时时刻要想着“这段代码将来要在别的平台跑”然后你就会发现很多你在Android上觉得很自然的写法在KMP里根本没法直接用。比如java.text.SimpleDateFormatAndroid上有但KMP的common代码里不能直接用比如SharedPreferencesAndroid上有但KMP里你要自己封装或者用第三方库。面试官真正想确认的是你有没有经历过这种“从Android思维切换到多平台思维”的转变。这个转变不是看两篇文档就有的必须靠实践。1.3 KMP适合什么样的项目根据我个人的观察KMP最适合两类场景。第一类是业务逻辑复杂、但平台特性不重的App。比如一个记账软件、一个工具类App核心是数据模型、计算规则、网络同步界面大部分是表单和列表这种项目用KMP能省掉至少30%的逻辑重复代码。第二类是团队里既有Android又有iOS开发、但彼此独立迭代吃力的场景。表面上看KMP引入了一套新的工程结构实际上它让两端开发在数据模型层和网络层达成了强一致避免出现“Android端的金额是BigDecimal、iOS端是Double”这种长期遗留问题。反过来如果你的App重度依赖系统能力比如AR、后台定位、深度相机调优或者团队没有人维护iOS端那KMP的优势就体现不出来反而会拖慢节奏。这一点在面试里也很加分说明你不只是会写还会判断。2. KMP核心技术点逐一拆解2.1 expect/actual的真实使用体验expect/actual是KMP最核心的语法没有之一。它做的事情很简单在commonMain里声明一个接口或者函数然后每个平台各自实现。但实际用起来有几个细节不太容易被文档讲清楚。第一个细节是actual实现的可见性。Kotlin 1.9.20之前actual实现必须是public的而且不能有默认参数值。1.9.20之后放宽了一些限制但我在实际项目中仍然习惯保持两边签名完全一致包括参数名称、默认值、可见性修饰符这样可以避免踩到编译器的坑。第二个细节是expect并不一定只能修饰函数它还可以修饰类、属性、注解、枚举。比如你想在common代码里使用一个带默认参数的枚举但不同平台对这个枚举的序列化方式不同你就可以把枚举也expect化。我的经验是expect的使用范围能小就小能用一个函数解决的不要expect一个类因为类级别的actual实现容易造成平台代码侵入共享逻辑。expect fun getDeviceId(): String actual fun getDeviceId(): String { return Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID ) }这是最基础的写法。但实际项目里我更多的做法是结合依赖注入把getDeviceId定义成接口在平台侧注入实现这样common代码完全不依赖Android的Context测试也更方便。2.2 编译目标的差异与选择KMP支持编译到多种目标Android、iOS通过Kotlin/Native、JS、Windows、Linux等。对于移动端团队来说最关心的就是Android和iOS两个目标怎么配置。Android端走的是标准JVM字节码链路你可以把共享模块当作一个普通library来依赖。iOS端则不同Kotlin/Native会把共享代码编译成一个静态库.framework然后由Xcode工程引用。这就带来一个直接的问题你的共享代码里不能有Java反射、不能直接用JVM的类库连kotlinx.coroutines在Native端的默认调度器行为都和JVM端不一样。我在配置iOS支持时踩过最大的坑是本地开发时Android侧的编译很快但到了iOS侧每改一次common代码都要重新生成framework并嵌入Xcode工程。如果不加缓存和脚本自动化这个流程会让人崩溃到想放弃KMP。后来我写了一个Gradle脚本把打包framework和拷贝到iOS工程目录的步骤串联起来才把一次改动的验证时间从十几分钟压缩到三分钟左右。2.3 共享代码的依赖管理KMP里的依赖管理比普通Android项目复杂因为你要给不同平台指定不同的依赖变体。最常见的做法是用third-party库和kotlinx库它们已经在多平台适配方面做得很好了。kotlin { androidTarget() iosX64() iosArm64() iosSimulatorArm64() sourceSets { commonMain.dependencies { implementation(io.ktor:ktor-client-core:2.3.0) implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3) } androidMain.dependencies { implementation(io.ktor:ktor-client-okhttp:2.3.0) } iosMain.dependencies { implementation(io.ktor:ktor-client-darwin:2.3.0) } } }这个配置里有一个容易被忽略的点iosMain指的其实是iosX64、iosArm64、iosSimulatorArm64三个目标共享的源码集。类似地你可以定义iosTest、androidUnitTest这种测试源码集。搞清楚源码集的层级关系比记住一堆库的坐标更有用。我个人在选择KMP第三方库时有个原则优先选kotlinx官方库coroutines、serialization、datetime其次选KMP社区维护活跃的库Ktor、SQLDelight、moko-resources等尽量不自己去封装平台独立但API复杂的功能因为维护成本真的很高。2.4 状态管理与数据存储的跨平台思路很多KMP初学者一上来就在common里写ViewModel和数据库我一般会劝一句别急先把数据流打通再说。KMP的common代码里可以写状态管理比如用StateFlow管理页面状态在Android侧和iOS侧各自收集。但要注意iOS侧没有LiveData它在KMP里的对应物就是Flow。iOS开发通常不会像Android那样写“订阅式”代码SwiftUI更习惯用ObservableObjectPublished所以你在共享代码里暴露Flow之后iOS侧往往要再做一层桥接。数据存储方面优先级从高到低是SQLDelight支持KMP的SQLite、DataStore官方已支持KMP、直接封装平台端的SharedPreferences/NSUserDefaults。我的实际体验是如果数据结构不复杂优先用DataStore它天然支持Flow序列化用kotlinx.serialization一套代码两端都能跑。如果涉及复杂查询和关系数据SQLDelight更合适它甚至支持在编译期验证SQL语句。我在其中一个项目里就是先用了SQLDelight建了三张表后来又因为需求变化迁移到了DataStore过程倒不算痛苦但确实暴露了一个问题KMP生态的库版本更新很快API兼容性不像AndroidX那样稳定。所以最佳实践是把读写操作封装在Repository里底层方案可以替换上层业务不要感知。3. KMP项目落地的完整实操记录3.1 从零搭建一个KMP共享模块我拿一个实际做过的“跨平台日程管理App”来复盘这个项目核心里程碑是Android和iOS共用一套数据模型、网络层和日程生成逻辑两端加起来的共享代码占比达到60%左右。新建工程时我建议直接使用Kotlin Multiplatform WizardIntelliJ插件或在线生成器不要手动建目录。它生成的模板已经配置好了androidTarget、iosX64等目标还带了一个简单的expect/actual示例能够帮你快速验证编译链是否通畅。生成完工程后第一步不是在common里写业务而是先确认三件事Android端能否正常编译并引用共享模块iOS framework能否打出来并被Xcode识别common里的单元测试能否Run起来。三件确认齐了再开始写代码。不然等你写了一大堆逻辑之后发现某个平台编不过排查成本会翻倍。3.2 共享模块与业务层的边界划分在开始写业务之前我和搭档先花了一整天画“共享边界图”。我们把日程管理App的功能逐条列出来然后分成三类必须共享数据模型、时间计算、日程规则判断、网络API、可以共享但要桥接登录态存储、日期格式化、日志、不能共享推送注册、系统日历交互、快捷方式。这个过程非常关键。它决定了你后续写的每行代码到底放在commonMain还是androidMain/iosMain。我见过很多失败项目就是因为边界没划清最后所有代码都堆在common里platform的actual越写越大共享的意义就没了。边界划分的一个实用技巧是从“测试难度”倒推。如果这段逻辑在Android和iOS上需要写两套单元测试才能验证那就考虑放平台侧如果一套测试就能验证放common。比如日程规律校验每周一、每月最后一天、法定节假日跳过之类这就是典型的可以共享且适合共享的逻辑。3.4 iOS集成那点事iOS侧集成共享framework是整个落地过程中最容易卡住的环节。我这里分享一个我花了两个星期才稳定下来的流程。通用做法是在shared模块的build.gradle.kts里增加一个自定义任务比如linkDebugFrameworkIosSimulatorArm64产出模拟器framework再通过一个embedAndSignAppleFrameworkForXcode任务在Xcode构建时自动拉取framework并签名。这个方案能绕开手动拷贝的烦琐流程但不能说没有任何版本兼容风险。注意Kotlin/Gradle和Xcode的版本组合哪怕差一个小版本都可能导致framework产物格式变化。我踩过一次升级Xcode后替代旧frameworkApp启动直接崩在动态库加载阶段排查了一整天才定位到是版本错配。我后来干脆换了一种更稳的方式Windows和Linux跨平台团队协同时用一个专门机器编译framework把产物上传到内部服务器iOS同事直接拉取固定版本就行。虽然听起来“土”但反而规避了很多本地环境差异带来的玄学问题。3.5 网络层与数据序列化的统一实现网络层我推荐用Ktor Client它在KMP里支持度最好而且引擎可替换Android用OkHttp、iOS用Darwin。数据序列化用kotlinx.serialization配合Ktor的ContentNegotiation插件使用。// 共享模块中的API定义示例 Serializable data class ScheduleItem( val id: String, val title: String, val startTime: Long, val endTime: Long, val rule: RepeatRule? null, ) class ScheduleApi(private val client: HttpClient) { suspend fun fetchWeekSchedule(startOfWeek: Long): ListScheduleItem { return client.get(https://api.example.com/schedules/week) { parameter(start, startOfWeek) }.body() } }这段代码两端通用。但真正的坑不在请求本身而在异常处理。Ktor在不同平台上的异常类型不完全一致比如超时异常在Android上是SocketTimeoutException在iOS上可能是DarwinHttpException包裹过的。所以我在共享层写了一个统一异常转换函数把所有异常映射成业务错误码两端UI只管错误码不接触底层异常。这一层设计好了之后两端网络层的代码重复度几乎为零联调效率提升明显。你如果去面试能讲清楚这个异常转换的思路面试官会认为你确实上过生产环境。4. 安卓开发工程师面试中的KMP应答策略4.1 从“我会用”到“我会评估”如果你的简历里只写“熟悉Kotlin Multiplatform”面试官大概率会问一些很基础但很致命的问题比如“KMP和Flutter怎么选”。这个问题表面上是问技术选型实际上是想看你的决策模型。我的建议是分三层来回答第一层说清楚KMP的定位是逻辑共享而非UI共享第二层给出对比维度团队构成、性能敏感度、UI复杂度、包体积约束第三层落到自己的项目经验说“在我们的场景里选择KMP是因为……最终达到……效果”。面试官听到一个能拿出清晰选型逻辑的候选人通常会认为你在设计层面有话语权这比单纯会敲代码更值钱。4.2 高频KMP面试题目清单结合我自己被面和面别人的经历整理了一组出现频率极高的问题expect/actual的实现机制是什么编译器如何处理两端的匹配Kotlin/Native是如何做到直接在iOS上运行的它和Objective-C的互操作原理是什么共享代码中能否使用反射为什么Ktor Client在Android和iOS上的网络引擎分别是怎样的如果不设置引擎会怎样Flow在KMP中的跨平台特性和平台差异点有哪些这些问题覆盖了“理解编译模型”“理解平台差异”“理解生态用法”三个层次。只背答案不实践的候选人第二层和第三层基本一问就露馅。4.3 面试中的项目描述话术“熟悉KMP”是形容词面试官不感冒。你要换成项目经历“我在某跨平台项目中负责搭建共享模块将数据层和业务规则层抽离为common代码Android/iOS复用率达到60%同时通过Ktor统一了网络栈并设计了跨平台异常映射体系。”这套话术里没有一句虚的每一句都对应一个可追问的点。当面试官往深了追问“复用率怎么统计的”“异常映射怎么设计的”“keyboard问题怎么办”时你都有具体经历可以回答。提示面试官如果主动提到了Compose Multiplatform建议你诚实地区分“我了解Compose MP的现状”和“我用了KMP但UI层是原生实现”。不要为了追热点把没做过的说成做过的编造的细节经不住技术追问。4.4 薪资谈判阶段如何把KMP价值放大这点很现实。KMP在你的薪资谈判里是一个加分项但单纯“会一门技术”加不了多少分要让面试官看到你通过KMP解决了一个实际业务痛点。你可以这样总结“我们团队通过引入KMP使两个平台的需求排期从双倍人力缩至单倍开发少量桥接数据模型不一致问题减少线上崩溃率下降了一个百分点。”有量化结果的技术亮点比“熟悉KMP”四个字值钱得多。5. 实战踩坑记录与问题排查速查表5.1 实际项目中最容易出问题的五个环节从我的真实经历看KMP项目的坑往往不在KMP本身而在工程化细节。这里列个排查表问题现象常见原因排查方向iOS编译报“Undefined symbols”framework没有包含对应架构检查目标架构与framework架构是否匹配Android编译通过但iOS编不过common代码里使用了JVM类库检查代码中是否有java.*隐式引用启动时ClassNotFoundExceptionAndroid混淆配置未更新检查R8规则keep共享模块类iOS偶发崩溃且堆栈不可读Kotlin/Native与Xcode版本错配统一Kotlin和Xcode版本重新生成framework序列化字段新增后iOS兼容异常kotlinx.serialization版本不一致锁定两端依赖版本避免浮动版本号这张表是浓缩版。如果你刚接触KMP建议把它贴在工位上至少能帮你少走一半弯路。5.2 关于协程与Flow在跨平台下的调度差异协程在KMP里一个比较隐蔽的点是默认调度器在不同平台上的实现不一样。在Android上Dispatchers.Main绑定的是Android主线程在iOS上它绑定的是主队列main queue。看起来差不多但如果你在处理后台任务时习惯用Dispatchers.IO要注意它在Native端的线程模型与JVM差异较大。我的经验是在common代码里尽量少指定具体Dispatcher而是通过依赖注入传入。这样测试的时候可以替换成UnconfinedTestDispatcher线上也可以按平台特性选择。这个技巧虽然不是KMP专属但在KMP里尤其重要因为平台差异被放大了。5.3 避免“共享地狱”的架构约束KMP项目很容易走上另一个极端为了让所有逻辑都能共享把UI相关状态也往common里塞结果越写越臃肿最终Common模块里有一堆需要与平台频繁交互的代码每一次改动都牵动两端。我当时立了一个约束common模块只允许暴露纯业务接口所有平台类型禁止出现在common API签名中。比如函数参数不能直接传Context不能返回Bitmap不能返回UIViewController。这个约束一开始会觉得很啰嗦但坚持一段时间后会发现工程结构非常清爽两端代码互相不“穿透”协作效率明显提升。KMP有没有成为所谓“终极跨平台方案”从我目前的实践看它更像是把“共享什么”和“不共享什么”的选择权交还给了开发者。这种自由度是好事但也意味着团队得有自己的判断力和工程纪律。6. 给不同阶段开发者的落地建议6.1 初级开发者先从小模块练手如果你刚开始接触KMP不建议直接在公司项目里引入。我的建议是找一个你熟悉的已有库或者工具类比如一个日期计算库、一个规则解析器把它用KMP重写一遍配上commonTest单元测试把它当作一次“纯逻辑重构”的练习。这个阶段的关键是理解“一份代码编译到多个目标”的基本流程不要求UI集成。6.2 中级开发者在中小项目里实践共享数据层当你已经能熟练配置构建目标、处理依赖变体后可以尝试在一个中小型项目的网络层和数据层引入KMP。重点体会Ktor、SQLDelight、Serialization这些库的实际使用并尝试制定共享边界规范。这个阶段你会遇到最多的坑也是成长最快的阶段。6.3 高级开发者思考团队协作与模块化拆分高级开发者需要关注的不是“KMP怎么用”而是“KMP怎么让团队更高效”。这包括把共享模块像组件一样单独建仓、设计团队内部的KMP规范、建立一个稳定的framework发布流水线、设计面向业务的错误码体系。这些工作没有现成答案考验的是架构能力和工程化管理能力。我自己在实践KMP过程中的体会是这个技术不会让人一劳永逸但它确实能训练出一种“多平台思维”——你在写每一行逻辑时都会多问一句这段代码在另一个平台上还能不能成立这个习惯反过来会让你在Android开发里也写出更干净、更少平台依赖的代码。对于安卓开发者来说掌握了KMP这个视角面试和实际工作视野都会开阔不少。