基于 JD 定制 · Kotlin + KMP (Kotlin Multiplatform) 方向 · 详细版
Kotlin Android KMP Coroutines Compose Multiplatform Jetpack
📋概述 & 备考策略
根据 JD 逆向推导面试重点,制定复习优先级
JD 关键词 → 面试考点映射
🎯 备考优先级建议
Kotlin 语言深度 — 这是 JD 第一要求,一定会深挖。重点:协程原理、空安全机制、内联函数原理、密封类 vs 枚举、集合惰性序列
KMP 原理与实践 — JD 第二要求,差异化考点。重点:expect/actual 编译机制、项目结构、跨平台库选型、与纯 Android 项目的集成
Android 基础 — 虽然不是重点突出,但 Android 岗必考。重点:生命周期、Handler 机制、RecyclerView、ViewModel/LiveData
架构与设计模式 — 体现"高效编码"能力。重点:MVVM、Clean Architecture、Repository 模式
软技能 — 用 STAR 法准备 3-5 个故事
K一、Kotlin 语言核心
JD 第一要求:"使用 Kotlin 语言进行高效编码" — 必考,需深度掌握
1.1 基础语法 & 空安全机制
QKotlin 的空安全机制是怎样的?中
Kotlin 在语言层面区分 nullable 和 non-null 类型:
编译期检查:
String不能为 null,String?可以为 null。编译器在编译期进行流分析,拒绝将 nullable 直接赋给 non-null!! 操作符:断言非空,如果为 null 则抛出
NullPointerException。生产代码应尽量避免? 操作符:安全调用,
a?.b?.c链中任一为 null 则整个表达式为 null?: Elvis 操作符:
a ?: "default",如果 a 为 null 则取默认值let 安全调用:
a?.let { ... }只在非空时执行块
// 安全调用链
val name: String? = user?.profile?.name ?: "Unknown"
// let 模式
nullableObj?.let { obj ->
// 只有非空才执行
doSomething(obj)
}
// 编译器智能转换
fun demo(s: String?) {
if (s != null) {
// 这里 s 被智能转换为 String(非空)
println(s.length)
}
}面试加分点:提到 Kotlin 的空安全是「编译期」而非运行期,且平台类型(platform type T!)来自 Java 互操作时没有空安全保证,需要显式处理。
Qvar vs val vs const val 的区别?易
注意:val 只保证引用不变,对象本身可能可变。val list = mutableListOf() 仍然可以 add/remove。
Qdata class 的原理和限制?中
data class 自动生成以下方法(基于主构造函数属性):
equals()/hashCode()— 基于所有主构造函数属性toString()— 格式ClassName(prop1=value1, prop2=value2)copy()— 浅拷贝,可指定属性覆盖componentN()— 解构声明支持
限制:
主构造函数至少 1 个参数
参数必须用
val或var标记不能是 abstract / open / sealed / inner
继承其他类时,自动生成的方法只考虑主构造函数属性
1.2 高级特性
Q扩展函数的原理是什么?会真的修改原类吗?中
不会修改原类。扩展函数在编译期被解析为静态方法调用,接收者作为第一个参数传入。
// Kotlin 源码
fun String.lastChar(): Char = this[this.length - 1]
// 编译后的等价 Java 代码
public static final char lastChar(String receiver) {
return receiver.charAt(receiver.length() - 1);
}关键特性:
静态解析:调用哪个扩展函数取决于「编译时类型」,不是运行时类型 → 不支持多态
可为空接收者:
fun String?.orEmpty()可以对 null 调用成员函数优先:如果类本身有同名成员函数,成员函数优先
扩展属性:也可以定义扩展属性,但不能有 backing field
面试坑点:扩展函数如果定义在 internal 或不同包下可能产生冲突,需用 @JvmName 消歧。
Qsealed class(密封类)vs enum vs abstract class?中
// 密封类经典用途:UI 状态 / Result 模式
sealed class UiState {
object Loading : UiState()
data class Success(val data: T) : UiState()
data class Error(val message: String) : UiState()
}
// when 穷尽检查,不需要 else
fun handle(state: UiState) = when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showData(state.data)
is UiState.Error -> showError(state.message)
}面试加分:密封类在 KMP 中尤其重要,是跨平台共享业务逻辑建模的核心手段。Kotlin 1.5+ 允许密封类子类在同一模块的不同文件中定义。
Qinline 内联函数的原理和作用?reified 是什么?难
inline 修饰的函数,编译器会将函数体直接「复制粘贴」到调用处,消除函数调用开销和 lambda 创建开销。
核心作用:
性能:消除高阶函数中 lambda 的对象创建开销
非局部返回:内联函数的 lambda 参数可以使用
return直接返回外层函数reified 泛型:内联函数可以用
reified在运行期访问泛型类型(普通泛型有类型擦除)
// reified 类型实化 — 运行期可访问泛型类型
inline fun Gson.fromJson(json: String): T =
fromJson(json, T::class.java)
// 调用时无需传 Class 对象
val user: User = gson.fromJson(json)
// ↑ 泛型类型在编译期被内联替换注意:过度使用 inline 会增大字节码体积。noinline 可标记某个 lambda 参数不内联,crossinline 用于不能非局部返回但需内联的场景。
Q委托属性(delegated property)有哪些?原理是什么?难
Kotlin 委托属性语法:val x by Delegate(),将属性的 get/set 委托给另一个对象。
标准委托:
lazy()— 懒加载,线程安全(默认LazyThreadSafetyMode.SYNCHRONIZED)Delegates.observable()— 属性变化监听Delegates.vetoable()— 属性变化前拦截(可拒绝赋值)Delegates.notNull()— 延迟初始化,使用前必须赋值
// lazy 懒加载
val expensiveObj: HeavyObject by lazy {
HeavyObject() // 第一次访问时才初始化
}
// 自定义委托 — 实现 getValue/setValue 操作符
class PrefDelegate(val key: String) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
SharedPreferences.getString(key, "")!!
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) =
SharedPreferences.putString(key, value).apply()
}
// 使用:属性读写自动委托到 SP
var userName: String by PrefDelegate("user_name")面试加分:自定义委托属性常用于优雅封装 SharedPreferences、Room 查询缓存等,体现「高效编码」。
Qobject companion object 的区别?易
object— 单例对象声明,定义即创建唯一实例companion object— 类的伴生对象,类似 Java 的 static,但本身是个真实对象(可实现接口)object : Foo()— 匿名对象表达式,类似 Java 匿名内部类,可继承/实现多个
class MyClass {
companion object {
const val TAG = "MyClass"
fun create() = MyClass() // 工厂方法
}
}
// 调用:MyClass.TAG, MyClass.create()
// object 单例
object AppConfig {
var debug = false
}注意:const val 是真正的编译期常量(内联),companion object 中的普通 val 在运行期初始化。
1.3 作用域函数(let / run / with / apply / also)
Qlet / run / with / apply / also 的区别和选择?中
// apply — 初始化对象
val dialog = AlertDialog.Builder(context).apply {
setTitle("提示")
setMessage("确认删除?")
setPositiveButton("确定") { _, _ -> delete() }
}.create()
// let — 空安全 + 重命名
val result = nullableUser?.let { user ->
"${user.name} (${user.age})"
} ?: "未知用户"
// also — 链式副作用
listOf(1, 2, 3)
.also { Log.d("TAG", "原始: $it") }
.map { it * 2 }
.also { Log.d("TAG", "处理后: $it") }记忆口诀:返回自身用 apply/also(apply 用 this,also 用 it);返回结果用 let/run/with(let/also 用 it,run/apply 用 this,with 是参数形式)。
1.4 集合操作 & 序列(Sequence)
QList vs MutableList?集合操作 map/filter 的性能?中
集合分类:
Collection→List(有序可重复)/Set(无序不重复)/Map只读接口(<5code>List)vs 可变接口(
MutableList)— Kotlin 的设计是「只读不等于不可变」
链式操作的问题:
// 普通 List 链式调用 — 每步都创建中间集合
list.map { it * 2 } // 创建新 List
.filter { it > 5 } // 再创建新 List
.take(3) // 再创建新 List
// 如果 list 有 10000 个元素,前面两步各处理全部 10000 个
// Sequence — 惰性求值,逐元素处理完整个链条
list.asSequence()
.map { it * 2 }
.filter { it > 5 }
.take(3)
.toList()
// 逐个元素走完 map→filter→take,找到 3 个就停面试要点:数据量大 + 链式多步操作时用 Sequence 减少中间集合。小数据量(<1000)用普通集合更直观。Sequence 是惰性的,必须有终端操作(toList/count/forEach)才真正执行。
⚡二、协程深入
Kotlin 协程是 Android 异步编程的标准方案,面试必考重点
2.1 协程基础
Q协程是什么?和线程的关系?为什么轻量?中
协程是「用户态的轻量线程」,由 Kotlin 协程库调度,不直接对应 OS 线程。
轻量原因:
协程挂起时不阻塞线程,线程可以去执行其他协程
协程的创建开销约 ~100 bytes(线程约 ~1MB 栈空间)
10 万个协程可以轻松创建,10 万个线程会 OOM
// 10 万协程毫无压力
runBlocking {
repeat(100_000) {
launch {
delay(1000)
}
}
}Dispatchers(调度器):
Dispatchers.Main— Android 主线程(UI 线程)Dispatchers.IO— IO 线程池(网络、磁盘),默认 64 线程上限Dispatchers.Default— CPU 密集型任务,线程数 = CPU 核数Dispatchers.Unconfined— 不切换线程,在调用者线程执行(少用)
Qsuspend 函数的原理?D难
CPS 变换(Continuation-Passing Style):编译器将 suspend 函数转换成带有 Continuation 参数的函数,通过状态机实现挂起与恢复。
// 源码
suspend fun fetchUser(): User {
val token = getToken() // 挂起点 1
val user = api.getUser(token) // 挂起点 2
return user
}
// 编译后(简化)— 通过 label 状态机
fun fetchUser(continuation: Continuation): Any? {
val sm = continuation as? FetchUserSM ?: FetchUserSM(continuation)
when (sm.label) {
0 -> {
sm.label = 1
return getToken(sm) // 挂起,返回 COROUTINE_SUSPENDED
}
1 -> {
sm.token = sm.result as String
sm.label = 2
return api.getUser(sm.token, sm)
}
2 -> {
return sm.result as User // 完成
}
}
}面试加分:说明 suspend 函数不一定会挂起(如果没有真正挂起的调用则同步执行),挂起通过返回 COROUTINE_SUSPENDED 标志实现。
QCoroutineScope / Job / SupervisorJob 的关系?中
CoroutineScope — 管理协程生命周期,提供取消和异常处理的上下文
Job — 协程的句柄,控制生命周期(cancel"cancel, join, invokeOnCompletion)
SupervisorJob — 子协程异常不会传播到父协程和其他兄弟协程
// 普通结构 — 一个子协程异常会取消整个父 scope
coroutineScope {
launch { throw RuntimeException("crash") } // 会导致兄弟协程也被取消
launch { delay(1000); println("可能不会执行") }
}
// SupervisorJob — 异常隔离
supervisorScope {
launch { throw RuntimeException("crash") } // 只取消自己
launch { delay(1000); println("正常执行") }
}<'strong>Android 实践:viewModelScope 使用的是 SupervisorJob + Main Dispatcher,确保一个网络请求失败不会影响 UI 状态更新。
2.2 Flow & Channel
QFlow 是什么?和 LiveData/RxJava 的区别?中
Flow 是冷流(Cold Stream),类似 RxJava 的 Observable,有终端操作符(collect)才会执行。
KMP 关键:Flow 是 kotlinx-coroutines-core 的一部分,天然支持 KMP,可在共享模块中使用,这是 KMP 跨平台异步的首选。
QStateFlow vs SharedFlow vs Channel?难
// StateFlow — 替代 LiveData 表示 UI 状态
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow = _uiState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Loading
_uiState.value = UiState.Success(repo.fetch())
}
}
}
// SharedFlow — 一次性事件(如导航、SnackBar)
private val _events = MutableSharedFlow()
val events = _events.asSharedFlow()2.3 结构化并发 & 异常处理
Q协程异常处理机制?CoroutineExceptionHandler 怎么用?难
异常传播规则:
普通 Job:子协程抛异常 → 取消父协程 → 取消所有兄弟协程 → 异常传播到 root
SupervisorJob:子协程异常不传播,只取消自己
supervisorScope / SupervisorJob 内的子协程各自处理异常
// CoroutineExceptionHandler — 只在 root 协程生效
val handler = CoroutineExceptionHandler { _, e ->
Log.e("TAG", "Caught: $e")
}
viewModelScope.launch(handler) {
throw RuntimeException("oops") // 被 handler 捕获
}
// try-catch 包裹具体挂起调用
viewModelScope.launch {
try {
repo.fetch()
} catch (e: IOException) {
_uiState.value = UiState.Error(e)
}
}面试坑点:CoroutineExceptionHandler 只能捕获未处理的异常且只在 root 协程中有效。非 root 协程的异常会先被 Job 层级传播机制处理。
QwithContext vs launch vs async 的区别?易
launch— 启动新协程,返回 Job,不返回结果(fire-and-forget)async— 启动新协程,返回 Deferred,await 获取结果(并行计算)withContext— 切换上下文(如线程),suspend 函数,不是启动新协程,返回结果
// async 并行
suspend fun loadPage(): Page = coroutineScope {
val header = async { loadHeader() }
val content = async { loadContent() }
val footer = async { loadFooter() }
Page(header.await(), content.await(), footer.await())
}
// withContext 切换线程
suspend fun save(data: Data) = withContext(Dispatchers.IO) {
db.save(data) // 在 IO 线程执行
}📱三、Android 开发核心
JD 第 1 点:"负责安卓平台的开发与维护"
3.1 四大组件 & 生命周期
QActivity 生命周期?onSaveInstanceState 什么时候调用?易
完整生命周期:onCreate → onStart → onResume → [运行] → onPause → onStop → onDestroy
onSaveInstanceState 调用时机:
API < 28:在 onStop 之前调用(且在 onPause 之前可能调用)
API ≥ 28:在 onStop 之后调用,且当「可能被系统杀死」时才调用(如按 Home、旋转屏幕)
不会在按 Back 销毁时调用(用户主动退出)
现代做法:用 ViewModel + SavedStateHandle 处理配置变更数据恢复,不再依赖 onSaveInstanceState 手动写 Bundle。
QFragment 生命周期?为什么 Fragment 被称为"最复杂的组件"?中
Fragment 生命周期比 Activity 多出:onAttach → onCreate → onCreateView → onViewCreated → onViewStateRestored → onStart → onResume
复杂原因:
Fragment 有「实例生命周期」和「View 生命周期」两套,
viewLifecycleOwnervsthis回退栈、嵌套 Fragment 时回调顺序复杂
FragmentManager 状态保存/恢复时机问题(IllegalStateException "Fragment not associated with FragmentManager")
多 Fragment 通信、状态同步
最佳实践:用 viewLifecycleOwner 而非 this 来观察 LiveData/Flow,避免 View 销毁后仍收到回调导致内存泄漏。
QHandler 机制原理?为什么不会导致 ANR?中
四要素:Handler、Message、MessageQueue、Looper
Looper:每个线程最多 1 个,
Looper.prepare()创建,Looper.loop()开启死循环。主线程的 Looper 在 ActivityThread.main() 中已 prepare+loopMessageQueue:单链表实现的优先队列,按
when(时间)排序Handler:发送 Message 到关联的 MessageQueue,Looper 取出后回调
handler.dispatchMessage()Message:通过对象池(最大 50 个)复用,
obtain()而非 new
为什么不 ANR:Looper.loop() 是死循环,但 MessageQueue.next() 在没有消息时会调用 nativePollOnce()(epoll 机制)阻塞线程释放 CPU,不占 CPU 资源。有消息或输入事件时被唤醒处理。ANR 是因为「处理某条消息耗时过长」导致后续输入事件超时(5s),不是因为 loop 本身。
面试加分:Handler 跨线程通信本质是「往目标线程的 MessageQueue 投递 Message」,目标 Looper 取出后在目标线程执行。这是 Android 线程通信的基础,Binder IPC 也是基于此。
3.2 Jetpack & 架构组件
QViewModel 为什么能在配置变更(旋转屏幕)时保留数据?原理?中
原理:
ViewModel 存储在
ViewModelStore中,ViewModelStore 挂在NonConfigurationInstances上Activity 配置变更时,系统通过
onRetainNonConfigurationInstance()保存 ViewModelStore新 Activity 创建后,通过
getLastNonConfigurationInstance()取回同一个 ViewModelStore所以 ViewModel 实例没变,数据保留
销毁时机:Activity 调用 finish() 或真正销毁时,ViewModel.onCleared() 被调用,viewModelScope 被取消。
注意:不要在 ViewModel 中持有 View/Activity/Context 引用(会泄漏)。需要 Context 时用 AndroidViewModel(app)。
QLiveData vs Flow 在 Android 中怎么选?中
KMP 场景:共享模块的 ViewModel 使用 StateFlow,Android 端用 collectAsStateWithLifecycle() 转为 Compose State。
QRoom 数据库的原理和注意事项?中
Room 是 SQLite 的抽象层,编译期校验 SQL 语句,自动生成实现代码。
编译期校验:SQL 语法错误、表名/字段名错误在编译期就能发现
@Dao:接口或抽象类,Room 生成实现。推荐用
suspend或Flow返回值返回 Flow:数据变更时自动重新查询(观察者模式)
迁移:用
Migration手写,fallbackToDestructiveMigration会清数据(仅开发用)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id = :id")
fun observe(id: String): Flow // 数据变更自动刷新
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: UserEntity)
}KMP 关联:Room 是 Android 专属,KMP 跨平台数据库用 SQLDelight(SQL 驱动,生成 Kotlin 代码,跨平台)。
QRecyclerView 的缓存机制?为什么比 ListView 高效?中
四级缓存:
mAttachedScrap — 屏幕内可见的 ViewHolder 缓存(屏幕旋转/数据变更时用)
mCachedViews — 刚滑出屏幕的 ViewHolder,默认 2 个(不需 rebind)
ViewCacheExtension — 自定义缓存层(少用)
RecycledViewPool — 按 viewType 缓存,默认每个 type 5 个(需 rebind)
比 ListView 高效的原因:多级缓存 + 强制 ViewHolder 模式 + 批量布局 + 预取(Prefetch)。setHasFixedSize(true)、DiffUtil 局部刷新是常见优化手段。
3.3 性能优化
Q内存泄漏的常见场景和排查?中
常见场景:
内部类(匿名内部类 / lambda)持有外部 Activity 引用 + 异步任务未取消
静态变量持有 Activity/View
单例回调持有 Context
Handler 在 Activity 销毁后仍有延迟消息
ViewModel 持有 View 引用
资源未关闭(Cursor、InputStream、BroadcastReceiver 未 unregister)
排查工具:
LeakCanary — 自动检测 Activity/Fragment 泄漏
Android Studio Profiler — 实时内存分析
MAT (Memory Analyzer) — 分析 hprof 快照,找 GC Root 引用链
StrictMode — 检测主线程 IO、网络等违规操作
面试加分:提到协程绑定 viewModelScope/lifecycleScope 后,配置变更时协程会自动取消,天然避免很多回调泄漏问题。
QANR 的原因和排查?中
ANR 阈值:
输入事件/广播 5 秒无响应(前台)
Service 20 秒(前台)/ 200 秒(后台)
ContentProvider publish 10 秒
排查:
看
/data/anr/traces.txt获取各线程堆栈主线程做了 IO / 数据库操作 / 大计算 / 同步锁等待
用
StrictMode提前发现用
BlockCanary监控主线程卡顿
Q启动优化方案?中
冷启动阶段:Application.onCreate → Activity.onCreate → 首帧绘制
优化手段:
延迟初始化非关键依赖(
App Startup/ 按需初始化)多线程并行初始化(不依赖的组件)
用
WindowBackground设置启动画面避免白屏闪屏用
SplashScreen API(Android 12+)首屏布局优化:减少层级、避免过度绘制
用
Baseline Profile+ Macrobenchmark 测量
测量:
Debug.startMethodTracing、ReportFragment记时、JetpackTraceAPI
3.4 多线程 & 底层机制
QBinder IPC 机制原理?为什么比其他 IPC 高效?难
Binder 是 Android 特色的 IPC 机制,基于 Linux 的 binder 驱动(/dev/binder)。
核心原理:
利用 内存映射(mmap):用户空间和内核空间共享物理内存,数据传递只需一次拷贝(普通 IPC 需两次)
面向对象:ServiceManager 管理服务,Client 通过代理对象调用,像本地方法调用
线程模型:每个 Binder 进程有一个 Binder 线程池(默认 15 个),处理跨进程调用
AIDL 是 Binder 的接口描述语言,编译器生成 Stub(服务端)和 Proxy(客户端)代码。
面试加分:四大组件的跨进程通信都基于 Binder。ContentProvider 的 CRUD 调用就是 Binder 调用,所以主线程调用 ContentProvider 可能 ANR。
🔗四、KMP 核心知识
JD 第 2 点:"参与 KMP 项目的技术实现与优化" — 差异化考点,必须深入
4.1 KMP 概念 & 项目架构
Q什么是 KMP?和 KMM、Compose Multiplatform 的关系?中
KMP(Kotlin Multiplatform)是 Kotlin 的跨平台能力,允许编写共享代码,编译到 JVM(Android/桌面)、Native(iOS)、JS 等目标。
KMP 的核心理念:不是「一次编写到处运行」(区别于 Flutter),而是「一次编写到处能复用」— 共享业务逻辑,平台特定部分各自实现。
面试加分:明确说明 KMP 共享的是「逻辑层」而非「UI 层」(除非用 Compose Multiplatform),平台特定 UI(如 Android 用 Compose、iOS 用 SwiftUI)各自实现,通过 expect/actual 桥接。
QKMP 项目的典型架构是怎样的?模块如何划分?中
MyProject/
├── shared/ # KMP 共享模块
│ ├── build.gradle.kts # KMP 模块配置
│ └── src/
│ ├── commonMain/ # 平台无关代码(所有平台共享)
│ │ └── kotlin/
│ ├── androidMain/ # Android 平台特定实现
│ │ └── kotlin/
│ └── iosMain/ # iOS 平台特定实现
│ └── kotlin/
├── androidApp/ # Android 应用
│ └── build.gradle.kts
└── iosApp/ # iOS 应用 (Xcode project)
└── ...层次划分(推荐 Clean Architecture 思路):
commonMain— Domain 层(纯 Kotlin,无平台依赖)+ Data 层接口 + UseCaseandroidMain/iosMain— Data 层实现(平台特定 DB、网络、文件等)Presentation 层(ViewModel + StateFlow)可放在 commonMain(用 KMP ViewModel)
// shared/build.gradle.kts
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("io.ktor:ktor-client-core:2.3.12")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:2.3.12")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.12")
}
}
}
}QKMP vs Flutter vs React Native 的对比?什么时候选 KMP?中
选 KMP 的场景:
团队已有 Kotlin/Android 基础,希望平滑扩展到 iOS
想共享业务逻辑但 UI 各平台原生实现(追求极致原生体验)
已有大型 Android 项目,想逐步抽取共享逻辑到 KMP(渐进式迁移)
需要和原生代码深度交互(KMP 可直接调用平台 API)
4.2 expect/actual 机制
Qexpect/actual 的原理和使用方式?难
expect/actual 是 KMP 的核心机制,类似接口与实现的关系,但发生在编译期。
expect— 在 commonMain 中声明「期望的平台特定 API」(类似接口)actual— 在各平台 sourceSet 中提供「实际实现」(类似实现类)编译期绑定:编译到 Android 时用 androidMain 的 actual,编译到 iOS 时用 iosMain 的 actual
// commonMain — 声明
expect class PlatformDate() {
fun toEpochMillis(): Long
fun format(pattern: String): String
}
expect fun platformName(): String
// androidMain — Android 实现
actual class PlatformDate {
private val cal = Calendar.getInstance()
actual fun toEpochMillis(): Long = cal.timeInMillis
actual fun format(pattern: String): String =
SimpleDateFormat(pattern).format(cal.time)
}
actual fun platformName(): String = "Android"
// iosMain — iOS 实现
actual class PlatformDate {
private val date = NSDate()
actual fun toEpochMillis(): Long =
(date.timeIntervalSince1970 * 1000).toLong()
actual fun format(pattern: String): String =
NSDateFormatter().apply { dateFormat = pattern }.stringFromDate(date)
}
actual fun platformName(): String = "iOS"expect/actual 的限制:
签名必须完全匹配(参数、返回类型、可见性)
可以声明 class / fun / property / object / typealias
actual 可以用
type)9typeC typealias映射到平台现有类型(最!如"零成本抽象")
9
// 用 typealias 将 expect �:映射9映射到平台类型 —G类型 — 零成本
// commonMain
expect class KMPDate
// androidMain
actual typealias KMPDate = java.util.Date
// iosMain
actual typealias KMPDate = kotlinx.datetime.Instant面试加分:typealias 方式是「零成本抽象」— 不创建包装类,直接映射到平台类型,无运行时开销。这是 KMP 性能优势的重要来源。
4.3 KMP 生态库
QKMP 项目中常用的跨平台库有哪些?中
注意:选库时一定要确认支持 KMP(有 commonMain artifact),有些 Android 库没有跨平台版本(如 Room → 用 SQLDelight 替代)。
QKtor 在 KMP 中怎么用?平台引擎如何切换?中
面试加分:说明 Ktor 的 Platform engine 是通过 expect/actual 或各平台工厂方法注入的,commonMain 不直接依赖引擎,只依赖 ktor-client-core。
QSQLDelight vs Room 的对比?为什么 KMP 用 SQLDelight?中
4.4
4.4 KMP 实践 & 踩坑
QKMP 开发中常见的坑和解决方案?难
1. iOS 新架构(新内存管理)
旧内存管理默认 strict,对象可能被自动冻结(frozen),导致跨线程访问报错
新内存管理(1.9+ 默认开启)取消了大部分限制,对象默认不被 frozen
协程在 iOS 上需配置 new memory manager 才能正常工作
2. 依赖在 commonMain 只能用 KMP 兼容的库
误引 Android 专属库到 commonMain → 编译错误
解决方案:放 androidMain,或用 expect/actual 封装
3. Kotlin/Native 不支持部分 JVM 特性
反射(有限支持)、
Class.forName不可用部分库依赖反射(如 Gson)→ 用 kotlinx.serialization 替代
JVM 的
Thread不可用 → 用协程
4. expect/actual 签名不匹配
编译期会报错,但错误信息可能不直观
注意:actual 的可见性必须 ≥ expect 的可见性
5. 构建配置复杂
iOS 构建需要 Xcode + Kotlin/Native 工具链
CI/CD 需要 macOS 环境(iOS 编译要求)
构建时间较长,需优化(caching、parallel、按需编译目标)
QKMP 项目如何优化构建速度和包体积?难
Q如何在已有 Android 项目中渐进式引入 KMP?中
渐进式迁移步骤:
第一步:创建 shared KMP 模块,配置 Android 目标
第二步:将纯 Kotlin 工具类(无 Android 依赖)迁移到 commonMain
第三步:将数据模型(data class)迁移到 commonMain,用 kotlinx.serialization
第四步:网络层用 Ktor 替换 Retrofit,接口定义放 commonMain
第五步:业务逻辑 / UseCase / Repository 迁移到 commonMain,平台特定用 expect/actual
第六步:ViewModel 迁移(用 KMP ViewModel),UI 层保持 Android 原生
第七步:如需 iOS,添加 iOS 目标,实现 iosMain 的 actual
实践建议:不要一次性迁移。先迁移无平台依赖的纯逻辑,验证编译通过后再迁移有依赖的部分。每步确保 Android 端功能不受影响。
🏛五、架构 & 设计模式
体现"高效编码"和工程能力
QMVVM 架构的各层职责?数据如何流动?中
三层职责:
View(Activity/Fragment/Compose):渲染 UI,观察 ViewModel 状态,转发用户事件
ViewModel:持有 UI 状态(StateFlow),处理业务逻辑,调用 UseCase/Repository
Model(Repository/UseCase):数据获取(网络/数据库),业务规则
数据流:单向数据流(UDF)
// 事件: View → ViewModel
// 状态: ViewModel → View (StateFlow)
// 副作用: ViewModel → View (SharedFlow 事件)
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
// 状态下行
private val _state = MutableStateFlow(UserState.Loading)
val state = _state.asStateFlow()
// 事件上行
fun onEvent(event: UserEvent) {
when (event) {
is UserEvent.Load -> loadUser()
is UserEvent.Retry -> loadUser()
}
}
private fun loadUser() = viewModelScope.launch {
_state.value = UserState.Loading
_state.value = try {
UserState.Success(getUserUseCase())
} catch (e: Exception) {
UserState.Error(e.message)
}
}
}QClean Architecture 的分层?在 KMP 中怎么落地?难
三层(依赖方向:外层依赖内层):
Domain 层:纯 Kotlin,无任何框架依赖。包含 Entity、UseCase、Repository 接口
Data 层:Repository 实现、数据源(网络/DB)、DTO ↔ Entity 映射
Presentation 层:ViewModel、UI State、平台 UI
KMP 落地:
Domain 层 →
commonMain(纯 Kotlin,最易跨平台)Data 层接口 →
commonMain,实现 → 各平台androidMain/iosMainPresentation ViewModel →
commonMain(KMP ViewModel + StateFlow)UI → 各平台独立实现(Android Compose / iOS SwiftUI)
依赖注入用 Koin,在 commonMain 定义模块,各平台提供平台特定实例
面试加分:Clean Architecture 的核心是「依赖反转」— Data 层实现 Domain 层定义的接口,而非 Domain 依赖 Data。这使得 Domain 层完全可测试且跨平台。
Q依赖注入:Hilt vs Koin?KMP 中怎么选?中
KMP 中选 Koin,因为 Hilt 依赖 Java 注解处理,不支持 Kotlin/Native。Koin 3+ 原生支持 KMP,在 commonMain 定义 DI 模块。
// commonMain
val appModule = module {
singleOf(::UserRepositoryImpl) bind UserRepository::class
factoryOf(::GetUserUseCase)
viewModelOf(::UserViewModel)
}
// androidMain
startKoin {
modules(appModule, platformModule)
}💬六、高频技术面试题速查
考前快速过一遍,确保都能说清楚
Kotlin 速查
为什么说 Kotlin 的空安全是编译期的?平台类型(T!)是什么?
by lazy 的线程安全模式有哪几种?默认哪个?
data class 的 copy 是深拷贝还是浅拷贝?
扩展函数能被子类继承吗?(不能,静态解析)
inline 函数为什么能用 reified?(编译期内联展开,类型已知)
密封类在 KMP 中的限制?(子类必须在同一模块)
协程的挂起函数一定会挂起吗?(不一定)
StateFlow 的初始值为什么必须提供?
Flow 是冷流还是热流?怎么变热流?(stateIn/shareIn)
launchIn 和 collect 的区别?(launchIn 非阻塞,collect 是 suspend)
Android 速查
Activity A 启动 B,B 再返回 A,A 的生命周期回调顺序?
onRestart 什么时候调用?(从 stop 回来,但未被销毁)
ViewModel 的 onCleared 什么时候调用?(Activity finish / Fragment detach 且非配置变更)
LiveData postValue vs setValue?(post 异步切主线程,set 必须主线程)
Room 的 @Transaction 作用?(原子操作,多个 DAO 方法一个事务)
WorkManager vs JobIntentService?(WorkManager 是 Jetpack 推荐方案,可约束、可观测)
Comops/Compose 的 remember vs rememberSaveable?(后者参与状态保存恢复)
为什么 Compose 中推荐 immutable collection?(重组优化)
RecyclerView 的 DiffUtil 作用?(计算差异,局部刷新,避免 notifyDataSetChanged)
OkHttp Interceptor 的责任链顺序?(应用 → 网络,Bridge → Connect → Call → ...)
KMP 速查
KMP 共享的是逻辑还是 UI?(逻辑,除非用 Compose MP)
expect/actual 签名不匹配会怎样?(编译错误)
为什么 typealias 形式的 actual 是零成本?(直接类型别名,无包装)
Kotlin/Native 为什么不支持反射?(AOT 编译,无运行时反射信息)
Ktor 为什么要在各平台提供不同 engine?(底层 HTTP 实现不同)
KMP 项目的 iosMain 编译需要什么环境?(macOS + Xcode)
新内存管理解决了什么问题?(对象冻结限制,协程更自然)
KMP 中 Gson 能用吗?(不能用,依赖反射 → 用 kotlinx.serialization)
shared 模块的 commonTest 怎么写?(expect/actual 测试 + 各平台跑)
KMP 和 Compose Multiplatform 的关系?(后者基于前者,共享 UI)
🤝七、软技能 & HR 面准备
JD 第 3-5 点:沟通、团队协作、学习能力
STAR 法回答行为面试题
S (Situation) 背景 → T (Task) 任务 → A (Action) 你做了什么 → R (Result) 结果
提前准备 3-5 个故事,覆盖不同维度:
Q"请描述一次你按时完成上级任务的经历"
准备方向:讲一个有明确 deadline 的任务,体现时间管理 + 沟通汇报。
S:项目有紧急需求,X 天内上线一个功能
T:我负责某模块开发,时间紧,需求有不确定性
A:拆解任务估算工时 → 每日同步进度 → 遇到风险提前预警并协调资源 → 必要时砍非核心 scope 保上线
R:按时上线,质量达标,后续迭代补齐
Q"说一次团队协作中你融入新环境的经历"
准备方向:空格讲快速融入团队的故事,体现适应力 + 沟通。
S:加入新项目 / 接手遗留代码
T:需要快速理解架构并开始产出
A:主动阅读文档和代码 → 找同事 1v1 了解上下文 → 先做小任务建立信任 → 产出后主动 Code Review 交流
R:X 周内独立交付功能,融入团队节奏
Q"你最近学了什么新技术?怎么学的?"
准备方向:体现学习能力 + 改进方法。可以结合 JD 聊 KMP/Compose。
具体技术:如 KMP、Coroutines 深入原理、Compose Multiplatform
学习方式:官方文档 → 实践 demo → 看源码 → 写博客总结
改进工作:用新知识优化了某个旧实现(具体例子)
持续习惯:关注 Kotlin/Android 官方 blog、release notes、优秀开源项目
Q"你对 KMP 怎么看?为什么感兴趣?"
结合 JD 回答,体现你对技术有判断力:
KMP 的价值在于「共享逻辑 + 原生 UI」,比 Flutter 等方案更保留原生体验
团队已有 Kotlin 基础,引入 KMP 成本低、可渐进式迁移
协程、Flow、kotlinx.serialization 生态成熟,跨平台基础设施齐备
对 Compose Multiplatform 也保持关注,关注其成熟度
理性一面:KMP 也有代价(构建复杂度、iOS 环境依赖、Kotlin/Native 限制),适合的场景才用
其他高频行为题(按 STAR 准备)
遇到过的最难的技术问题?怎么解决的?
和同事意见分歧时怎么处理?
你做过的一次有成效的技术改进 / 重构?
错过 deadline 的经历?学到了什么?
你觉得自己需要改进的地方?
为什么离职 / 为什么选我们公司?
✅八、面试前 Checklist
技术复习清单
☐ Kotlin 空安全、扩展函数原理、密封类、内联函数、委托属性 — 能口述
☐ 协程:suspend 原理(CPS 变换)、Dispatcher 选择、结构化并发、异常处理
☐ Flow vs StateFlow vs SharedFlow 区别和场景
☐ Android 生命周期、Handler 机制、ViewModel 原理
☐ KMP:expect/actual 机制(能手写示例)、项目结构、常用库
☐ KMP vs Flutter 对比、KMP 适用场景
☐ MVVM / Clean Architecture 分层、KMP 中怎么落地
☐ Koin vs Hilt、SQLDelight vs Room、Ktor 用法
☐ 内存泄漏 / ANR / 启动优化的具体手段
☐ 准备 1-2 个项目深挖点(能聊 10 分钟的细节)
面试当天准备
☐ 准备 3-5 个 STAR 故事(按时完成、融入团队、学习新技术、解决难题、意见分歧)
☐ 了解面试公司业务 + 技术栈,准备 2-3 个好问题反问面试官
☐ 准备自我介绍(3 分钟版 + 1 分钟版)
☐ 如果有 KMP 项目经验,准备架构图 + expect/actual 示例
☐ 诚实原则:不会的说不了解但说思路,不编造
推荐反问面试官的问题
团队目前 KMP 的使用范围?是纯逻辑共享还是包含 Compose Multiplatform?
共享逻辑覆盖哪些模块?iOS 端是 SwiftUI 还是 Compose?
团队的 Kotlin 版本和主要技术栈?协程、Koin、Ktor 等
CI/CD 流程?iOS 构建如何处理(是否有 macOS CI runner)?
团队对 KMP 的未来规划?是否有扩大共享范围的计划?
code review 流程?技术分享机制?