Administrator
发布于 2026-08-26 / 27 阅读
0
0

Android 开发面试知识准备

基于 JD 定制 · Kotlin + KMP (Kotlin Multiplatform) 方向 · 详细版

Kotlin Android KMP Coroutines Compose Multiplatform Jetpack

📋概述 & 备考策略

根据 JD 逆向推导面试重点,制定复习优先级

JD 关键词 → 面试考点映射

JD 关键词

推断面试考点

优先级

Kotlin 高效编码

空安全、协程、集合操作、作用域函数、密封类、内联函数、委托属性

⭐⭐⭐⭐⭐

KMP 技术实现

expect/actual、共享模块架构、Ktor、kotlinx.serialization、SQLDelight、Gradle 配置

⭐⭐⭐⭐⭐

KMP 优化

构建优化、平台特定代码组织、依赖管理、性能调优

⭐⭐⭐⭐

安卓平台开发维护

四大组件、生命周期、Jetpack、性能优化、内存管理

⭐⭐⭐⭐

善于沟通/团队协作

行为面试题(STAR 法)、协作场景

⭐⭐⭐

学习能力/改进方法

技术成长、踩坑经验、对新技术态度

⭐⭐⭐

🎯 备考优先级建议

  1. Kotlin 语言深度 — 这是 JD 第一要求,一定会深挖。重点:协程原理、空安全机制、内联函数原理、密封类 vs 枚举、集合惰性序列

  2. KMP 原理与实践 — JD 第二要求,差异化考点。重点:expect/actual 编译机制、项目结构、跨平台库选型、与纯 Android 项目的集成

  3. Android 基础 — 虽然不是重点突出,但 Android 岗必考。重点:生命周期、Handler 机制、RecyclerView、ViewModel/LiveData

  4. 架构与设计模式 — 体现"高效编码"能力。重点:MVVM、Clean Architecture、Repository 模式

  5. 软技能 — 用 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 的区别?易

关键字

可变?/th>

编译期?

使用场景

var

可变引用

运行期

需要重新赋值的变量

val

只读引用(不等于不可变对象)

运行期

大部分场景,优先使用

const val

只读

编译期常量

原始类型 + String,必须顶层或 object 内

注意: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?中

特性

sealed class

enum class

abstract class

子类数量

有限、已知(同一文件/模块)

固定实例

不限

子类可带状态

✅ 每个子类独立状态

❌ 所有实例共享属性

✅

when 穷尽检查

✅ 编译器要求覆盖所有分支

✅

❌

可实例化

子类可以

枚举值

不能

// 密封类经典用途: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 的区别和选择?中

函数

接收者

返回值

典型用途

let

it (参数)

lambda 结果

空安全检查、变量重命名

run

this (接收者)

lambda 结果

对象配置 + 计算结果

with

this (参数传入)

lambda 结果

对同一对象连续调用

apply

this

对象本身

对象初始化/Builder 模式

also

it

对象本身

副作用(日志、调试)不改链式

// 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)才会执行。

特性

Flow

LiveData

RxJava

冷/热

冷(默认)

热

都有

背压

内置支持

无

有

线程切换

flowOn/

固定主线程

subscribeOn/observeOn

生命周期感知

需 repeatOnLifecycle

内置

需手动管理

KMP 支持

✅ 跨平台

❌ 仅 Android

❌

KMP 关键:Flow 是 kotlinx-coroutines-core 的一部分,天然支持 KMP,可在共享模块中使用,这是 KMP 跨平台异步的首选。

QStateFlow vs SharedFlow vs Channel?难

StateFlow

SharedFlow

Channel

类型

热流(状态持有)

热流(事件广播)

点对点通信

初始值

必须有

可选

无

去重

conflate + distinctUntilChanged

无(可配)

无

缓存

仅最新 1 个

replay 可配

buffer 可配

用途

UI 状态

事件/消息

生产者-消费者

// 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 生命周期」两套,viewLifecycleOwner vs this

  • 回退栈、嵌套 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+loop

  • MessageQueue:单链表实现的优先队列,按 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 中怎么选?中

场景

推荐

原因

UI 状态(数据展示)

