性能优化
性能优化是中级面试的高频区,也是你的潜在主场——风控 SDK 对体积/性能极度敏感,你的经验比一般应用开发者更有说服力。
一、启动优化
启动分类:
- 冷启动:进程不存在,需创建进程 + Application + Activity,最慢。
- 温启动:进程在但 Activity 重建。
- 热启动:Activity 还在,仅恢复,最快。
冷启动耗时点 + 优化:
- Application.onCreate 过重:第三方 SDK 初始化阻塞。优化:异步初始化、延迟初始化、按需初始化(用 Jetpack App Startup 统一管理依赖顺序)。
- 闪屏页(Splash):用 Android 12+ SplashScreen API,避免白屏/黑屏。
- 主线程 IO:首屏不读大文件/SP。
- 测量:
adb shell am start -W看 TotalTime;Perfetto/Systrace 看主线程占用。 - 进阶:类预加载、ContentProvider 初始化收敛(很多 SDK 用 ContentProvider 自启,拖慢启动)。
二、内存优化
内存泄漏(常见场景)
- 非静态内部类/匿名类持有外部 Activity(Handler、Runnable、监听器)。
- 静态变量持有 Context/View。
- 单例持有 Activity Context(应用 Application Context)。
- 未注销监听器/广播/观察者。
- 资源未关闭(Cursor、Stream、Bitmap)。
LeakCanary 原理(以 LeakCanary 2.x 思路理解):默认会 watch Activity、Fragment、Fragment View、ViewModel 等应被回收的对象。对象销毁后交给 ObjectWatcher,内部用弱引用和引用队列观察;延迟一小段时间后触发/等待 GC,若对象仍未被回收,就认为是 retained object。达到阈值后 dump HPROF,再由 Shark 分析 GC Roots 到泄漏对象的最短强引用链。
| 步骤 | 看到的证据 | 面试边界 |
|---|---|---|
| watch 销毁对象 | 对象带 description 和 watch time | 不是所有 retained 都是业务泄漏,可能是系统/库已知泄漏或调试期延迟。 |
| 等待 + GC | 对象没有进入 ReferenceQueue | GC 触发时机不是业务可精确控制的 API,不要说“立刻判定”。 |
| heap dump | HPROF 文件、分析耗时 | dump 会暂停进程/占内存,通常只在 debug 或内测环境开。 |
| Shark 分析 | 引用链、Library Leak 标记 | 结论要结合生命周期判断,不是看到 Activity 就机械删除引用。 |
内存抖动
短时间大量创建小对象(如 onDraw / 循环里 new),频繁 GC 导致卡顿。优化:对象复用、避免在高频路径分配、用对象池。
三、卡顿与 ANR
掉帧原理
屏幕 60fps → 每帧 16.6ms 内必须完成 measure/layout/draw。超时就丢帧(jank)。Choreographer 接收 VSync 信号驱动每帧渲染,可监控帧耗时。
卡顿优化
- 布局优化:减少层级(ConstraintLayout 扁平化)、
<merge>/<ViewStub>、避免过度绘制(overdraw)。 - 主线程不做耗时(IO、解析、复杂计算),用协程切后台。
- RecyclerView:复用、DiffUtil、避免在 onBindViewHolder 做重活、预取。
ANR(Application Not Responding)
触发条件:
- 主线程阻塞:常见记忆口径是 Input 事件约 5s 未响应、前台 BroadcastReceiver 约 10s、后台广播约 60s、前台 Service 约 20s、后台 Service 约 200s。但这些阈值会受 Android 版本、前后台状态、组件类型和厂商策略影响,面试要说“常见阈值/典型场景”,不要当成所有版本的硬契约。 定位:
/data/anr/traces.txt看主线程堆栈。- 看是否主线程 IO、锁等待、Binder 阻塞、死锁。
- 工具:ANR-WatchDog、线上监控(主线程卡顿监控)。
卡顿/ANR 排查链路
| 症状 | 可能原因 | 工具 | 关键证据 | 修复方向 | 取舍 |
|---|---|---|---|---|---|
| 冷启动白屏久 | Application/Provider 初始化重、主线程 IO、首屏 inflate 慢 | am start -W、Android Studio Startup/Profiler、Perfetto | TotalTime 增大、main thread 上长任务、bindApplication/launchActivity slice 慢 | 延迟/按需初始化、Provider 收敛、首屏最小化 | 延迟初始化可能把耗时转移到首个功能入口,要配合预热和埋点。 |
| 滑动掉帧 | onBind 重活、图片解码过大、布局层级深、频繁 GC | Profiler、Perfetto Frame Timeline、Layout Inspector | 帧耗时超过预算、UI thread/RenderThread slice 长、GC 频繁 | DiffUtil、异步解码、固定尺寸、减少层级/overdraw | 过度缓存会增加内存,异步化要处理取消和错位。 |
| 偶发长卡顿 | 主线程锁等待、同步 Binder、磁盘 IO、类加载 | Perfetto/Systrace、StrictMode、线程 dump | main thread blocked/runnable 状态、binder transaction、disk read/write | 缩小锁范围、后台线程、缓存预热、异步 IPC | 后台化不等于无限开线程,要限制并发和生命周期。 |
| ANR | 主线程长时间不取消息、广播/服务超时、死锁 | ANR traces、Perfetto、日志埋点、线上 ANR 平台 | traces 中 main 堆栈、锁 owner、Binder 对端、CPU/IO 状态 | 移除主线程阻塞、拆分任务、避免持锁跨 Binder | 只看 main 堆栈可能误判,还要看 CPU 饥饿和对端进程。 |
| 内存泄漏 | Activity/Fragment 被静态对象、监听器、协程持有 | LeakCanary、Memory Profiler | retained object、GC Root 引用链、实例数量持续上涨 | 解绑监听、用 application context、取消协程/请求 | 弱引用不是万能解,优先修正生命周期所有权。 |
四、包体积优化
- 代码:R8/ProGuard 混淆 + 裁剪无用代码(shrinkResources)。
- 资源:无用资源删除、图片用 WebP、矢量图、资源混淆(AndResGuard)。
- so 库:按 ABI 拆分(abiFilters 通常只留 arm64-v8a)、动态下发。这是你的强项——SDK 开发对 so 体积敏感。
- dex:移除未用依赖、避免方法数膨胀。
- App Bundle(aab):按设备下发,Google Play 强制。
五、工具链
- Android Studio Profiler:适合开发期快速看 CPU / Memory / Network / Energy。证据是方法耗时、分配热点、网络请求时间线;局限是插桩/采样会有开销,线上问题要结合 trace/埋点。
- Memory Profiler:看 Java/Kotlin 堆、对象数量、分配调用栈、dump HPROF。用于“内存上涨/频繁 GC/疑似泄漏”,修复后要对比同一路径的实例数量是否回落。
- Perfetto / Systrace:系统级 trace,看 main thread、RenderThread、Binder、锁、调度、Frame Timeline。Perfetto 是现代首选;Systrace 常用于老项目/旧资料口径。证据是具体 slice 和线程状态,不是“感觉变流畅”。
- LeakCanary:自动发现 retained objects 并给出引用链。适合 debug/内测定位泄漏;不要在生产默认开启 heap dump。
- Layout Inspector:看视图层级、约束、重叠与 Compose/View 混合结构。适合布局层级深、过度绘制、首屏 inflate 慢的问题。
- StrictMode:开发期检测主线程磁盘/网络、资源未关闭等。适合把“偶发卡顿”提前暴露;惩罚策略可从 log 开始,不要在生产随意 crash 用户。
工具选择速答表
| 你观察到 | 先用 | 再用 | 面试表达 |
|---|---|---|---|
| “启动慢” | am start -W 定量 | Perfetto 找 bindApplication/首帧长任务 | 先量化,再定位主线程 slice,最后验证优化前后。 |
| “滑动卡” | Frame Timeline/Profiler | Layout Inspector + 图片库日志 | 区分 UI 线程、RenderThread、GC、图片解码。 |
| “内存涨” | Memory Profiler | LeakCanary/heap dump | 看趋势和引用链,不要只看一次快照。 |
| “线上 ANR” | ANR traces/平台聚合 | Perfetto/埋点复现 | 主线程栈 + 锁/Binder/CPU 状态一起看。 |
| “怀疑主线程 IO” | StrictMode | Perfetto disk slice | 开发期用 StrictMode 前置发现,trace 证明耗时。 |
高频面试题
Q1:冷启动优化有哪些手段?
异步/延迟/按需初始化 SDK、收敛 ContentProvider 自启动、避免主线程 IO、用 SplashScreen API、类与资源预加载。用 am start -W 和 Perfetto 量化。
Q2:内存泄漏常见场景?LeakCanary 怎么检测的? 非静态内部类/Handler 持 Activity、单例持 Context、未注销监听、资源未关。LeakCanary 2.x 可理解为 ObjectWatcher 监听销毁对象,用弱引用+引用队列观察是否回收;延迟和 GC 后仍 retained,再 dump HPROF 并用 Shark 分析引用链。结论要结合生命周期,不要把所有 retained 都当业务泄漏。
Q3:为什么 16.6ms 是关键?掉帧怎么排查? 60fps 下每帧预算 16.6ms,超时丢帧。用 Choreographer 监控帧耗时,Profiler/Perfetto 看主线程哪段超时,优化布局层级和主线程耗时操作。
Q4:ANR 触发条件?怎么定位? 主线程阻塞超阈值会触发 ANR,常见口径是输入约 5s、前台广播约 10s、前台 Service 约 20s,后台场景通常更长,但具体阈值和触发条件随版本/前后台/厂商策略变化。定位看 ANR traces 主线程堆栈,结合锁 owner、Binder 对端、CPU/IO 状态,排查主线程 IO、锁竞争、死锁、同步 Binder。
Q5:包体积优化怎么做?(你的强项,展开答) R8 裁剪混淆、资源压缩、WebP/矢量图、so 按 ABI 拆分(只留 arm64-v8a)+ 动态下发、移除无用依赖、用 App Bundle 按需下发。结合 SDK 经验讲 so 瘦身和符号裁剪更有说服力。
Q6:RecyclerView 卡顿如何优化? DiffUtil 局部刷新、onBindViewHolder 不做耗时、setHasFixedSize、预取(prefetch)、图片异步加载、减少 item 布局层级、复用 ViewHolder。
Q7:Serializable 和 Parcelable 哪个对性能更好,为什么? Parcelable。Serializable 用反射、产生大量临时对象触发 GC;Parcelable 专为内存/IPC 设计,无反射,速度快约一个数量级。