性能优化 / 稳定性 — 面试题篇
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
Q1: 冷启动优化做过哪些?怎么衡量效果?
考察点:启动优化实战经验
完整回答:
冷启动全链路:Zygote fork → Application 初始化 → Activity 创建 → 首帧绘制。
优化手段:
- Application.onCreate:SDK 异步初始化(无依赖的放子线程)、延迟初始化(非必要的延迟到首帧后)、任务编排(DAG 处理依赖关系)
- ContentProvider 合并:用 App Startup 合并多个 Provider 为一个
- Activity:减少布局层级、ViewStub 延迟加载非首屏内容、异步 inflate
- 类加载优化:提前加载热点类、MultiDex 优化
- 闪屏页:设置 windowBackground 为品牌图,视觉上"秒开"
衡量指标:
- TTID:首帧显示时间(
adb shell am start -W的 TotalTime) - TTFD:完整内容显示时间(
reportFullyDrawn()) - 使用 Systrace/Perfetto 分析主线程耗时分布
追问:异步初始化怎么处理有依赖关系的 SDK?
用 DAG(有向无环图)编排。每个初始化任务声明依赖的前置任务,框架拓扑排序后并行执行无依赖的任务,有依赖的等前置完成后再执行。类似 Gradle 的 Task 依赖。
Q2: 内存泄漏怎么排查?常见的泄漏场景?
考察点:内存优化能力
完整回答:
排查工具:
- LeakCanary:自动检测 Activity/Fragment 泄漏,展示引用链
- Android Studio Profiler:dump heap,分析对象引用
- MAT(Memory Analyzer Tool):分析 hprof 文件,查找 GC Root 引用链
常见泄漏场景:
- Handler:非静态内部类持有 Activity 引用,Message 在队列中等待 → 用静态内部类 + WeakReference + onDestroy 清除消息
- 单例持有 Context:传了 Activity Context → 改用 Application Context
- 匿名内部类:Runnable、Callback 持有外部 Activity → 用静态类或在 onDestroy 取消
- 未注销监听:EventBus、BroadcastReceiver、SensorManager → onDestroy 中注销
- WebView:WebView 内部持有 Activity → 独立进程或动态添加/移除
- 集合类:只添加不移除 → 及时清理
追问:LeakCanary 的原理?
- 注册 ActivityLifecycleCallbacks 监听 onDestroy
- 将销毁的 Activity 包装为 WeakReference,关联 ReferenceQueue
- 延迟 5 秒检查 ReferenceQueue,如果 WeakReference 没入队说明未被回收
- 手动 GC 后再检查,仍未回收则 dump hprof
- 使用 Shark 库分析 hprof,找到 GC Root 到泄漏对象的最短引用链
Q3: ANR 是什么?怎么排查?
考察点:ANR 分析能力
完整回答:
ANR(Application Not Responding)是应用在规定时间内没有响应系统事件:
- 输入事件(触摸/按键):5 秒
- BroadcastReceiver:前台 10 秒,后台 60 秒
- Service:前台 20 秒,后台 200 秒
排查步骤:
- 获取 ANR 日志:
/data/anr/traces.txt或线上监控平台 - 分析主线程堆栈,看主线程在做什么:
- 死锁:
waiting to lock <0x...> held by thread X - IO 阻塞:文件读写、数据库查询在主线程
- Binder 调用:跨进程调用对端响应慢
- CPU 密集:主线程做大量计算
- 死锁:
- 查看 CPU 使用率:如果 CPU 使用率很高,可能是计算密集;如果很低,可能是锁等待或 IO
预防措施:
- 主线程不做 IO、网络、数据库操作
- 使用 StrictMode 检测主线程违规操作
- BroadcastReceiver 中不做耗时操作(用 goAsync 或转发给 Service/WorkManager)
- 监控主线程消息处理耗时(Looper printer 或 Choreographer)
追问:线上 ANR 怎么监控?
- FileObserver 监听
/data/anr/目录变化 - 主线程 Watchdog:子线程定期向主线程 post 消息,超时未执行则判定卡顿/ANR
- 使用 ANR-WatchDog 库或自建监控
Q4: 卡顿优化怎么做?怎么监控帧率?
考察点:渲染性能优化
完整回答:
卡顿原因:一帧超过 16.6ms(60fps)。
监控方式:
- Choreographer.FrameCallback:计算相邻帧的时间差,超过 16.6ms 即掉帧
- Looper printer:监控主线程每个 Message 的处理耗时(BlockCanary 原理)
- FrameMetrics API(API 24+):精确获取每帧各阶段耗时
优化手段:
- 布局优化:减少层级(ConstraintLayout)、移除过度绘制(
Debug GPU Overdraw)、ViewStub 延迟加载 - 主线程优化:IO/计算移到子线程、减少主线程锁等待
- RecyclerView 优化:setHasFixedSize、DiffUtil、预加载、共享 RecycledViewPool
- 减少内存抖动:避免在 onDraw/onBindViewHolder 中创建对象,复用对象
- 图片优化:合适的采样率、RGB_565、inBitmap 复用
追问:Systrace 怎么用?
python systrace.py -t 5 -o trace.html gfx view wm am在 Chrome 中打开 trace.html,查看主线程的方法调用耗时。关注:
- 绿色帧:正常(<16ms)
- 黄色/红色帧:掉帧
- 长条方法:耗时操作
Q5: Bitmap 内存怎么计算?怎么优化?
考察点:Bitmap 内存管理
完整回答:
内存计算:宽 × 高 × 每像素字节数
像素格式:
- ARGB_8888:4 字节/像素(默认)
- RGB_565:2 字节/像素(无透明通道)
- ALPHA_8:1 字节/像素
注意:从资源加载时会根据 dpi 缩放。如果图片在 mdpi(160) 目录,设备是 xxhdpi(480),图片会放大 3 倍,内存 = 原始宽×3 × 原始高×3 × 4。
优化手段:
- 采样加载:
inSampleSize降低分辨率 - 合适的像素格式:不需要透明通道用 RGB_565
- inBitmap 复用:复用已有 Bitmap 的内存块
- 及时回收:不用时
bitmap.recycle() - 放在合适的资源目录:避免不必要的缩放
- 使用 Glide/Coil:自动管理生命周期和缓存
Q6: 包体积优化做过哪些?
考察点:工程化能力
完整回答:
- 代码压缩:R8/ProGuard 移除无用代码、混淆、优化字节码
- 资源压缩:
shrinkResources true移除无用资源 - 图片优化:PNG → WebP(体积减少 25-35%)、TinyPNG 压缩、矢量图替代小图标
- 资源混淆:AndResGuard 将资源名缩短(res/drawable/icon → r/d/a)
- so 库:只保留 arm64-v8a(主流架构)、按需加载 so
- 动态下发:大资源/功能模块通过 Dynamic Feature 或插件化按需下载
- 代码优化:移除无用依赖、避免重复依赖
分析工具:Android Studio 的 APK Analyzer,查看各部分占比(dex/res/lib/assets)。
加分点:提到 R8 的 Full Mode 可以进一步优化(移除未使用的类成员),以及 Baseline Profile 优化运行时性能。
Q7: 内存抖动是什么?怎么检测和解决?
考察点:内存优化
完整回答:
内存抖动是短时间内大量创建和销毁对象,导致频繁 GC。表现为 Memory Profiler 中锯齿状的内存曲线。
常见场景:
- onDraw 中创建 Paint/Rect 对象(每帧调用)
- 循环中创建字符串(应用 StringBuilder)
- RecyclerView.onBindViewHolder 中创建对象
- 频繁装箱拆箱(int → Integer)
检测:Android Studio Profiler → Memory → 观察内存曲线是否呈锯齿状 → Allocations 面板定位频繁分配的对象。
解决:对象复用(在构造函数中创建,onDraw 中复用)、使用对象池、避免不必要的装箱。
Q8: 过度绘制是什么?怎么优化?
考察点:渲染优化
完整回答:
过度绘制(Overdraw)是同一个像素在一帧中被绘制多次。比如一个白色背景上放一个白色卡片,卡片区域被绘制了两次。
检测:开发者选项 → 调试 GPU 过度绘制。颜色含义:
- 无色:无过度绘制
- 蓝色:1 次过度绘制
- 绿色:2 次
- 粉色:3 次
- 红色:4 次以上
优化手段:
- 移除不必要的背景(Activity 默认有 Window 背景,如果布局有自己的背景可以移除 Window 背景)
android:background="@null"或window.setBackgroundDrawable(null)- 使用
clipRect限制绘制区域 - 减少布局层级(ConstraintLayout 替代嵌套)
- ViewStub 延迟加载不可见的布局
Q9: 线上 Crash 怎么治理?有什么方法论?
考察点:稳定性治理
完整回答:
治理流程:
监控:接入 Crash 监控 SDK(Firebase Crashlytics、Bugly),收集崩溃堆栈、设备信息、用户操作路径
分类:按影响面排序
- Top Crash:影响用户数最多的崩溃优先修复
- 按模块/页面分类,找到问题集中的区域
分析:
- Java Crash:直接看堆栈定位代码
- Native Crash:addr2line 还原符号,分析 tombstone
- ANR:分析 traces.txt 主线程堆栈
修复:
- 紧急问题:热修复(Tinker/Sophix)
- 常规问题:下个版本修复
预防:
- 代码规范 + Lint 检查
- 单元测试覆盖核心逻辑
- 灰度发布(先 1% → 10% → 全量)
- 质量门禁(Crash 率超标则阻止发版)
追问:Crash 率怎么计算?业界标准是多少?
Crash 率 = 崩溃用户数 / DAU × 100%。业界标准:Crash Rate < 0.1%(千分之一),ANR Rate < 0.3%。
Q10: 网络优化做过哪些?
考察点:网络性能
完整回答:
连接优化:
- HTTP/2 多路复用(一个连接并行多个请求)
- 连接池复用(OkHttp 默认 5 个空闲连接)
- DNS 预解析 + 缓存(避免 DNS 查询耗时)
数据优化:
- Protobuf 替代 JSON(体积减少 60-90%)
- gzip 压缩请求/响应体
- 增量更新(只传变化的数据)
缓存优化:
- HTTP 缓存(Cache-Control)
- 本地缓存(先展示缓存,后台更新)
- 预加载(提前请求下一页数据)
弱网优化:
- 超时时间自适应(WiFi 短,移动网络长)
- 请求优先级(核心请求优先)
- 失败重试(指数退避)
- 降级策略(弱网下降低图片质量)
监控:
- OkHttp EventListener 监控各阶段耗时
- 统计请求成功率、平均耗时、错误码分布
实习面试补充:性能与稳定性入门高频题
实习面试不会默认要求你做过完整线上治理,但会看你是否知道卡顿、内存泄漏、ANR 的基本原因和排查方向。
Q11: 什么是 ANR?常见原因有哪些?
考察点:稳定性基础
完整回答:
ANR 是 Application Not Responding,表示应用在规定时间内没有响应系统输入或组件调度。常见场景:
- 主线程做耗时操作,比如网络请求、数据库读写、大文件 IO。
- 主线程等待锁,锁被子线程长期持有。
- BroadcastReceiver 执行太久。
- Service 生命周期方法中执行耗时任务。
解决思路是减少主线程耗时,把耗时任务放到子线程,必要时通过日志、Trace、ANR traces 文件定位主线程卡在哪里。
追问:ANR 和 Crash 的区别?
Crash 是应用异常退出;ANR 是应用没有及时响应,系统弹出无响应对话框,用户可以选择等待或关闭。
Q12: 常见内存泄漏场景有哪些?
考察点:内存管理基础
完整回答:
常见内存泄漏包括:
- 静态变量持有 Activity 或 View。
- 匿名内部类、Handler、Runnable 长时间持有 Activity。
- 注册监听、广播、观察者后没有取消。
- Fragment 的 ViewBinding 在
onDestroyView后没有释放。 - 单例中保存了短生命周期对象。
修复原则是:长生命周期对象不要持有短生命周期对象;需要注册的资源要在合适生命周期取消。
加分点:可以使用 LeakCanary 在开发阶段发现泄漏。
Q13: 页面卡顿一般怎么排查?
考察点:性能排查思路
完整回答:
先判断卡顿发生在什么场景:启动、列表滑动、页面切换、图片加载还是动画。常见排查方向:
- 主线程是否有耗时任务。
- RecyclerView 是否重复创建对象、频繁全量刷新。
- 图片是否过大,是否在主线程解码。
- 布局是否层级过深或过度绘制。
- 是否频繁触发布局重绘。
简单优化包括:耗时任务异步化、使用 DiffUtil 局部刷新、压缩图片、减少嵌套布局、避免在 onDraw 中创建对象。
