Skip to content

性能优化 / 稳定性 — 面试题篇

更新: 5/15/2026 字数: 0 字 时长: 0 分钟


Q1: 冷启动优化做过哪些?怎么衡量效果?

考察点:启动优化实战经验

完整回答

冷启动全链路:Zygote fork → Application 初始化 → Activity 创建 → 首帧绘制。

优化手段:

  1. Application.onCreate:SDK 异步初始化(无依赖的放子线程)、延迟初始化(非必要的延迟到首帧后)、任务编排(DAG 处理依赖关系)
  2. ContentProvider 合并:用 App Startup 合并多个 Provider 为一个
  3. Activity:减少布局层级、ViewStub 延迟加载非首屏内容、异步 inflate
  4. 类加载优化:提前加载热点类、MultiDex 优化
  5. 闪屏页:设置 windowBackground 为品牌图,视觉上"秒开"

衡量指标:

  • TTID:首帧显示时间(adb shell am start -W 的 TotalTime)
  • TTFD:完整内容显示时间(reportFullyDrawn()
  • 使用 Systrace/Perfetto 分析主线程耗时分布

追问:异步初始化怎么处理有依赖关系的 SDK?

用 DAG(有向无环图)编排。每个初始化任务声明依赖的前置任务,框架拓扑排序后并行执行无依赖的任务,有依赖的等前置完成后再执行。类似 Gradle 的 Task 依赖。


Q2: 内存泄漏怎么排查?常见的泄漏场景?

考察点:内存优化能力

完整回答

排查工具:

  1. LeakCanary:自动检测 Activity/Fragment 泄漏,展示引用链
  2. Android Studio Profiler:dump heap,分析对象引用
  3. MAT(Memory Analyzer Tool):分析 hprof 文件,查找 GC Root 引用链

常见泄漏场景:

  1. Handler:非静态内部类持有 Activity 引用,Message 在队列中等待 → 用静态内部类 + WeakReference + onDestroy 清除消息
  2. 单例持有 Context:传了 Activity Context → 改用 Application Context
  3. 匿名内部类:Runnable、Callback 持有外部 Activity → 用静态类或在 onDestroy 取消
  4. 未注销监听:EventBus、BroadcastReceiver、SensorManager → onDestroy 中注销
  5. WebView:WebView 内部持有 Activity → 独立进程或动态添加/移除
  6. 集合类:只添加不移除 → 及时清理

追问:LeakCanary 的原理?

  1. 注册 ActivityLifecycleCallbacks 监听 onDestroy
  2. 将销毁的 Activity 包装为 WeakReference,关联 ReferenceQueue
  3. 延迟 5 秒检查 ReferenceQueue,如果 WeakReference 没入队说明未被回收
  4. 手动 GC 后再检查,仍未回收则 dump hprof
  5. 使用 Shark 库分析 hprof,找到 GC Root 到泄漏对象的最短引用链

Q3: ANR 是什么?怎么排查?

考察点:ANR 分析能力

完整回答

ANR(Application Not Responding)是应用在规定时间内没有响应系统事件:

  • 输入事件(触摸/按键):5 秒
  • BroadcastReceiver:前台 10 秒,后台 60 秒
  • Service:前台 20 秒,后台 200 秒

排查步骤:

  1. 获取 ANR 日志:/data/anr/traces.txt 或线上监控平台
  2. 分析主线程堆栈,看主线程在做什么:
    • 死锁waiting to lock <0x...> held by thread X
    • IO 阻塞:文件读写、数据库查询在主线程
    • Binder 调用:跨进程调用对端响应慢
    • CPU 密集:主线程做大量计算
  3. 查看 CPU 使用率:如果 CPU 使用率很高,可能是计算密集;如果很低,可能是锁等待或 IO

预防措施:

  • 主线程不做 IO、网络、数据库操作
  • 使用 StrictMode 检测主线程违规操作
  • BroadcastReceiver 中不做耗时操作(用 goAsync 或转发给 Service/WorkManager)
  • 监控主线程消息处理耗时(Looper printer 或 Choreographer)

追问:线上 ANR 怎么监控?

  1. FileObserver 监听 /data/anr/ 目录变化
  2. 主线程 Watchdog:子线程定期向主线程 post 消息,超时未执行则判定卡顿/ANR
  3. 使用 ANR-WatchDog 库或自建监控

Q4: 卡顿优化怎么做?怎么监控帧率?

考察点:渲染性能优化

完整回答

卡顿原因:一帧超过 16.6ms(60fps)。

监控方式:

  1. Choreographer.FrameCallback:计算相邻帧的时间差,超过 16.6ms 即掉帧
  2. Looper printer:监控主线程每个 Message 的处理耗时(BlockCanary 原理)
  3. FrameMetrics API(API 24+):精确获取每帧各阶段耗时

优化手段:

  • 布局优化:减少层级(ConstraintLayout)、移除过度绘制(Debug GPU Overdraw)、ViewStub 延迟加载
  • 主线程优化:IO/计算移到子线程、减少主线程锁等待
  • RecyclerView 优化:setHasFixedSize、DiffUtil、预加载、共享 RecycledViewPool
  • 减少内存抖动:避免在 onDraw/onBindViewHolder 中创建对象,复用对象
  • 图片优化:合适的采样率、RGB_565、inBitmap 复用

追问:Systrace 怎么用?

bash
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。

优化手段:

  1. 采样加载inSampleSize 降低分辨率
  2. 合适的像素格式:不需要透明通道用 RGB_565
  3. inBitmap 复用:复用已有 Bitmap 的内存块
  4. 及时回收:不用时 bitmap.recycle()
  5. 放在合适的资源目录:避免不必要的缩放
  6. 使用 Glide/Coil:自动管理生命周期和缓存

Q6: 包体积优化做过哪些?

考察点:工程化能力

完整回答

  1. 代码压缩:R8/ProGuard 移除无用代码、混淆、优化字节码
  2. 资源压缩shrinkResources true 移除无用资源
  3. 图片优化:PNG → WebP(体积减少 25-35%)、TinyPNG 压缩、矢量图替代小图标
  4. 资源混淆:AndResGuard 将资源名缩短(res/drawable/icon → r/d/a)
  5. so 库:只保留 arm64-v8a(主流架构)、按需加载 so
  6. 动态下发:大资源/功能模块通过 Dynamic Feature 或插件化按需下载
  7. 代码优化:移除无用依赖、避免重复依赖

分析工具: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 次以上

优化手段:

  1. 移除不必要的背景(Activity 默认有 Window 背景,如果布局有自己的背景可以移除 Window 背景)
  2. android:background="@null"window.setBackgroundDrawable(null)
  3. 使用 clipRect 限制绘制区域
  4. 减少布局层级(ConstraintLayout 替代嵌套)
  5. ViewStub 延迟加载不可见的布局

Q9: 线上 Crash 怎么治理?有什么方法论?

考察点:稳定性治理

完整回答

治理流程:

  1. 监控:接入 Crash 监控 SDK(Firebase Crashlytics、Bugly),收集崩溃堆栈、设备信息、用户操作路径

  2. 分类:按影响面排序

    • Top Crash:影响用户数最多的崩溃优先修复
    • 按模块/页面分类,找到问题集中的区域
  3. 分析

    • Java Crash:直接看堆栈定位代码
    • Native Crash:addr2line 还原符号,分析 tombstone
    • ANR:分析 traces.txt 主线程堆栈
  4. 修复

    • 紧急问题:热修复(Tinker/Sophix)
    • 常规问题:下个版本修复
  5. 预防

    • 代码规范 + Lint 检查
    • 单元测试覆盖核心逻辑
    • 灰度发布(先 1% → 10% → 全量)
    • 质量门禁(Crash 率超标则阻止发版)

追问:Crash 率怎么计算?业界标准是多少?

Crash 率 = 崩溃用户数 / DAU × 100%。业界标准:Crash Rate < 0.1%(千分之一),ANR Rate < 0.3%。


Q10: 网络优化做过哪些?

考察点:网络性能

完整回答

  1. 连接优化

    • HTTP/2 多路复用(一个连接并行多个请求)
    • 连接池复用(OkHttp 默认 5 个空闲连接)
    • DNS 预解析 + 缓存(避免 DNS 查询耗时)
  2. 数据优化

    • Protobuf 替代 JSON(体积减少 60-90%)
    • gzip 压缩请求/响应体
    • 增量更新(只传变化的数据)
  3. 缓存优化

    • HTTP 缓存(Cache-Control)
    • 本地缓存(先展示缓存,后台更新)
    • 预加载(提前请求下一页数据)
  4. 弱网优化

    • 超时时间自适应(WiFi 短,移动网络长)
    • 请求优先级(核心请求优先)
    • 失败重试(指数退避)
    • 降级策略(弱网下降低图片质量)
  5. 监控

    • 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 中创建对象。