Android 面经
核心考点
1. Activity & Fragment 生命周期
Activity: onCreate → onStart → onResume → [运行] → onPause → onStop → onDestroy
Fragment: onAttach → onCreate → onCreateView → onViewCreated → onStart → onResume
→ onPause → onStop → onDestroyView → onDestroy → onDetach
- A 跳 B:A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop
- 按 Back 返回:B.onPause → A.onRestart → A.onStart → A.onResume → B.onStop → B.onDestroy
2. 内存泄漏常见场景
- 静态变量持有 Activity 引用
- 非静态内部类(Handler/AsyncTask)持有外部类引用
- 未注销的监听器、广播、回调
- 资源(Cursor、Stream、Bitmap)未及时释放
- 解决:使用 WeakReference、在 onDestroy 中解绑
3. Handler / Looper / MessageQueue
- Looper 在线程中维护 MessageQueue,loop() 死循环取消息
- Handler 发送/处理消息,绑定到创建它的线程 Looper
- 主线程 Looper 由系统启动时初始化(ActivityThread.main)
4. 协程 (Kotlin Coroutines)
- 轻量级线程,挂起不阻塞线程
Dispatchers.Main / IO / Default:分别用于 UI、IO 密集、CPU 密集viewModelScope / lifecycleScope:自动跟随生命周期取消Flow:冷流,适合异步数据流;StateFlow/SharedFlow:热流
5. Jetpack 核心组件
| 组件 | 作用 |
|---|---|
| ViewModel | 跨配置变化保留数据 |
| LiveData | 生命周期感知的可观察数据 |
| Room | SQLite ORM |
| Navigation | Fragment 导航管理 |
| WorkManager | 后台任务调度 |
| Compose | 声明式 UI 框架 |
6. 性能优化
- ANR 触发:主线程 5s 无响应(耗时操作放子线程)
- 过度绘制:减少层级,避免不必要背景,开启 GPU 调试
- 启动优化:懒加载、异步初始化、减少冷启动耗时
- 内存优化:Bitmap 压缩复用,避免内存抖动
7. 高频面试题
- 进程间通信 (IPC):AIDL、Messenger、ContentProvider、BroadcastReceiver
- 四大组件:Activity、Service、BroadcastReceiver、ContentProvider
- View 绘制流程:measure → layout → draw
- RecyclerView vs ListView:RecyclerView 解耦更彻底,支持多种布局、动画、局部刷新
Android 性能优化专题
性能优化全攻略
1. 渲染性能
过度绘制(Overdraw)
- 开启:开发者选项 → 调试 GPU 过度绘制
- 蓝色=1x,绿色=2x,粉色=3x,红色=4x+(红色区域需优化)
- 方案:移除不必要的背景色,减少布局层级,使用
clipRect限制绘制区域
布局优化
- 使用
ConstraintLayout替代嵌套 LinearLayout,减少层级 include/merge/ViewStub复用和延迟加载布局- 使用
Hierarchy Viewer/Layout Inspector分析布局层级
列表优化
- RecyclerView:合理设置
ViewHolder复用 DiffUtil.Callback精准局部刷新,避免notifyDataSetChangedsetHasFixedSize(true):item 大小固定时跳过 requestLayout- 预取:
RecyclerView.setItemViewCacheSize+RecycledViewPool
2. 启动优化
启动类型
- 冷启动:进程从零开始(最慢,重点优化)
- 温启动:进程存在,Activity 需重建
- 热启动:Activity 在后台,直接前台
冷启动优化
- Application
onCreate中异步初始化三方库(WorkManager / 协程) - 减少主线程耗时操作(SP 改 MMKV,IO 操作移至子线程)
- 使用 SplashScreen API(Android 12+)
- 启动页
windowBackground设为占位图,消除白屏
测量工具
adb shell am start -W com.example/.MainActivity
# ThisTime: 此次 Activity 启动时间
# TotalTime: 整个启动过程时间
3. 内存优化
- Bitmap 优化:
inSampleSize压缩,RGB_565替代ARGB_8888(省一半内存) - 内存泄漏检测:LeakCanary 自动检测,
adb shell dumpsys meminfo分析 - 内存抖动:避免在
onDraw/循环中创建对象,使用对象池 - LruCache:内存缓存图片,基于最近最少使用淘汰
4. 电量与网络优化
- Doze 模式:系统限制后台网络/CPU,使用 WorkManager 适配
- 批量网络请求:合并请求减少无线电激活次数
- 图片格式:WebP 替代 PNG/JPEG,体积减少约 30%
- HTTP/2:多路复用减少连接建立开销
- OkHttp 缓存:离线可用 + 减少重复请求
5. ANR 排查
# 查看 ANR 日志
adb pull /data/anr/traces.txt
# 常见原因
# 1. 主线程 IO(网络/磁盘)
# 2. 主线程等待锁
# 3. Binder 调用耗时(跨进程)
# 4. 主线程处理大量 Message
- StrictMode 开发期检测主线程 IO
- Systrace / Perfetto 分析线程状态和方法耗时
6. 高频面试题
- 如何检测内存泄漏? LeakCanary + MAT(Memory Analyzer Tool)分析 heap dump
- 卡顿原理是什么? 主线程一帧(16.6ms)内未完成 measure/layout/draw
- 为什么不能在子线程更新 UI? ViewRootImpl 检查线程,invalidate 触发检查
- Bitmap 如何高效加载大图? BitmapFactory.Options.inSampleSize 按比例采样
Android 架构模式(MVC / MVP / MVVM / MVI)
Android 架构演进
1. MVC
Model ← Controller → View
↑ ↓
└───────────────┘
- Android 中 Activity 既是 Controller 又是 View,导致职责混乱
- 随业务增长 Activity 代码膨胀,难以测试
2. MVP
View (Activity/Fragment)
↕ 接口
Presenter (业务逻辑)
↕
Model (数据层)
- View 和 Presenter 通过接口解耦,便于单元测试
- 缺点:接口类爆炸,Presenter 持有 View 引用需注意内存泄漏
3. MVVM(Google 官方推荐)
View (Activity/Fragment)
↓ 观察
ViewModel (持有 LiveData/StateFlow)
↕
Repository
↕
Remote DataSource / Local DataSource
ViewModel 核心优势
- 生命周期感知:屏幕旋转不丢失数据
- 不持有 View 引用,无内存泄漏风险
SavedStateHandle:进程被杀后恢复状态
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.update { it.copy(loading = true) }
val user = repo.getUser(id)
_uiState.update { it.copy(user = user, loading = false) }
}
}
}
4. MVI(单向数据流)
User Action (Intent)
↓
ViewModel 处理 → 新 State
↓
View 渲染
- State:UI 的完整快照,单一数据源
- Intent:用户操作,密封类描述所有可能事件
- 单向数据流:状态不可变,每次更新生成新 State
- 优点:状态可预测、易于调试和测试
- 代表库:Orbit MVI、MVI Kotlin
sealed class UserIntent {
data class LoadUser(val id: String) : UserIntent()
object Logout : UserIntent()
}
data class UserState(
val user: User? = null,
val loading: Boolean = false,
val error: String? = null
)
5. Clean Architecture 分层
Presentation Layer (ViewModel + UI)
↓
Domain Layer (UseCase + Entity) ← 纯 Kotlin,无 Android 依赖
↓
Data Layer (Repository + DataSource)
- UseCase(交互器):封装单一业务规则,可单独测试
- Repository:抽象数据来源,协调本地缓存和远端数据
- 依赖规则:内层不依赖外层
6. Hilt 依赖注入
@HiltAndroidApp class App : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModel: UserViewModel
}
@Module @InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()...
}
7. 高频面试题
- ViewModel 为什么能在屏幕旋转后存活? 存储在
ViewModelStore中,与 Activity 生命周期解绑 - LiveData 和 StateFlow 如何选择? 新项目推荐 StateFlow(更强大,支持初始值,协程原生)
- Repository 的作用? 单一数据源,屏蔽本地/远端差异,统一数据格式
- UseCase 一定需要吗? 简单场景可省略,复杂业务规则建议抽取便于测试
Jetpack Compose 深入面经
Jetpack Compose 核心知识
1. 重组原理
什么时候会重组?
- 状态变化时,依赖该状态的 Composable 会重组
- Compose 通过水平比较(Structural Equality)决定是否跳过重组
如何避免不必要的重组
// 1. @Stable / @Immutable 标记类,告知 Compose 安全跳过
@Stable // 所有 public property 变化时都通知 Compose
data class User(val name: String, val age: Int)
// 2. remember 缓存计算结果
val sortedList = remember(list) { list.sorted() }
// 3. derivedStateOf 对 state 派生新 state
val isScrolled = remember { derivedStateOf { scrollState.value > 0 } }
// 4. key() 给列表项设置稳定标识
Column {
items.forEach { item ->
key(item.id) { // 避免内容相同时不必要的重组
ItemComposable(item)
}
}
}
2. 状态管理
状态提升(State Hoisting)
// 非受控组件(自己持有状态)
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}
// 受控组件(状态提升到调用者)
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("$count") }
}
// 调用者
@Composable
fun ParentScreen() {
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}
ViewModel 与 Compose 集成
@Composable
fun OrderScreen(viewModel: OrderViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is Event.NavigateToSuccess -> navController.navigate("success")
is Event.ShowError -> showSnackbar(event.msg)
}
}
}
when (uiState) {
is Loading -> CircularProgressIndicator()
is Success -> OrderContent((uiState as Success).order)
is Error -> ErrorView((uiState as Error).message)
}
}
3. 布局与修饰器
// Modifier 顺序很重要!
Box(
Modifier
.size(100.dp) // 先设大小
.background(Color.Blue) // 再设背景(背景在内容区域内)
.padding(8.dp) // 再设内边距
.clickable { } // 点击区域包含 padding
)
// Modifier 和 UI 组件顺序的弱和强规则
// padding -> background 让内边距内没有背景
Text(
text = "Hello",
modifier = Modifier
.background(Color.Red) // 背景包含 padding 内容
.padding(16.dp)
)
自定义 Layout
@Composable
fun MyCustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOf { it.width }
val height = placeables.sumOf { it.height }
layout(width, height) {
var y = 0
placeables.forEach {
it.placeRelative(0, y)
y += it.height
}
}
}
}
4. 动画系统
// 状态动画
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = tween(durationMillis = 300)
)
// 可见性动画
AnimatedVisibility(
visible = visible,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
MyContent()
}
// 转场动画
AnimatedContent(
targetState = currentPage,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) { page ->
PageContent(page)
}
// 共享元素过渡
// Screen A
Box(Modifier.sharedElement(rememberSharedContentState("image"), ...) ) {
Image(...)
}
// Screen B 中同样的 sharedElement key 就会自动过渡
5. 性能优化
// 1. 长列表用 LazyColumn
LazyColumn {
items(items = list, key = { it.id }) { item -> // key 必须提供
ItemCard(item)
}
}
// 2. 避免在 composable 中读取 MutableList
// bad: list.forEach { ... } -> 无法追踪变化
// good: val items by rememberUpdatedState(newItems)
// 3. 使用 Baseline Profiles 提升启动性能
// 在 macrobenchmark 中定义核心路径,编译器预编译这些类
// 4. 不要在 Composable 中做老不必要的异步工作
// bad: 在 composable 中直接 launch
// good: 使用 LaunchedEffect(key) { ... }
// 5. 提副本加载验证
CompositionLocal
6. Compose 中的副作用
// LaunchedEffect:进入居和成立时执行,key 变化时重新执行
LaunchedEffect(userId) {
viewModel.loadUser(userId) // 副作用都在协程中执行
}
// SideEffect:每次重组成功后执行,不可以挂起
SideEffect {
analytics.reportScreen("OrderScreen") // 同步,安全访问外部系统
}
// DisposableEffect:组件离开时需清理
DisposableEffect(sensor) {
sensor.start()
onDispose { sensor.stop() } // 离开成居时执行
}
// rememberCoroutineScope:获取协程作用域,用于事件回调中
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { doSomething() } }) { ... }
7. 资深面试题
- Compose 与传统 View 的格局算法比较?
- 传统 View:Measure(36项 spec) -> Layout -> Draw,可多次 Measure
- Compose:单次 Measure(展开 Composable) -> 分配大小 -> 放置 -> 绘制
- Compose 由设计上保证单次 Measure,性能更可预测
- remember 在配置变化后是否会清除?
- 默认会清除,用
rememberSaveable写入 Bundle 保持状态 - 复杂对象用
Saver接口自定义序列化
- 默认会清除,用
- derivedStateOf 和 remember { ... } 的区别?
remember缓存结果,依赖的 key 变化时重新计算derivedStateOf用于导出 state,内部 state 变化时才重算,避免不必要重组derivedStateOf应包在remember { }里
- 如何在 Compose 中使用传统 View?
AndroidView { context -> CustomView(context).apply { ... } }AndroidViewBinding使用 ViewBindingsetViewCompositionStrategy管理成居策略
Kotlin 协程与 Flow 深入面经
Kotlin 协程深水区
1. 协程底层原理
CPS 变换(Continuation Passing Style)
- Kotlin 编译器将 suspend 函数变换为状态机
- 每个 suspend 点都是一个状态,函数被切分成多段
// 原始代码
suspend fun fetchUser(id: String): User {
val token = getToken() // suspend 点 1
val user = api.get(id) // suspend 点 2
return user
}
// 编译后(简化伪代码)
fun fetchUser(id: String, continuation: Continuation<User>) {
when (continuation.label) {
0 -> {
continuation.label = 1
getToken(continuation) // 挂起等待
}
1 -> {
val token = continuation.result as String
continuation.label = 2
api.get(id, continuation)
}
2 -> {
continuation.resume(continuation.result as User)
}
}
}
挂起与恢复
- suspend 函数调用后,当前线程不阻塞,返回
COROUTINE_SUSPENDED标记 - IO 完成后,通过
continuation.resume(result)恢复执行 - 恢复时可能在不同线程(取决于 Dispatcher)
2. Flow 深入
冷流 vs 热流
// 冷流:collect 时才执行,每个 collector 独立
val coldFlow = flow {
println("开始生产")
emit(1)
emit(2)
}
// 没有 collect 时,内部代码不执行
// StateFlow:热流,始终有最新值
val stateFlow = MutableStateFlow(0)
// SharedFlow:热流,replay 控制新订阅者能收到的历史消息数
val sharedFlow = MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 64)
背压处理
// buffer:生产者和消费者并发运行
flow { emit(heavyData()) }
.buffer(Channel.UNLIMITED)
.collect { process(it) }
// conflate:只保留最新值,丢弃中间值(适合 UI 状态)
stateFlow.conflate().collect { render(it) }
// collectLatest:新值到来时取消旧的处理
stateFlow.collectLatest { slowRender(it) } // 只完成最新一次
channelFlow 并发发射
// 允许在不同协程中 emit
val result = channelFlow {
launch { send(fetchFromNetwork()) } // 并发
launch { send(fetchFromCache()) } // 并发
}
3. 异常处理
// SupervisorJob:子协程独立失败
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch { failingTask() } // 失败不影响其他子协程
scope.launch { normalTask() } // 继续正常运行
// CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, e ->
Log.e("TAG", "Uncaught exception", e)
}
scope.launch(handler) { riskyWork() }
// async 的异常在 await 时抛出
val deferred = async { riskyTask() }
try {
val result = deferred.await()
} catch (e: Exception) {
// 处理异常
}
// Flow 的异常处理
flow { emit(riskyOperation()) }
.catch { e -> emit(defaultValue) } // 只捕获 catch 上游异常
.collect { }
4. Dispatcher 选择
| Dispatcher | 线程池 | 适用场景 |
|---|---|---|
| Main | 主线程 | UI 操作、轻量任务 |
| IO | 64 个线程 | 网络、文件读写 |
| Default | CPU 核数 | 计算密集 |
| Unconfined | 不指定 | 测试用 |
// limitedParallelism 限制并发度
val limitedIO = Dispatchers.IO.limitedParallelism(4)
withContext(limitedIO) { /* 最多 4 个线程并发执行 */ }
5. 结构化并发
viewModelScope.launch {
// 并发执行两个请求
val userDeferred = async { fetchUser() }
val postsDeferred = async { fetchPosts() }
// 任一失败,另一个自动取消
val user = userDeferred.await()
val posts = postsDeferred.await()
_uiState.value = Success(user, posts)
}
// coroutineScope:子协程全部完成才返回
suspend fun loadData() = coroutineScope {
val a = async { taskA() }
val b = async { taskB() }
Pair(a.await(), b.await()) // 任一失败都会取消其他
}
6. ViewModel 中的实践
class OrderViewModel(
private val repo: OrderRepository,
savedState: SavedStateHandle
) : ViewModel() {
private val orderId = savedState.get<String>("orderId")!!
// StateFlow 替代 LiveData
private val _uiState = MutableStateFlow<UiState>(Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 一次性事件用 SharedFlow
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
init {
loadOrder()
}
private fun loadOrder() = viewModelScope.launch {
_uiState.value = Loading
try {
val order = repo.getOrder(orderId)
_uiState.value = Success(order)
} catch (e: Exception) {
_uiState.value = Error(e.message)
}
}
fun submitOrder() = viewModelScope.launch {
try {
repo.submit(orderId)
_events.emit(Event.NavigateToSuccess)
} catch (e: Exception) {
_events.emit(Event.ShowError(e.message))
}
}
}
7. 资深面试题
- suspend 函数为什么不能在普通函数中调用?
- 编译后带 Continuation 参数,需要协程上下文提供该对象
- 景默认解决:用
runBlocking桥接(但会阻塞线程,仅用于测试)
- StateFlow vs LiveData 如何选择?
- 新项目一律推荐 StateFlow:要求初始值、协程原生、功能更强完整
- LiveData 主要优势是自动处理生命周期,StateFlow 必须配合
repeatOnLifecycle
- Flow 的 catch 操作符为什么只捕上游异常?
- Flow 是流式数据,cat操作符在管道中,只能捕获它上游的异常
- 下游(collect 中)的异常不会被捕获,需单独处理
- 协程和线程的核心区别?
- 协程是语言级的轻量调度单元,运行在线程之上
- 一个线程可运行数千个协程
- 协程切换无需内核介入,开销远小于线程切换