StateFlow

KMP 兼容、背压支持、不依赖 Android 生命周期

一次性事件(导航/通知)

SharedFlow

不会重复发射,事件语义

简单 UI 数据绑定

LiveData

生命周期感知,简单直观

KMP 共享逻辑

StateFlow/Flow

LiveData 无法在 KMP common 模块使用

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 高效?中

四级缓存:

  1. mAttachedScrap — 屏幕内可见的 ViewHolder 缓存(屏幕旋转/数据变更时用)

  2. mCachedViews — 刚滑出屏幕的 ViewHolder,默认 2 个(不需 rebind)

  3. ViewCacheExtension — 自定义缓存层(少用)

  4. 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 记时、Jetpack Trace API

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;tr>

Kotlin 跨平台技术总称

底层能力

KMM

K*otlin Multiplatform Mobile

KMP 的移动端子集(Android + iOS)

Compose Multiplatform

UI 框架跨平台

基于 KMP,共享 UI(Android/iOS/桌面/Web)

Kotlin/Native

编译到原生二进制

KMP 的 iOS 目标编译器

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 层接口 + UseCase

  • androidMain / 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

Flutter

React Native

语言

Kotlin

Dart

JS/TS

共享范围

逻辑层(可选 UI)

UI + 逻辑

UI + 逻辑

UI 方案

各自原生 / Compose MP

自绘引擎

原生组件映射

性能

原生性能(编译到平台)

接近原生(Dart VM)

JS Bridge / JSI

包体积

小(无运行时)

中等(含 Flutter 引擎)

中等

原生体验

100% 原生

接近但非完全

接近

学习曲线

低(已有 Kotlin 基础)

中(学 Dart)

中(学 RN 框架)

选 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 项目中常用的跨平台库有哪些?中

领域

库

说明

网络

Ktor Client

跨平台 HTTP 客户端,各平台不同引擎(Android: OkHttp, iOS: Darwin)

序列化

kotlinx.serialization

JSON/ProtoBuf,编译期生成代码,跨平台

日期时间

kotlinx-datetime

跨平台日期时间 API

协程

kotlinx-coroutines-core

Flow、suspend,跨平台异步

数据库

SQLDelight

SQL 驱动,生成 Kotlin 代码,跨平台

日志

Napier / Kermit

跨平台日志库

依赖注入

Koin

纯 Kotlin DI,跨平台

key-value 存储

multiplatform-settings

跨平台 K/V 存储(SP/> NSUserDefaults)

ViewModel

KMP ViewModel

Jetpack ViewModel 跨平台版( lifecycle-viewmodel 2.8+)

注意:选库时一定要确认支持 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?中

渐进式迁移步骤:

  1. 第一步:创建 shared KMP 模块,配置 Android 目标

  2. 第二步:将纯 Kotlin 工具类(无 Android 依赖)迁移到 commonMain

  3. 第三步:将数据模型(data class)迁移到 commonMain,用 kotlinx.serialization

  4. 第四步:网络层用 Ktor 替换 Retrofit,接口定义放 commonMain

  5. 第五步:业务逻辑 / UseCase / Repository 迁移到 commonMain,平台特定用 expect/actual

  6. 第六步:ViewModel 迁移(用 KMP ViewModel),UI 层保持 Android 原生

  7. 第七步:如需 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/iosMain

  • Presentation ViewModel → commonMain(KMP ViewModel + StateFlow)

  • UI → 各平台独立实现(Android Compose / iOS SwiftUI)

  • 依赖注入用 Koin,在 commonMain 定义模块,各平台提供平台特定实例

面试加分:Clean Architecture 的核心是「依赖反转」— Data 层实现 Domain 层定义的接口,而非 Domain 依赖 Data。这使得 Domain 层完全可测试且跨平台。

Q依赖注入:Hilt vs Koin?KMP 中怎么选?中

维度

Hilt (Dagger)

Koin

类型

编译期(注解处理器)

运行期(Service Locator)

性能

快(编译期生成)

稍慢(运行期查找)

错误发现

编译期

运行期

跨平台

❌ 仅 Android/JVM

✅ 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 流程?技术分享机制?


评论