Android 资深岗 Kotlin 面试题:只会用协程不够,你得懂它为什么这么设计
高级岗考察"会不会用",资深岗考察"为什么这么设计"。这篇文章不是更难的选择题,而是让你解释清楚每一个设计决策背后的 trade-off。
一、类型系统与编译期行为
1.1 平台类型(Platform Type)在工程中怎么处理?
核心 :平台类型是 Kotlin 与 Java 互操作时的妥协------编译器无法从 Java 字节码推断可空性,既不强制检查,也不保证非空。必须通过分级处理规范来治理,外部依赖按可空处理,内部契约按非空处理。
kotlin
// 外部接口(第三方 SDK、HTTP 响应)------ 按可空处理
class ThirdPartyService {
fun fetchUser(): UserDto? {
return api.getUser()?.let { UserDto(it) }
}
}
// 内部接口(模块间契约)------ 按非空 + 契约声明
@Contract("null -> null; !null -> !null")
fun String?.orThrow(message: String): String = this ?: throw IllegalStateException(message)
// 内部接口强制返回非空
class UserRepository {
fun getUser(): User {
return localDataSource.getUser()
?: throw IllegalStateException("User must exist")
}
}
Android 实战:
kotlin
// Java SDK 包装器,明确可空性
object SafePreferences {
private val delegate = PreferenceManager.getDefaultSharedPreferences(context)
fun getString(key: String, default: String?): String? = delegate.getString(key, default)
fun requireString(key: String): String = getString(key, null)
?: throw IllegalStateException("Missing required key: $key")
}
// Lint 检查配置
// .editorconfig 或 ktlint 规则中配置
ktlint:
no-explicit-annotation-for-platform-type: error
面试加分:
- 核心业务模块间强制非空契约,外部依赖统一可空
- CI 中加入 null-safety 检查,lint 规则强制显式标注平台类型
- Java 库用 Kotlin 包装器隔离,接口层强制返回可空/非空
@Nullable注解需要手动配置 Kapt/Kotlin 编译器才能生效- 团队需要制定 Platform Type 处理规范并严格执行
1.2 Smart Cast 为何不生效?
核心 :Smart cast 基于数据流分析 ------编译器在分支内"记住"变量的非空状态。但受限于线程安全性 和求值时机,var/可变属性/自定义 getter 无法触发。
kotlin
// var 无法 smart cast(可能被其他线程修改)
var name: String? = "hello"
if (name != null) {
println(name.length) // 编译错误!var 可能在 if 检查后被修改
}
// 封闭对象属性无法 smart cast(可能被其他线程修改)
class User { var name: String? = "Alice" }
fun test(user: User) {
if (user.name != null) {
println(user.name.length) // 编译错误!user.name 是 var
}
}
// 自定义 getter 每次访问可能不同值
class Person {
val name: String? get() = computeName() // 每次访问执行计算
}
if (person.name != null) {
println(person.name.length) // 编译错误!
}
// Lambda 捕获的 var(Kotlin 1.1+ 放宽限制)
fun process(list: MutableList<String>?) {
if (list != null) {
list.add("item") // OK,同一作用域内,编译器允许
}
}
Android 实战:
kotlin
// 用 contract 扩展 smart cast 能力
@OptIn(ExperimentalContracts::class)
fun String?.orEmpty(): String {
contract { returnsNotNull() implies (this@orEmpty != null) }
return this ?: ""
}
// 使用后编译器知道 result 一定非空
fun test(s: String?) {
val result = s.orEmpty()
println(result.length) // OK
}
// 标准库实践
fun CharSequence?.isNullOrBlank(): Boolean {
contract { returns(false) implies (this@isNullOrBlank != null) }
return this == null || this.isBlank()
}
// ViewHolder 中的 smart cast
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) ?: return
holder.bind(item) // item 是 val,smart cast 生效
}
}
面试加分:
- Kotlin 1.1+ 放宽了同一作用域内 Lambda 捕获 val 的限制
- 用
let捕获局部变量可触发 smart cast @JvmField破坏 final 语义,导致 smart cast 失效Contract可以扩展编译器对空安全的认知- 遇到不生效时,用局部变量捕获或显式类型转换
1.3 类型投影(Type Projection)什么场景必须用?
核心 :当泛型类同时包含产出 和消费 操作时(如 MutableContainer<T>),无法声明型变,必须用投影限制使用方向。
kotlin
class MutableContainer<T>(var value: T)
// 不使用投影:编译错误
// fun copyFrom(source: MutableContainer<String>, target: MutableContainer<Any>) {
// target.value = source.value // 编译错误!
// }
// 使用 out 投影限制读取方向
fun copyFrom(source: MutableContainer<out Any>, target: MutableContainer<Any>) {
target.value = source.value // OK,out 投影允许读取
}
// 决策树:
// - 同时读写?→ 不使用投影,保持不变
// - 只读(生产者)?→ <out T> 协变
// - 只写(消费者)?→ <in T> 逆变
// - 不知道类型,只读?→ <*>
Android 实战:
kotlin
kotlin
// Repository 设计:产出 vs 消费分离
interface Producer<out T> { fun produce(): T }
interface Consumer<in T> { fun consume(value: T) }
// 同时需要产出和消费时,拆成两个接口
interface Repository<T> {
fun getAll(): List<T> // 产出
fun save(item: T) // 消费
// 不能声明为协变,需要拆成 Producer/Consumer
}
// 只读容器
class ReadOnlyContainer<out T>(private val item: T) {
fun get(): T = item
}
// 只写容器
class WriteOnlyContainer<in T> {
fun set(value: T) { /* ... */ }
}
// 星投影使用
val box: MutableContainer<*> = MutableContainer("hello")
val value: Any? = box.value // 可以读取,返回 Any?
// box.value = 123 // 编译错误,不能写入
面试加分:
out T= 协变,只能作为返回值类型(产出);in T= 逆变,只能作为参数类型(消费)*星投影等价于out Any?,只能读、不能写List<String>可赋值给List<Any>,因为 List 只读不变MutableList<String>不能赋值给MutableList<Any>,否则可写入破坏类型安全- Kotlin 标准库中的
List是协变的,MutableList不是
1.4 Reified 为什么只能在 inline 函数中使用?
核心 :reified 通过字节码展开 实现------编译器在每个调用点用具体类型替换 T,避免泛型擦除。inline 确保调用点代码可见,字节码才能被替换。
kotlin
inline fun <reified T> myInstanceOf(obj: Any): Boolean = obj is T
// 编译后等价于(字节码展开):
fun myInstanceOf_STRING(obj: Any): Boolean = obj is String
fun myInstanceOf_INT(obj: Any): Boolean = obj is Int
fun myInstanceOf_LONG(obj: Any): Boolean = obj is Long
// 调用 myInstanceOf<String>() 直接变成 obj is String
Android 实战:
kotlin
kotlin
// Gson 扩展:不需要 TypeToken
inline fun <reified T> Gson.fromJson(json: String): T =
this.fromJson(json, T::class.java)
// 使用
val user: User = gson.fromJson(jsonString)
// 安全类型转换
inline fun <reified T> Any.safeCast(): T? = this as? T
// 类型检查
inline fun <reified T> Any.isType(): Boolean = this is T
// 获取泛型类型
inline fun <reified T> getGenericType(): Class<T> = T::class.java
面试加分:
- JVM 泛型擦除:运行时无法区分
List<String>和List<Int> - reified 的代价是字节码膨胀(N 个调用点 → N 份代码)
T::class.java编译成字面量,无运行时查找开销- 性能敏感场景注意控制 reified 函数调用点数量
- 不要为了"方便"滥用 reified,只在确实需要运行时类型时使用
1.5 Nothing 和 Unit 的本质区别?
核心 :Nothing 是底类型(Bottom Type),所有类型的子类型,表示"永不返回";Unit 是单例,表示"无意义返回值"。两者在语义和使用场景上有本质区别。
kotlin
// Nothing:永不返回,用于标记异常路径
fun fail(message: String): Nothing = throw IllegalStateException(message)
// Unit:表示"无意义返回"的单例
fun log(message: String): Unit {
println(message)
return Unit // 显式返回,但可省略
}
// Nothing 在泛型中的特殊地位
val failure: Result<Nothing> = Result.failure(Exception())
// Result<Nothing> 可以赋值给 Result<T>,因为 Nothing 是所有类型的子类型
fun <T> handle(r: Result<T>) { /* ... */ }
handle(failure) // OK:Result<Nothing> → Result<T>
// TODO() 返回 Nothing,编译器知道后续代码不可达
fun calculate(x: Int): Int = when (x) {
0 -> 0
else -> TODO("Implement for x=$x")
}
Android 实战:
kotlin
// sealed class 用 Nothing 表示"无数据"的错误分支
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val exception: Throwable) : ApiResult<Nothing>() // Nothing!
}
// 编译器知道 Error 分支不可能返回数据
when (result) {
is ApiResult.Success -> render(result.data)
is ApiResult.Error -> showError(result.exception.message)
}
// requireNotNull 内部返回 Nothing
fun validate(user: User?) {
requireNotNull(user) { "User must exist" }
println(user.name) // 编译器知道 user 现在非空
}
// 不可达代码检测
fun process(x: Int): String = when (x) {
1 -> "one"
else -> {
fail("Unsupported value: $x")
// 编译器知道这里不可达
}
}
面试加分:
requireNotNull/checkNotNull内部返回 Nothing,编译器知道非空- sealed class 配合 Nothing 可以精确表达"失败无数据"的语义
- Nothing 的唯一实例是它自己(不存在),用于标记不可达路径
exitProcess()等函数返回 Nothing 表示终止进程永不返回Result<Nothing>是所有Result<T>的子类型,这在泛型中非常有用
1.6 Value Class(@JvmInline)解决了什么?有哪些限制?
核心 :编译期提供类型安全标签,运行时零开销(内联到基础类型)。适合 ID、度量单位等需要类型区分但又不想增加运行时开销的场景。
kotlin
@JvmInline
value class UserId(val value: Long)
@JvmInline
value class OrderId(val value: Long)
fun findUser(id: UserId) = /* ... */
fun findOrder(id: OrderId) = /* ... */
findUser(OrderId(123L)) // 编译错误!类型不匹配
// 运行时:findUser(123L) - 无对象创建,直接传 Long
Android 实战:
less
@JvmInline
value class Milliseconds(val value: Long)
@JvmInline
value class Seconds(val value: Long)
@JvmInline
value class Bytes(val value: Int)
fun sleep(ms: Milliseconds) = Thread.sleep(ms.value)
fun sleep(seconds: Seconds) = Thread.sleep(seconds.value * 1000)
sleep(Milliseconds(1000)) // OK
sleep(Seconds(1)) // OK
sleep(1000) // 编译错误!必须显式转换
面试加分:
- 必须有且仅有一个 val 属性,不能有 init 块
- 不能继承或被继承
- 运行时无对象创建,方法直接接受基础类型参数
- data class 会创建对象包装,value class 不会
- 字节码对比:value class 用
invokestatic,data class 用new+invokespecial
二、协程与异步的工程深度
2.1 Dispatcher 是怎么实现的?能不能自定义?
核心 :Dispatcher 基于 ContinuationInterceptor 实现------协程启动时,拦截器包装 Continuation,在 dispatch 方法中决定在哪里执行。自定义 Dispatcher 只需重写 dispatch 方法。
kotlin
// CoroutineDispatcher 核心方法
abstract class CoroutineDispatcher : AbstractCoroutineContextElement(), ContinuationInterceptor {
abstract fun dispatch(context: CoroutineContext, block: Runnable)
open fun dispatchYield(context: CoroutineContext, block: Runnable) {
dispatch(context, block)
}
}
// 自定义 Dispatcher
class SingleThreadDispatcher(private val name: String) : CoroutineDispatcher() {
private val executor = Executors.newSingleThreadExecutor { Thread(it, name) }
override fun dispatch(context: CoroutineContext, block: Runnable) {
executor.submit(block)
}
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
executor.submit(block) // 让出 CPU 后继续
}
}
Android 实战:
kotlin
// 图片预加载低优先级 Dispatcher
class ImagePreloadDispatcher : CoroutineDispatcher() {
private val executor = ForkJoinPool.commonPool()
override fun dispatch(context: CoroutineContext, block: Runnable) {
executor.execute {
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
block.run()
} catch (e: CancellationException) {
// 忽略取消异常
}
}
}
}
// 限制并发数
class LimitedConcurrencyDispatcher(private val maxConcurrency: Int) : CoroutineDispatcher() {
private val semaphore = Semaphore(maxConcurrency)
override fun dispatch(context: CoroutineContext, block: Runnable) {
if (semaphore.tryAcquire()) {
try { block.run() } finally { semaphore.release() }
} else {
launch(Dispatchers.IO) {
semaphore.acquire()
try { block.run() } finally { semaphore.release() }
}
}
}
}
// 优先级调度
class PriorityDispatcher : CoroutineDispatcher() {
private val queue = PriorityBlockingQueue<Runnable>()
override fun dispatch(context: CoroutineContext, block: Runnable) {
val priority = context[ContinuationInterceptor]?.let { /* 提取优先级 */ } ?: 0
queue.add(PrioritizedRunnable(priority, block))
}
}
面试加分:
Dispatchers.IO用阻塞 IO 线程池(64 线程),Default用 CPU 密集型线程池(核心数线程)- 自定义 Dispatcher 可实现并发限制(Semaphore)、优先级调度
dispatchYield让出 CPU 后继续执行,适合低优先级任务- Dispatcher 切换不创建新协程,只是改变执行线程
- Android 主线程调度通过
Handler.dispatch()实现
2.2 StateFlow 和 SharedFlow 选型标准?
核心 :需要最新值 (只关心当前状态)选 StateFlow;需要完整事件流(不能丢事件)选 SharedFlow。StateFlow 的 conflation 策略会丢失中间值。
ini
// StateFlow:值合并,只保留最新值
val stateFlow = MutableStateFlow(0)
stateFlow.value = 1
stateFlow.value = 2
stateFlow.value = 3
stateFlow.collect { println(it) } // 只输出 3
// SharedFlow:可配置 replay 和 buffer
val sharedFlow = MutableSharedFlow<Int>(replay = 2, extraBufferCapacity = 3)
sharedFlow.emit(1)
sharedFlow.emit(2)
// replay=2:新订阅者收到 [2, 3]
// buffer=[1,2,3] - replay拿走2个,还剩1个在buffer
Android 实战:
kotlin
// UI 状态 → StateFlow
class UiState(val items: List<Item>, val loading: Boolean)
val _state = MutableStateFlow(UiState(emptyList(), false))
val state: StateFlow<UiState> = _state
// 一次性事件 → SharedFlow(replay=0)
private val _events = MutableSharedFlow<UiEvent>(replay = 0)
val events: SharedFlow<UiEvent> = _events
// StateFlow equals 去重问题
data class UiState2(val value: Int)
val flow = MutableStateFlow(UiState2(0))
flow.value = flow.value.copy(value = 0) // equals 为 true,不触发 collect!
// 解决方案:version 字段
data class UiState3(val value: Int, val version: Long = 0) {
fun increment() = UiState3(value, version + 1)
}
val flow2 = MutableStateFlow(UiState3(0))
flow2.value = flow2.value.increment() // version 不同,触发通知
面试加分:
- StateFlow 的
equals比较:相同值不触发 collect - SharedFlow 的
replay控制新订阅者收到几个历史值 bufferOverflow控制缓冲区满时的策略(SUSPEND/DROP_OLDEST/DROP_LATEST)- 配置变更恢复用 StateFlow + SavedStateHandle
MutableSharedFlow(replay=1)可替代 StateFlow 的 replay 行为
2.3 协程异常传播机制:为什么 structured concurrency 重要?
核心 :structured concurrency 确保父子协程的生命周期绑定 。子协程异常会传播并取消兄弟协程,形成"全有或全无"的语义。除非用 supervisorScope 阻断。
kotlin
// 异常传播链路
suspend fun parent() = coroutineScope {
launch { throw Exception("Child error") } // 1. 子协程抛异常
launch { delay(1000); println("This won't run") } // 2. 兄弟协程被取消
}
// 3. 父协程重新抛出异常
// supervisorScope 阻断传播
suspend fun isolated() = supervisorScope {
launch { throw Exception("Child error") }
launch { delay(1000); println("This still runs!") } // 不受影响
}
// 协程正常完成,异常不传播
Android 实战:
kotlin
kotlin
// 并行请求,一个失败不影响另一个
suspend fun fetchAll(): List<Result<Data>> = supervisorScope {
urls.map { url -> async { fetch(url) } }
.awaitAll()
.map { it.getOrElse { e -> Result.failure(e) } }
}
// 串行步骤,任何一步失败回滚
suspend fun processWithRollback() = coroutineScope {
try {
val data = fetchData()
saveToCache(data)
notifyListeners(data)
} catch (e: Exception) {
rollback()
throw e
}
}
// 后台任务,失败静默重试
suspend fun backgroundTask() = supervisorScope {
launch {
retry(times = 3) { performBackgroundWork() }
}
}
// 坑:withContext 中的异常行为
suspend fun tricky() = coroutineScope {
launch {
withContext(Dispatchers.Default) {
throw Exception("Error in withContext")
}
}
launch {
delay(1000)
println("This won't run!") // 兄弟协程仍然被取消
}
}
// withContext 中的异常会传播到 launch,coroutineScope 取消所有子协程
面试加分:
withContext不创建新作用域,不影响兄弟协程coroutineScope会传播异常并取消所有子协程- 全局 CoroutineScope(ViewModel.viewModelScope)默认是 SupervisorJob
- 异常处理要在合适的作用域层级
CoroutineExceptionHandler只对根协程生效
2.4 select 表达式做什么的?什么场景必须用?
核心 :select 用于竞速场景------等待多个挂起操作,第一个完成的结果立即返回,其他未完成的继续执行(或被取消)。
kotlin
suspend fun selectWinner() {
val deferred1 = async { delay(100); "result1" }
val deferred2 = async { delay(200); "result2" }
val result = select<String> {
deferred1.onAwait { it }
deferred2.onAwait { it }
}
// "result1" 先返回,deferred2 可选择继续执行或取消
}
Android 实战:
kotlin
// 多数据源竞速:本地缓存 vs 网络请求
suspend fun getUser(userId: String): User {
val localDeferred = async(Dispatchers.IO) { local.getUser(userId) }
val remoteDeferred = async(Dispatchers.IO) { remote.fetchUser(userId) }
return select {
localDeferred.onAwait { user ->
local.cacheUser(user); user // 本地命中,保存后返回
}
remoteDeferred.onAwait { user ->
local.cacheUser(user); user // 网络更快,同步缓存后返回
}
}.also {
// 取消未完成的任务
if (!localDeferred.isCompleted) localDeferred.cancel()
if (!remoteDeferred.isCompleted) remoteDeferred.cancel()
}
}
// 超时控制
suspend fun <T> withTimeoutOrElse(timeout: Long, fallback: () -> T, block: suspend () -> T): T {
return select {
block().onAwait { it }
delay(timeout).onAwait { fallback() }
}
}
// 多支付渠道竞速
suspend fun selectWinner(): PaymentChannel = select {
async { alipayPay() }.onAwait { PaymentChannel("alipay", it) }
async { wechatPay() }.onAwait { PaymentChannel("wechat", it) }
async { unionPay() }.onAwait { PaymentChannel("union", it) }
}
面试加分:
- 需要所有结果用
awaitAll或flow { ... }.buffer()并发收集 - select 可用于 Channel 的多路复用(selectChannel)
onAwait等待 Deferred,onReceive等待 Channel- select 不是 suspend 函数,不会阻塞当前协程
kotlinx.coroutines.select需要单独依赖
三、性能与编译期优化
3.1 by lazy 三种线程安全模式怎么选?
核心:默认 SYNCHRONIZED 用双重检查锁保证线程安全;PUBLICATION 可能多次初始化但只使用第一个结果;NONE 无锁最快但不安全。
scss
// SYNCHRONIZED(默认):双重检查锁
val resource1 by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { HeavyResource() }
// PUBLICATION:可能多次初始化,只使用第一个
val resource2 by lazy(LazyThreadSafetyMode.PUBLICATION) { HeavyResource() }
// NONE:无锁,最快但不安全
val resource3 by lazy(LazyThreadSafetyMode.NONE) { HeavyResource() }
Android 实战:
kotlin
class MyViewModel : ViewModel() {
// 主线程创建,重复初始化代价小 → PUBLICATION
val repository by lazy(LazyThreadSafetyMode.PUBLICATION) { UserRepository() }
}
class MyFragment : Fragment() {
// 主线程创建,UI 初始化,且 Fragment 只在主线程访问 → NONE
private val adapter by lazy(LazyThreadSafetyMode.NONE) { MyAdapter() }
}
class App : Application() {
// Application 可能通过多路径初始化 → SYNCHRONIZED
val config by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { AppConfig() }
}
// NONE 模式字节码(最简单)
// private final Object resource3$delegate;
// public final Object getResource3() {
// Object $instance;
// if ((($instance = this.resource3$delegate) != null) ||
// ($instance = new Object()) != null) {
// return $instance; // 直接返回,无任何锁
// }
// }
面试加分:
- NONE 模式字节码最简单,无任何同步开销
- PUBLICATION 可能产生多个对象,但只有第一个被使用
- Application 单例推荐 SYNCHRONIZED,防止 ContentProvider 等多初始化路径
- ViewModel 创建在主线程,但构造可能被多次调用(配置变更)→ PUBLICATION
- 选错模式可能导致重复初始化或多线程问题
3.2 data class copy() 有哪些隐蔽的坑?
核心 :copy 是浅拷贝 ------嵌套对象共享引用;copy 产生的新对象与原对象 equals 为 true 时,StateFlow/collect 不会触发回调。
ini
data class Address(val city: String, val street: String)
data class Person(val name: String, val address: Address)
val alice = Person("Alice", Address("Beijing", "Main St"))
val bob = alice.copy(name = "Bob")
// bob.address === alice.address → true(嵌套对象是同一个引用!)
println(bob.address === alice.address) // true
// equals 去重问题
val stateFlow = MutableStateFlow(State(0))
stateFlow.value = stateFlow.value.copy(count = 0) // equals 为 true,不触发!
// copy() 显式传参 vs 默认参数
val state1 = State(0)
val state2 = state1.copy() // 新对象,equals 为 true
val state3 = state1.copy(count = 0) // 也是新对象,equals 为 true
Android 实战:
kotlin
// 正确的深拷贝
val bob = alice.copy(
name = "Bob",
address = alice.address.copy() // 嵌套也要 copy
)
// Immutable 模式(推荐)
class Person private constructor(
val name: String,
val address: Address
) {
companion object {
fun create(name: String, address: Address) = Person(name, address)
}
fun withName(newName: String) = Person(newName, address)
fun withAddress(newAddress: Address) = Person(name, newAddress)
}
// 序列化/反序列化深拷贝
val bob = alice.copyDeepClone() // 需要实现深拷贝方法
面试加分:
- 嵌套对象需要手动 copy 或用序列化/反序列化实现深拷贝
- StateFlow 用 version 字段解决 equals 去重问题
copy()默认参数只在未传参时使用,显式传相同值会产生新对象(但 equals 仍为 true)- Immutable 设计 + copy-on-write 是更彻底的解决方案
- 使用 copy 时要注意嵌套对象的深拷贝问题
3.3 Kotlin 空安全在编译期和运行期的保障分别是什么?
核心 :编译期靠类型系统强制检查 ,运行期靠 Intrinsics.checkNotNullParameter 等 intrinsic 函数。Java 互调、反射、多线程竞态、泛型擦除等场景会失效。
kotlin
// 编译期保障
val name: String? = null
println(name.length) // 编译错误!
// 运行期保障
fun processName(name: String) {
// 编译器插入:checkNotNullParameter(name, "name")
// 如果 name 为 null,抛出 IllegalArgumentException
}
// 运行期失效场景
val name: String = javaMethodReturningNull() // 编译通过,运行时 NPE
// 反射修改 val
val person = Person("Alice")
Person::class.java.getDeclaredField("name").apply {
isAccessible = true; set(person, null) // val 也能改!
}
// 多线程 TOCTOU
var value: String? = "initialized"
fun process() {
if (value != null) {
// Thread B: value = null
println(value!!.length) // 可能 NPE
}
}
// 泛型擦除
fun <T> printList(list: List<T>) {
// 无法区分 T 是 String 还是 String?
}
// 序列化/反序列化
val json = """{"data":null}"""
val response = ObjectMapper().readValue(json, Response::class.java)
// data 字段被反序列化为 null,但类型是 String(非空)
Android 实战:
kotlin
// 关键路径加运行时断言
fun processUser(user: User) {
requireNotNull(user.id) { "User ID must not be null" }
requireNotNull(user.email) { "User email must not be null" }
}
// 外部数据做契约验证
fun parseUserResponse(json: String): User {
return json.toUser() ?: throw IllegalStateException("Invalid user data")
}
// @JvmStatic 确保 Java 调用时也进行空检查
class UserConfig {
companion object {
@JvmStatic fun getInstance(): UserConfig = instance
}
}
面试加分:
- Java 注解(@Nullable/@NotNull)需要配置 Kapt/Kotlin 编译器才生效
- 泛型擦除导致
<String>和<String?>运行时无法区分 - 序列化/反序列化可能破坏空安全(Gson 默认将 null 反序列化为字段类型默认值)
@JvmStatic确保 Java 调用时也进行空检查- 反射可以修改任何字段,包括 val(通过 Unsafe)
3.4 集合操作在性能敏感场景怎么选?
核心 :多次中间操作 用 Sequence(懒执行),单次终态操作用 List(无额外开销)。小数据量用 ArrayMap/SparseArray 避免装箱开销。
scss
// Sequence:懒执行,只遍历一次
sequenceOf(1, 2, 3, 4, 5)
.map { it * 2 } // 装饰器,不执行
.filter { it > 4 } // 装饰器,不执行
.forEach { println(it) } // 按需执行
// List:每次操作都创建中间集合
listOf(1, 2, 3, 4, 5)
.map { it * 2 } // 创建 List(5)
.filter { it > 4 } // 创建 List(3)
.forEach { println(it) }
// 单次操作无区别
listOf(1, 2, 3).first() // 与 sequence.first() 几乎相同
// JMH 基准测试结果(10000 元素)
// List chain: ~0.5ms
// Sequence chain: ~0.2ms (60% faster)
Android 实战:
dart
// 小数据量 ArrayMap(<100 元素)
val arrayMap = ArrayMap<String, String>().apply {
put("key1", "value1")
put("key2", "value2")
}
// SparseArray 避免 Integer 装箱(比 HashMap 省 60% 内存)
val sparseArray = SparseArray<String>()
sparseArray.put(1, "value1")
val value: String? = sparseArray.get(1)
// 大数据量 HashMap
val hashMap = HashMap<String, String>()
repeat(1000) { put("key$it", "value$it") }
// 经验法则:
// < 100 元素:ArrayMap
// >= 100 元素:HashMap
// Integer/Int 作为 Key:SparseArray
面试加分:
- Sequence 中间操作是装饰器,终态操作才真正执行
- 经验法则:< 100 元素 ArrayMap 更快,>= 100 HashMap 更快
- SparseArray 比
Map<Int, V>节省 Integer 装箱开销 - Android 的
ArrayMap和SparseArray是内存敏感场景的优化利器 - Sequence 的惰性求值在大数据量或昂贵操作时效果显著
四、架构设计
4.1 如何设计类型安全的路由框架?
核心 :用 sealed class 定义路由表实现穷举检查,用 reified 泛型 传递目标类型,导航参数作为 data class 属性,编译期确保类型安全。
kotlin
sealed class Route {
@Route("home") data object Home : Route()
@Route("detail/{id}") data class Detail(val id: String) : Route()
@Route("settings") data object Settings : Route()
// 新增路由时,编译器强制你处理所有 when 分支
}
object Router {
inline fun <reified T : Route> navigate(context: Context) {
val route = T::class.java.getAnnotation(Route::class.java)
val path = route?.value?.parsePath(T::class) ?: "/unknown"
Navigation.findNavController(context).navigate(path)
}
private fun String.parsePath(clazz: KClass<out Route>): String {
return this.replace(Regex("\{(\w+)}")) { match ->
when (clazz) {
Route.Detail::class -> (clazz.objectInstance as? Route.Detail)?.id ?: ""
else -> ""
}
}
}
}
// 使用
navigate<Route.Detail> { id = "123" } // 类型安全
Android 实战:
kotlin
@OptIn(ExperimentalContracts::class)
inline fun <reified T : Route> navigateTo(context: Context, block: T.() -> Unit = {}) {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
val route = T::class.java.getAnnotation(Route::class.java)
check(route != null) { "Route annotation not found for ${T::class.java}" }
val routeInstance = when (val obj = T::class.objectInstance) {
is Route -> obj
else -> createRouteInstance<T>()
}
routeInstance.block()
navigate(context)
}
// 对比 ARouter
// | 维度 | ARouter | Kotlin 类型安全路由 |
// |-------------|----------------|-------------------|
// | 路由定义 | 注解 + 编译时生成 | sealed class 编译期穷举 |
// | 参数传递 | 字符串 key | 属性直接赋值 |
// | 类型安全 | 运行时检查 | 编译期检查 |
// | when 穷举 | 无 | 有 |
面试加分:
- sealed class 配合 when 实现穷举检查,新增路由时编译器强制处理
- reified 在编译时获取类型信息,避免字符串路径
- ARouter 等框架是运行时检查,Kotlin 类型安全方案更优
- 导航参数作为 data class 属性,有 IDE 自动补全和类型检查
- Navigation Component 的 SafeArgs 也是类型安全的方案
4.2 Kotlin Multiplatform 先共享哪层?
核心 :数据层(Model + Repository) 最安全,改动最小。Domain 层中等风险。UI 层(Compose Multiplatform)生态不成熟,暂不推荐。
arduino
// 共享策略决策
// 只是 Android 优化?→ 不需要 KMP,用 Kotlin DSL 优化构建
// 只需要共享数据层?→ ✅ 最安全
// - Entity/DTO 定义在 common
// - Repository 接口在 common,实现分开
// - Network/Database 具体实现 platform-specific
// 需要共享 Domain 层?→ ⚠️ 中等风险
// - Domain 逻辑跨平台
// - DI 需要 platform-specific 适配器
// 想共享 UI 层?→ ❌ 不推荐
// - 生态不成熟,库支持有限
// - 学习曲线陡峭
// - 调试困难
Android 实战:
kotlin
// expect/actual 机制
expect class Logger {
fun log(message: String)
fun setLevel(level: LogLevel)
}
enum class LogLevel { DEBUG, INFO, WARN, ERROR }
// androidMain
actual class Logger {
private val logger = android.util.Log
actual fun log(message: String) {
logger.d("Platform", message)
}
actual fun setLevel(level: LogLevel) {
// Android: 可以设置日志级别过滤
}
}
// 坑:expect 只能声明,不能写实现
// expect class Logger {
// fun log(message: String) // 不能有实现
// }
// Android 特有的 API 无法直接共享
expect fun getCurrentTime(): Long
// actual android
// actual fun getCurrentTime(): Long = System.currentTimeMillis()
// actual ios
// actual fun getCurrentTime(): Long = mach_absolute_time()
面试加分:
- expect/actual 维护成本高,新增方法需改所有平台
- Context、Lifecycle 等 Android 特有概念无法共享
- KMP 生态正在成熟,但库支持仍有限
- 建议从数据层切入,逐步扩展
- Kotlin 1.9+ 改进了 expect/actual 语法
4.3 如何设计可靠的"一次性事件"机制?
核心 :用 SharedFlow(replay=0) 配合 repeatOnLifecycle,确保事件只被消费一次,配置变更后可恢复。
kotlin
class SingleLiveEvent<T> {
private val _events = MutableSharedFlow<T>(replay = 0)
val events: SharedFlow<T> = _events.asSharedFlow()
fun emitEvent(event: T) = _events.tryEmit(event)
suspend fun sendEvent(event: T) = _events.emit(event)
}
// 方案对比
// | 方案 | 事件丢失风险 | 配置变更恢复 | 代码复杂度 |
// |-----------------|-------------|-----------|---------|
// | SingleLiveEvent | 无 | 重新发射 | 中 |
// | Event Wrapper | 无 | 重新发射 | 低 |
// | SharedFlow | 有(无消费者时)| 重新发射 | 低 |
// | Channel | 有(无消费者时)| 重新发射 | 中 |
Android 实战:
kotlin
sealed class UiEvent {
data class ShowToast(val message: String) : UiEvent()
data class ShowSnackbar(val message: String, val action: String? = null) : UiEvent()
data class Navigate(val route: Route) : UiEvent()
object NavigateBack : UiEvent()
}
class UiEventHandler(
private val snackbarHost: SnackbarHostState
) {
fun handleEvents(events: SharedFlow<UiEvent>, scope: CoroutineScope) {
scope.launch {
events.collect { event ->
when (event) {
is UiEvent.ShowToast -> showToast(event.message)
is UiEvent.ShowSnackbar -> snackbarHost.showSnackbar(event.message, event.action)
is UiEvent.Navigate -> navigateTo(event.route)
is UiEvent.NavigateBack -> navigateBack()
}
}
}
}
}
// Fragment 中使用
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiEvents.collect { /* 处理事件 */ }
}
}
// 权限请求结果
class PermissionViewModel : ViewModel() {
private val _permissionResult = MutableSharedFlow<PermissionResult>(replay = 0)
val permissionResult: SharedFlow<PermissionResult> = _permissionResult
}
面试加分:
- LiveData 事件会在配置变更后重复发射
- SharedFlow(replay=0) 无缓冲,有消费者才能接收
repeatOnLifecycle确保只在 STARTED 状态收集- replay=1 可让新订阅者收到最近一次状态
- 权限请求结果、Toast/Snackbar、导航事件都是一次性事件的典型场景
4.4 expect/actual 在 KMP 中怎么用?
核心:expect 声明公共接口,actual 提供平台实现。接口设计要谨慎,确保签名完全匹配。
kotlin
// commonMain
expect class DateFormatter {
fun formatDate(timestamp: Long): String
fun formatTime(timestamp: Long): String
fun formatDateTime(timestamp: Long): String
}
// androidMain
actual class DateFormatter {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
actual fun formatDate(timestamp: Long) = dateFormat.format(Date(timestamp))
actual fun formatTime(timestamp: Long) = timeFormat.format(Date(timestamp))
actual fun formatDateTime(timestamp: Long) = dateTimeFormat.format(Date(timestamp))
}
// iosMain
actual class DateFormatter {
private val dateFormatter = NSDateFormatter().apply { dateFormat = "yyyy-MM-dd" }
private val timeFormatter = NSDateFormatter().apply { dateFormat = "HH:mm:ss" }
actual fun formatDate(timestamp: Long): String {
return dateFormatter.stringFromDate(Date(timestamp / 1000.0))
}
actual fun formatTime(timestamp: Long): String {
return timeFormatter.stringFromDate(Date(timestamp / 1000.0))
}
actual fun formatDateTime(timestamp: Long): String {
return formatDate(timestamp) + " " + formatTime(timestamp)
}
}
Android 实战:
kotlin
// 坑 1:expect 类不能有默认实现
expect class Logger {
fun log(message: String) // ❌ 不能写实现
}
// 正确
expect class Logger {
fun log(message: String)
}
// 坑 2:actual 必须完全匹配签名
expect class Parser {
fun parse(date: String): Long
}
// ❌ 错误:参数类型不匹配
actual class Parser {
actual fun parse(timestamp: Long): String
}
// ✅ 正确
actual class Parser {
actual fun parse(date: String): Long
}
// 坑 3:平台实现差异过大
// 如果 Android 和 iOS 的行为差异太大,考虑拆分接口
面试加分:
- expect/actual 支持函数、类、属性
- 平台实现差异过大时应考虑拆分接口
- Kotlin 1.9+ 改进了 expect/actual 语法
- 测试需要在每个平台编写集成测试
- expect class 可以有 primary constructor
五、Android 实战场景深度
5.1 ViewModel 创建和销毁过程有哪些注意点?
核心 :by viewModels() 在 onViewCreated 时才初始化;SavedStateHandle 是配置变更后状态恢复的关键;理解 ViewModel 的生命周期边界。
kotlin
// by viewModels() Lazy 实现
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> ViewModelProvider.Factory)?
): Lazy<VM> = ViewModelLazy(
VM::class, // ViewModel 类
{ viewModelStore }, // ViewModelStore
{ factory ?: getDefaultViewModelProviderFactory() } // Factory
)
// ViewModelProvider.Factory 默认实现
class NewInstanceFactory : ViewModelProvider.Factory {
@Suppress("ClassNewInstance")
public open fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.newInstance()
}
}
Android 实战:
kotlin
// 带参数的 ViewModel
class DetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val userId: String
) : ViewModel() {
// SavedStateHandle 的使用
val user: String? = savedStateHandle["user_id"]
fun saveData(data: String) {
savedStateHandle["key"] = data
}
class Factory(private val userId: String) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return DetailViewModel(
SavedStateHandle.createHandle(null, null),
userId
) as T
}
}
}
class DetailFragment : Fragment() {
private val userId: String by navArgs()
private val viewModel: DetailViewModel by viewModels {
DetailViewModel.Factory(userId)
}
// ⚠️ onCreate 中访问可能还未初始化
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// viewModel 可能还未初始化!
}
// ✅ onViewCreated 后才初始化
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.initialize()
}
}
面试加分:
- onCreate 中访问 viewmodel 可能还未初始化,应在 onViewCreated 之后
- SavedStateHandle 依赖 CreationExtras 注入
- ViewModel 销毁只因配置变更,onCleared() 只在进程被销毁时调用
- Koin/Hilt 等 DI 框架简化了参数传递
- ViewModelProvider 用 key 区分不同实例
5.2 Delegates.observable 和 vetoable 实用场景?
核心 :observable 在属性变更后 触发回调,适合日志、验证、联动更新;vetoable 在变更前可否决,适合输入校验。
kotlin
// observable:监听变更
var count by observable(0) { property, oldValue, newValue ->
println("${property.name}: $oldValue -> $newValue")
}
count = 1 // 输出:Property count: 0 -> 1
count = 2 // 输出:Property count: 1 -> 2
// vetoable:变更前否决
var age by vetoable(0) { property, oldValue, newValue ->
newValue in 0..150 // 拒绝不合理的年龄
}
age = 25 // OK
age = 200 // 被否决,age 仍为 25
Android 实战:
kotlin
// 配置变更监听
class SettingsViewModel : ViewModel() {
var theme by observable(Theme.LIGHT) { _, old, new ->
if (old != new) _themeChanged.tryEmit(new)
}
var language by observable(Language.EN) { _, old, new ->
if (old != new) _languageChanged.tryEmit(new)
}
private val _themeChanged = MutableSharedFlow<Theme>(replay = 0)
val themeChanged: SharedFlow<Theme> = _themeChanged
}
// RecyclerView Adapter 数据变更
class SmartAdapter : RecyclerView.Adapter<SmartAdapter.ViewHolder>() {
private var _items: List<Item> by observable(emptyList()) { _, oldList, newList ->
val diffResult = DiffUtil.calculateDiff(ItemDiffCallback(oldList, newList))
submitList(newList)
diffResult.dispatchUpdatesTo(this)
}
val items: List<Item> get() = _items
fun updateItems(newItems: List<Item>) {
_items = newItems // 自动触发 diff
}
}
// 权限管理
class PermissionManager {
var grantedPermissions by vetoable(emptySet<String>()) { _, _, newValue ->
// 权限只能增加不能减少(简化版)
true
}
fun grantPermission(permission: String) {
grantedPermissions = grantedPermissions + permission
}
}
面试加分:
- observable 适合日志记录、跨组件联动、状态同步
- vetoable 适合输入校验(但更好的做法是用函数验证)
- Kotlin 属性代理支持自定义
getValue/setValue - 配合 StateFlow/SharedFlow 可实现响应式数据流
- DiffUtil.calculateDiff 需要 oldList 和 newList
5.3 如何实现类型安全的 Builder 模式?
核心 :用 DSL 风格 + 扩展函数,比 Java Builder 更简洁、类型安全更好。
kotlin
class HttpRequest private constructor(
val url: String,
val method: String,
val headers: Map<String, String>,
val body: String?
) {
class Builder {
private var url = ""
private var method = "GET"
private val headers = mutableMapOf<String, String>()
private var body: String? = null
fun url(url: String) = apply { this.url = url }
fun method(method: String) = apply { this.method = method }
fun header(key: String, value: String) = apply { headers[key] = value }
fun body(body: String) = apply { this.body = body }
fun build() = HttpRequest(url, method, headers, body)
}
}
fun httpRequest(block: HttpRequest.Builder.() -> Unit): HttpRequest {
return HttpRequest.Builder().apply(block).build()
}
// 使用
val request = httpRequest {
url("https://api.example.com/users")
method("POST")
header("Content-Type", "application/json")
header("Authorization", "Bearer token")
body("""{"name": "Alice"}""")
}
Android 实战:
kotlin
// Retrofit Interceptor 链配置
class InterceptorChainBuilder {
private val interceptors = mutableListOf<Interceptor>()
fun addInterceptor(interceptor: Interceptor) = interceptors.add(interceptor)
fun addInterceptor(block: Request.() -> Request) {
interceptors.add { chain ->
chain.proceed(chain.request.block())
}
}
fun build(): List<Interceptor> = interceptors.toList()
}
fun interceptorChain(block: InterceptorChainBuilder.() -> Unit): List<Interceptor> {
return InterceptorChainBuilder().apply(block).build()
}
val chain = interceptorChain {
addInterceptor { chain ->
println("Request: ${chain.request.url}")
chain.proceed(chain.request)
}
addInterceptor { chain ->
val auth = chain.request.header("Authorization")
?: return@addInterceptor chain.proceed(
chain.request.newBuilder()
.header("Authorization", "Bearer token")
.build()
)
chain.proceed(chain.request)
}
addInterceptor { chain ->
var request = chain.request
repeat(3) {
try {
return@addInterceptor chain.proceed(request)
} catch (e: IOException) {
if (it == 2) throw e
}
}
chain.proceed(request)
}
}
面试加分:
- DSL 用
apply块传递配置块,语法更自然 - 私有构造函数强制通过 Builder 创建
- Kotlin 的 lambda 尾调用语法使 DSL 更优雅
- 可嵌套 DSL 处理复杂配置结构
- Retrofit OkHttpClient 配置常用 DSL 风格
5.4 Kotlin 哪些特性是 Compose 的基础?
核心 :Compose 深度依赖 Kotlin 的多个特性:inline (性能优化)、reified (remember)、协程 (LaunchedEffect)、扩展函数 (Modifier)、委托(mutableStateOf by)。
kotlin
// @Composable 本质:编译器变换为状态机
@Composable
fun Greeting(name: String) {
Text("Hello, $name!")
}
// 编译器生成等价的状态机(简化)
class GreetingComposer(val composer: Composer, var changed: Int = 0) {
operator fun invoke(name: String) {
composer.start(123456) // 组 ID
if (changed != composer.changed) {
Text("Hello, $name!")
}
composer.end()
}
}
Android 实战:
kotlin
kotlin
@Composable
fun DataLoader(url: String) {
var data by remember { mutableStateOf<String?>(null) }
var error by remember { mutableStateOf<Throwable?>(null) }
LaunchedEffect(url) {
try {
data = fetchData(url)
} catch (e: Exception) {
error = e
}
}
DisposableEffect(Unit) {
val job = scope.launch { /* 后台任务 */ }
onDispose { job.cancel() }
}
}
// derivedStateOf 性能优化
@Composable
fun TodoList(todos: List<Todo>) {
// 反面:每次重组都过滤
val filteredTodos = todos.filter { it.isCompleted }
// 正面:只在 todos 变化时重新计算
val filteredTodos = remember(todos) {
derivedStateOf { todos.filter { it.isCompleted } }
}
}
// remember vs rememberSaveable
@Composable
fun Example() {
var count by remember { mutableStateOf(0) }
var config by rememberSaveable { mutableStateOf(Config()) }
// remember: 重组时保持
// rememberSaveable: 配置变更后也保持
}
// Kotlin 特性在 Compose 中的用途
// | Kotlin 特性 | Compose 中的用途 |
// |--------------|----------------------------|
// | inline | 性能优化,减少对象创建 |
// | reified | remember { derivedStateOf } |
// | 协程 | LaunchedEffect, rememberCoroutineScope |
// | 扩展函数 | Modifier.extension functions |
// | 委托 | mutableStateOf by |
面试加分:
remember跨重组保持状态,rememberSaveable配置变更也保持LaunchedEffect依赖 key 变化时重启协程derivedStateOf只在依赖变化时重新计算,减少不必要的重组- Compose Modifier 本质是扩展函数链:
Modifier.padding().background().clickable() - @Composable 函数不能递归调用,不能有默认值参数
5.5 伴生对象(companion object)高级用法?
核心 :实现 Factory 模式 、接口实现 、扩展函数,配合 @JvmStatic/@JvmField 控制 Java 互操作。
kotlin
class NetworkClient private constructor(
private val baseUrl: String,
private val timeout: Int
) {
companion object : ViewModelProvider.Factory {
private const val DEFAULT_URL = "https://api.example.com"
private const val DEFAULT_TIMEOUT = 30_000
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(NetworkClient::class.java) -> {
NetworkClient(DEFAULT_URL, DEFAULT_TIMEOUT) as T
}
else -> throw IllegalArgumentException("Unknown: $modelClass")
}
}
fun create(baseUrl: String, timeout: Int = DEFAULT_TIMEOUT) =
NetworkClient(baseUrl, timeout)
}
}
Android 实战:
kotlin
// 伴生对象扩展函数
class User private constructor(val name: String, val age: Int) {
companion object // 空伴生对象,允许外部扩展
}
fun User.Companion.create(name: String, age: Int): User {
require(age >= 0) { "Age must be non-negative" }
return User(name, age)
}
fun User.Companion.createRandom(): User {
return User.create(listOf("Alice", "Bob").random(), Random.nextInt(0, 100))
}
// Room TypeConverter
class Converters {
companion object {
@JvmStatic fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@JvmStatic fun dateToTimestamp(date: Date?): Long? = date?.time
@JvmStatic fun fromStringList(value: String?): List<String> = value?.split(",") ?: emptyList()
@JvmStatic fun toStringList(list: List<String>?): String? = list?.joinToString(",")
}
}
// @JvmField vs @JvmStatic
class Constants {
companion object {
@JvmField val API_KEY = "abc123" // 直接访问 Constants.API_KEY
@JvmStatic var baseUrl = "https://api.com" // Constants.getBaseUrl()
val version = "1.0" // Constants.Companion.getVersion()
}
}
// Fragment newInstance
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
companion object {
private const val ARG_USER_ID = "user_id"
private const val ARG_FROM_NOTIFICATION = "from_notification"
fun newInstance(userId: String, fromNotification: Boolean = false): DetailFragment {
return DetailFragment().apply {
arguments = Bundle().apply {
putString(ARG_USER_ID, userId)
putBoolean(ARG_FROM_NOTIFICATION, fromNotification)
}
}
}
}
}
面试加分:
- 伴生对象实现接口时,接口方法即静态方法
- @JvmField 直接生成静态字段,@JvmStatic 生成静态方法
- 私有伴生对象可被外部扩展
- Fragment.newInstance 工厂方法推荐放在 companion object
- @JvmOverloads 可生成多个重载方法
5.6 object 表达式在回调场景的最佳实践?
核心 :Lambda 用于单方法接口 ,object 表达式用于多方法接口 或需要持有状态 的场景。警惕内存泄漏------匿名 object 持有外部类引用。
kotlin
// Lambda:单方法接口
view.setOnClickListener { /* 处理点击 */ }
viewModelScope.launch { /* 协程 */ }
// object:多方法或需持有状态
val gestureDetector = GestureDetector(context, object : OnGestureListener {
override fun onDown(e: MotionEvent) = true
override fun onShowPress(e: MotionEvent) { }
override fun onSingleTapUp(e: MotionEvent) = false
override fun onScroll(...) = false
override fun onLongPress(e: MotionEvent) { }
override fun onFling(...) = false
})
Android 实战:
kotlin
kotlin
// 防止内存泄漏:使用 lifecycleScope + 生命周期感知
class MyFragment : Fragment() {
fun startTimerSafe() {
val owner = viewLifecycleOwner
lifecycleScope.launch {
delay(1000)
if (owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
updateUI()
}
}
}
// 反面:Handler.postDelayed 可能泄漏
fun startTimerUnsafe() {
handler.postDelayed(object : Runnable {
override fun run() {
// 匿名对象持有 Activity 引用
updateUI() // Activity 已销毁时崩溃
}
}, 1000)
}
}
// ViewPager2 多状态监听
viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
updateIndicator(position)
sendAnalytics("page_view", position)
}
override fun onPageScrollStateChanged(state: Int) {
when (state) {
SCROLL_STATE_IDLE -> { /* 滚动结束 */ }
SCROLL_STATE_DRAGGING -> { /* 开始拖动 */ }
SCROLL_STATE_SETTLING -> { /* 惯性滚动 */ }
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
updateParallax(position, positionOffset)
}
})
// Animation AnimatorListener
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
view.visibility = View.VISIBLE
}
override fun onAnimationEnd(animation: Animator) {
onComplete()
}
// AnimatorListenerAdapter 只重写需要的方法
})
面试加分:
- 匿名 object 持有外部类引用,可能导致 Activity/Fragment 内存泄漏
- Handler.postDelayed 用 object 表达式要小心生命周期
DisposableEffect替代手动清理,更安全- AndroidX 提供
AnimatorListenerAdapter只重写需要的方法 registerOnPageChangeCallback需要在onDestroyView注销