Android性能优化面试题:你说你会优化,结果连ANR都排查不了?
性能优化这块,问深了特别容易暴露功底。今天咱们把Android性能优化的高频面试题过一遍,每道题都掰开了揉碎了讲,保证你面试不再心虚。
1. 冷启动优化:你做了哪些?怎么衡量?从点击图标到首帧渲染经历了什么?
核心回答
冷启动优化不是玄学,你得先知道它经历了什么,才能知道优化什么。
冷启动分为三个阶段:
- Launcher进程创建 → 点击图标,Launcher进程fork出APP进程
- Application初始化 → 执行attachBaseContext、onCreate,还有各种SDK的init
- Activity渲染 → onCreate、onStart、onResume、首次measure/layout/draw、Choreographer绑定Vsync、首帧绘制
怎么衡量?两个关键指标:
kotlin
// 1. adb shell am start -W -n 包名/Activity名
// TimeToFirstPaint(TTFP): 首次绘制时间
// TimeToFullDraw(TTFD): 完整绘制时间
// 2. 代码中埋点(更精确)
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 记录进程创建时间
val startTime = Process.getStartElapsedRealtime()
// 发送到APM
}
}
// 3. systrace + Perfetto(推荐)
// systrace.py -a com.example.app -b 16384 -o trace.html sched gfx view wm app
原理/代码
冷启动的核心是进程创建 + Resource加载 + ContentProvider初始化。这其中最容易拖时间的是:
kotlin
// Application的onCreate,第三方SDK扎堆的地方
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 这些都是耗时大户
Bugly.init(this, "appId", false)
UMConfigure.init(...)
RetrofitClient.init(...)
ImageLoader.init(...)
// 你以为的"初始化",其实是网络IO + 文件IO + 反射
}
}
优化思路:
- 异步初始化 → 不阻塞主线程
- 延迟初始化 → 不在Application做,按需加载
- 预加载 → Splash阶段提前初始化
- ReDex优化 → 减少类加载时间
Android实战场景
kotlin
// 方案1:异步初始化SDK
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 主线程只做必须同步的
initBugly()
// 其他SDK延迟到子线程
Handler(Looper.getMainLooper()).post {
initUMeng()
}
// 或者用更好的方式:启动器
AppStartup.getInstance().init(this)
}
}
// 方案2:Splash预初始化
// 在SplashActivity中预加载下一屏需要的资源
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 同时做两件事:展示Splash + 预初始化
lifecycleScope.launch {
// 预加载用户数据
async { preloadUserData() }
async { preloadHomeResources() }
// 等待足够展示2秒后跳转
delay(2000)
startActivity<MainActivity>()
}
}
}
面试加分点
- 知道Systrace的猫点(traceBegin/traceEnd) ,能自己打点定位慢在哪
- 了解CPU Profiler的Method Trace,能分析方法调用耗时
- 知道启动时间60fps基准:从点击到首帧<500ms,用户感知流畅
2. 热启动和温启动的区别?优化思路有什么不同?
核心回答
三个启动方式,耗时从低到高:
表格
| 类型 | 定义 | 耗时 | 优化重点 |
|---|---|---|---|
| 热启动 | APP还在后台,Activity还在内存 | <100ms | 基本无需优化 |
| 温启动 | APP在前台,但Activity被销毁重建 | 100-300ms | ViewModel复用、状态恢复 |
| 冷启动 | APP进程不存在,从零创建 | 500ms+ | 进程创建 + Application初始化 |
温启动的关键是状态恢复:
kotlin
// onCreate中的状态恢复
class MainActivity : AppCompatActivity() {
// 方案1:ViewModel(配置变更后保留数据)
private val viewModel: MainViewModel by viewModels()
// 方案2:onSaveInstanceState(进程被杀后恢复)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("key", value) // 只存轻量数据
}
override fun onCreate(savedInstanceState: Bundle?) {
// savedInstanceState != null 说明是温启动
if (savedInstanceState != null) {
val value = savedInstanceState.getString("key")
// 恢复状态
}
}
}
原理/代码
热启动、温启动的区别在于Activity实例是否还在内存:
arduino
// ActivityManagerService中的逻辑简化
final boolean attachApplicationLocked(...) {
// 冷启动:创建新进程
// 热/温启动:复用现有进程
// 关键:判断ActivityStack中的Task
// 热启动 → Activity实例在栈顶,直接resume
// 温启动 → Activity实例被销毁,但Task还在,需要重建
}
Android实战场景
kotlin
// 温启动优化:避免重复网络请求
class MainViewModel : ViewModel() {
private var cachedData: Result? = null
fun loadData(): LiveData<Result> {
if (cachedData != null) {
return MutableLiveData(cachedData)
}
return Retrofit.service.getData().also {
cachedData = it
}
}
}
// 或者用SavedStateHandle处理进程死亡场景
class MainViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val data = savedStateHandle.getLiveData<String>("data")
}
面试加分点
- 了解onCreate和onRestoreInstanceState的调用时机
- 知道进程优先级(前台进程 > 可见进程 > 服务进程 > 后台进程),理解为什么热启动快
- 能说出配置变更(如旋转屏幕)导致Activity重建,但进程不销毁
3. 内存泄漏和内存溢出的区别?常见的泄漏场景有哪些?
核心回答
内存溢出(OOM) :内存不够用了,请求分配内存时系统给不出。
内存泄漏(Leak) :内存明明不用了,却因为错误引用无法被GC回收。一点点漏,最终导致OOM。
常见泄漏场景,说五个以上:
表格
| 场景 | 原因 | 后果 |
|---|---|---|
| 单例持有Context | 单例生命周期>Activity | Activity无法回收 |
| Handler泄漏 | 非静态内部类持有外部引用,消息队列延迟 | 页面关闭后Handler还在执行 |
| 静态View | view被static引用 | View所属Activity无法释放 |
| 监听器未注销 | BroadcastReceiver、EventBus监听未移除 | 持有整个Activity引用 |
| WebView泄漏 | WebView持有Context + 复杂JS引擎 | 泄漏严重 |
| 内部类泄漏 | 非静态内部类默认持有外部类引用 | 常见但容易被忽略 |
原理/代码
kotlin
// 场景1:Handler泄漏(经典错误)
class LeakActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper()) {
// 这个匿名内部类会持有LeakActivity引用
true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 发送延迟消息
handler.postDelayed({
// 如果Activity已销毁,这条消息还持有引用
}, 5000)
}
// 正确写法:静态内部类 + WeakReference
class SafeHandler(activity: Activity) : Handler(Looper.getMainLooper()) {
private val ref = WeakReference(activity)
override fun handleMessage(msg: Message) {
ref.get()?.let {
// 安全使用
}
}
}
}
// 场景2:静态View泄漏
class LeakActivity : AppCompatActivity() {
companion object {
// 危险!这是一个泄漏炸弹
var textView: TextView? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 如果把View赋值给静态变量,Activity永远不会被回收
}
}
// 场景3:监听器未注销
class LeakActivity : AppCompatActivity() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// 这个receiver持有Activity引用
}
}
override fun onResume() {
super.onResume()
registerReceiver(receiver, IntentFilter("ACTION"))
}
override fun onPause() {
super.onPause()
// 必须注销,否则泄漏
unregisterReceiver(receiver)
}
}
Android实战场景
kotlin
// 监听器泄漏的最佳实践:Lifecycle感知
class LeakActivity : AppCompatActivity() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// 业务逻辑
}
}
override fun onStart() {
super.onStart()
LocalBroadcastManager.getInstance(this)
.registerReceiver(receiver, filter)
}
override fun onStop() {
super.onStop()
// Lifecycle-aware: onStop时自动注销
LocalBroadcastManager.getInstance(this)
.unregisterReceiver(receiver)
}
}
// 或者用 lifecycleScope 自动管理
class LeakActivity : AppCompatActivity() {
private val job = Job()
override fun onDestroy() {
super.onDestroy()
// coroutine 自动取消
job.cancel()
}
}
面试加分点
- 能解释可达性分析:GC Root到不了的对象就是泄漏对象
- 知道四种GC Root类型:虚拟机栈引用的对象、方法区静态/常量引用的对象、本地方法栈引用的对象
- 理解内存抖动:频繁创建对象导致频繁GC,影响性能
4. LeakCanary的原理是什么?为什么线上不用LeakCanary?
核心回答
LeakCanary是Square开源的内存泄漏检测工具,原理很巧妙:
- ObjectWatcher:WeakReference + ReferenceQueue 监控对象
- WeakReference + GC :
WeakReference被GC回收时会进入ReferenceQueue,如果5秒后对象还在,说明泄漏了 - Hprof解析:dump heap,分析泄漏路径
- GCRoot扫描:从GC Root开始遍历,找到泄露对象的引用链
kotlin
// LeakCanary核心原理简化
class ObjectWatcher(
private val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)
) {
private val watchedObjects = WeakHashMap<Object, LeakInfo>()
private val queue = ReferenceQueue<Any>()
fun watch(obj: Any, name: String) {
// 1. 创建KeyedWeakReference
val ref = KeyedWeakReference(obj, name, this, queue)
// 2. 等待GC后检查
val job = Executors.newSingleThreadScheduledExecutor()
job.schedule({
// 3. 5秒后检查是否被回收
removeWeaklyReachableObjects()
if (watchedObjects.containsKey(ref)) {
// 没被回收 → 泄漏!dump hprof
val leakInfo = dumpHeap(ref)
// 分析并展示
}
}, watchDurationMillis, TimeUnit.MILLISECONDS)
}
}
原理/代码
kotlin
// KeyedWeakReference的定义
class KeyedWeakReference(
referent: Any,
val key: String,
val name: String,
val referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(referent, referenceQueue)
// GC时,如果对象只被WeakReference持有,会被回收并进入ReferenceQueue
// LeakCanary就是检查这个queue,如果5秒后对象没进queue,说明有强引用阻止回收
为什么线上不用LeakCanary?
表格
| 问题 | 原因 |
|---|---|
| 性能开销大 | dump hprof、解析、找引用链,CPU和内存开销都很大 |
| 用户体验差 | 弹Dialog、生成文件,普通用户看到会恐慌 |
| 内存占用高 | 监控大量对象本身就需要内存 |
| 隐私问题 | hprof包含完整堆快照,有敏感数据 |
线上方案通常是:
- 采样监控:只监控部分用户,不全量
- 无Dialog:静默上报,不打扰用户
- 轻量检测:不dump hprof,只检测可疑对象数量
kotlin
// 线上轻量级泄漏检测思路
class LiteLeakMonitor {
private val objectCounter = AtomicInteger()
private val maxObjects = 100
fun watch(obj: Any) {
val ref = KeyedWeakReference(obj, generateKey(), queue)
val current = objectCounter.incrementAndGet()
if (current > maxObjects) {
// 对象数量异常,可能有泄漏
reportLeakSuspect(current)
}
}
private fun reportLeakSuspect(count: Int) {
// 上报到APM,不dump,不弹窗
CrashReport.reportLeak(count)
}
}
面试加分点
- 能画出LeakCanary的工作流程图
- 了解hprof文件格式,知道MAT/LeakCanary解析原理
- 知道Shallow Size vs Retained Size的区别
5. OOM怎么排查?线上OOM监控方案怎么设计?
核心回答
OOM分三类,排查方法不同:
表格
| 类型 | 原因 | 排查方法 |
|---|---|---|
| 堆溢出(Heap OOM) | 内存分配超限 | MAT分析hprof |
| 栈溢出(Stack OOM) | 线程栈过大/递归过深 | 检查线程数量和栈大小 |
| Native OOM | Native层分配失败 | Address Sanitizer / addr2line |
原理/代码
ini
// 堆溢出的常见原因:大图片 + 缓存无限增长
// 1. 大图片导致OOM
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.huge_image)
// 这张图2000x2000 RGBA = 2000*2000*4 = 16MB
// 加上Bitmap内部内存,实际占用更大
// 正确做法:按需采样
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true // 只获取尺寸,不加载
}
BitmapFactory.decodeResource(resources, R.drawable.huge_image, options)
val sampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
options.inSampleSize = sampleSize
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.huge_image, options)
// 2. 缓存无限增长
// 错误:LruCache没有限制
val cache = LruCache<Any, Any>(Int.MAX_VALUE) // 等于没限制
// 正确:根据可用内存计算
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8 // 使用1/8的堆内存
val cache = LruCache<Any, Any>(cacheSize)
Android实战场景
kotlin
// 线上OOM监控方案
class OOMMonitor private constructor() {
companion object {
val instance: OOMMonitor by lazy { OOMMonitor() }
}
// 1. 捕获OutOfMemoryError
fun init() {
// 设置全局OOM处理器
Thread.setDefaultUncaughtExceptionHandler { thread, ex ->
if (ex is OutOfMemoryError) {
handleOOM(ex)
}
}
}
private fun handleOOM(error: OutOfMemoryError) {
// 1. 记录内存状态
val memInfo = ActivityManager.MemoryInfo()
val am = application.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.getMemoryInfo(memInfo)
// 2. dump内存快照(后台进行,不阻塞主线程)
Debug.dumpHprofData("/sdcard/oom_${System.currentTimeMillis()}.hprof")
// 3. 上报基本信息
val report = mapOf(
"availableMemory" to memInfo.availMem,
"totalMemory" to memInfo.totalMem,
"threshold" to memInfo.threshold,
"message" to error.message,
"stackTrace" to error.stackTrace.toString()
)
CrashReport.report(report)
}
}
// 内存预警:在OOM之前监控
class MemoryWatcher {
private val lowMemoryThreshold = 50 * 1024 * 1024 // 50MB
fun checkMemory(): MemoryStatus {
val runtime = Runtime.getRuntime()
val freeMemory = runtime.freeMemory()
val totalMemory = runtime.maxMemory()
return when {
freeMemory < lowMemoryThreshold -> MemoryStatus.CRITICAL
freeMemory < totalMemory * 0.2 -> MemoryStatus.WARNING
else -> MemoryStatus.NORMAL
}
}
}
面试加分点
- 能说出DevTools的Memory Profiler + Allocation Tracker的使用方法
- 知道MAT的Dominator Tree、Histogram、Top Consumers怎么用
- 理解对象引用链:为什么强引用阻止GC
6. 卡顿怎么定位?Choreographer原理是什么?怎么监控帧率?
核心回答
卡顿的本质是主线程有耗时操作,导致Vsync信号到来时,CPU/GPU还没准备好,丢帧。
Choreographer原理
Choreographer是Android的帧率协调器,核心是接收Vsync信号,按顺序执行输入 → 处理 → 绘制:
scss
// Choreographer的工作流程
class Choreographer {
// 每帧都会调用的回调
fun postFrameCallback(callback: FrameCallback) {
// 内部会监听Choreographer.getInstance().postFrameCallback()
// 当Vsync信号到来时,调用doFrame()
}
// doFrame的执行顺序(看源码)
void doFrame(long frameTimeNanos) {
// 1. Input阶段的输入事件处理
doCallbacks(CALLBACK_INPUT, frameTimeNanos);
// 2. Animation动画
doCallbacks(CALLBACK_ANIMATION, frameTimeNanos);
// 3. Insets动画
doCallbacks(CALLBACK_INSETS_ANIMATION, frameTimeNanos);
// 4. Traversal:View树遍历(measure/layout/draw)
doCallbacks(CALLBACK_TRAVERSAL, frameTimeNanos);
// 5. Commit:提交到GPU
doCallbacks(CALLBACK_COMMIT, frameTimeNanos);
}
}
// 为什么卡顿?因为某一步耗时,导致下一帧错失Vsync
// 丢一帧 = 16.6ms,用户能感知到
帧率监控方案
kotlin
// 方案1:Choreographer.FrameCallback
class FrameRateMonitor {
private var lastFrameTimeNanos = 0L
private val frameRates = mutableListOf<Float>()
fun start() {
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (lastFrameTimeNanos != 0L) {
val deltaMs = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000f
val fps = 1000f / deltaMs
if (fps < 55) { // 丢帧阈值
Log.w("FrameRate", "Jank! FPS=$fps, delta=${deltaMs}ms")
}
}
lastFrameTimeNanos = frameTimeNanos
Choreographer.getInstance().postFrameCallback(this)
}
})
}
}
// 方案2:UI Thread Looper监控(更准确)
class LooperMonitor {
fun start() {
Looper.getMainLooper().setMessageLogging { log ->
if (log.startsWith(">>>")) {
// 消息开始执行
} else if (log.startsWith("<<<")) {
// 消息执行结束
val duration = parseDuration(log)
if (duration > 16) { // 超过一帧时间
Log.e("Looper", "Main thread blocked for ${duration}ms")
}
}
}
}
}
Android实战场景
kotlin
// systrace定位卡顿(推荐)
// 在可疑代码处打点
Trace.beginSection("loadUserData")
try {
// 耗时操作
loadUserData()
} finally {
Trace.endSection()
}
// 使用Systrace分析
// python systrace.py -a com.example.app -b 16384 -o trace.html gfx view wm am app
// 打开trace.html,按g进入卡顿区域,查看是哪个操作耗时
// 实际项目中的自动化卡顿检测
class BlockCanary {
private val handler = Handler(Looper.getMainLooper())
private var startTime = 0L
fun start() {
handler.post(object : Runnable {
override fun run() {
startTime = System.currentTimeMillis()
handler.postAtFrontOfQueue(this)
}
})
}
fun onFrameEnd() {
val blockTime = System.currentTimeMillis() - startTime
if (blockTime > 16) {
// 记录卡顿堆栈
saveStackTrace(blockTime)
}
}
}
面试加分点
- 能解释Vsync信号机制:双缓冲、Triple Buffer
- 知道SurfaceFlinger在渲染链路中的位置
- 理解Jank类型:Single Jank、Continuous Jank、Severe Jank
7. ANR的触发条件是什么?怎么排查?traces.txt怎么看?
核心回答
ANR是Application Not Responding,Android的自我保护机制。
触发条件:
表格
| 类型 | 条件 |
|---|---|
| Input超时 | 主线程5秒内没处理完输入事件 |
| Broadcast超时 | 前台Broadcast 10秒,后台Broadcast 60秒没处理完 |
| Service超时 | 前台Service 20秒,后台Service 200秒没完成 |
| ContentProvider超时 | 10秒内没完成publish |
原理/代码
arduino
// ANR的核心机制:InputDispatcher + InputChannel
// 当Input事件到达主线程,如果5秒内没消费,AMS就会判定ANR
// BroadcastQueue.processNextBroadcast() 的超时逻辑
// 每个BroadcastReceiver处理超过10秒(前台),就会ANR
// Service启动的ANR检测
// ActiveServices.realizeService() 中有这个逻辑
// timeoutFGService = 20s, timeoutBGService = 200s
traces.txt怎么看?
php
# traces.txt位置:/data/anr/traces.txt(需要root权限查看)
# 关键信息:
----- pid 12345 at 2024-01-15 10:30:00 -----
# 主线程堆栈
"main" prio=5 tid=1 Runnable
group="main" sCount=0 dsCount=0
# 这是关键!wait=0 说明线程正在执行,wait>0 说明在等锁
# sCount表示suspend count
at com.example.app.MainActivity.onCreate(MainActivity.java:45)
at android.app.Activity.performCreate(Activity.java:7890)
# 关键行:CPU usage
CPU usage from 0ms to 10000ms later:
# 这段是关键!如果User%和System%都很低,说明是等锁/IO阻塞
# 如果CPU%高,说明是CPU密集型计算
12% 12345/com.example.app: 10% user + 2% kernel / faults: 5000 minor
# 如果只有2%,说明大部分时间在等IO或等锁
# 关键字分析
at dalvik.system.VMStack.getThreadStackTrace(VMStack.java)
at java.lang.Object.wait(Native Method) # 线程在等待
at java.lang.Object.wait(Object.java:386) # 等待某个锁
Android实战场景
kotlin
// ANR的常见原因和排查
// 原因1:主线程做IO
class BadExample : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 错误:在主线程读写文件
val data = File("file.txt").readText() // ANR高发
val json = SharedPreferences.getString("key", "") // 也是IO
// 正确:移到子线程
lifecycleScope.launch(Dispatchers.IO) {
val data = withContext(Dispatchers.IO) {
File("file.txt").readText()
}
withContext(Dispatchers.Main) {
// 更新UI
}
}
}
}
// 原因2:主线程网络请求
// 错误示例
class BadExample : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 网络请求在主线程?ANR妥妥的
val response = api.getData() // 不要这样做
}
}
// 原因3:死锁
class DeadLockExample {
val lock1 = Object()
val lock2 = Object()
fun method1() {
synchronized(lock1) {
synchronized(lock2) {
// 操作
}
}
}
fun method2() {
synchronized(lock2) { // 和method1的加锁顺序相反,死锁!
synchronized(lock1) {
// 操作
}
}
}
}
// 解决:统一加锁顺序
面试加分点
- 知道StrictMode怎么开启,能检测主线程IO/网络
- 能解释ANR和Crash的区别:ANR是超时,Crash是异常
- 了解Watchdog机制:Service和Broadcast的ANR是怎么被SystemServer监控的
8. 内存优化:大图加载、Bitmap复用、内存池设计
核心回答
内存优化的核心就三点:少加载、按需加载、复用已有内存。
Bitmap优化
kotlin
// 1. 按需采样
class BitmapOptimizer {
fun loadBitmap(resId: Int, reqWidth: Int, reqHeight: Int): Bitmap? {
val options = BitmapFactory.Options().apply {
// 只读取图片尺寸,不加载像素数据
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, resId, options)
// 计算采样率
options.inSampleSize = calculateInSampleSize(
options.outWidth, options.outHeight,
reqWidth, reqHeight
)
// 真正加载
options.inJustDecodeBounds = false
options.inPreferredConfig = Bitmap.Config.RGB_565 // 2字节/像素,省一半
return BitmapFactory.decodeResource(resources, resId, options)
}
private fun calculateInSampleSize(
width: Int, height: Int,
reqWidth: Int, reqHeight: Int
): Int {
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (halfHeight / inSampleSize >= reqHeight &&
halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
}
// 2. Bitmap复用(避免频繁创建和回收)
class BitmapPool {
// 复用池
private val bitmapPool = LinkedHashMap<String, Bitmap>()
private val maxSize = 20 * 1024 * 1024 // 20MB
fun put(bitmap: Bitmap) {
if (bitmap.isMutable) {
val key = "${bitmap.width}x${bitmap.height}"
bitmapPool[key] = bitmap
trimToSize()
}
}
fun get(width: Int, height: Int, config: Bitmap.Config): Bitmap? {
val key = "${width}x${height}"
return bitmapPool.remove(key)
}
private fun trimToSize() {
var totalSize = 0L
val iterator = bitmapPool.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
totalSize += entry.value.allocationByteCount
if (totalSize > maxSize) {
iterator.remove()
}
}
}
}
// 使用BitmapFactory.Options.inBitmap
class BitmapReuseExample {
private var reusableBitmap: Bitmap? = null
fun loadBitmap(path: String): Bitmap? {
val options = BitmapFactory.Options().apply {
// 尝试复用
inMutable = true
inBitmap = reusableBitmap
}
return try {
BitmapFactory.decodeFile(path, options).also {
// 复用成功,下一张图就能用这个Bitmap的内存
reusableBitmap = it
}
} catch (e: Exception) {
// 复用失败,重新创建
options.inBitmap = null
BitmapFactory.decodeFile(path, options)
}
}
}
内存池设计
kotlin
// 设计一个对象池,复用频繁创建的对象
class MessagePool {
private val pool = ConcurrentLinkedQueue<Message>()
private val maxSize = 50
fun obtain(): Message {
return pool.poll() ?: Message()
}
fun recycle(message: Message) {
if (pool.size < maxSize) {
message.reset() // 重置状态
pool.offer(message)
}
}
}
// OkHttp的ConnectionPool就是这个思路
// 连接池复用Http连接,避免频繁创建TCP连接
class ConnectionPool {
private val connections = LinkedHashMap<String, Connection>()
private val maxIdleConnections = 5
private val keepAliveDuration = 5 * 60 * 1000L // 5分钟
fun getConnection(host: String): Connection? {
val conn = connections[host]
if (conn != null && !conn.isExpired()) {
conn.lastUseTime = System.currentTimeMillis()
return conn
}
return null
}
}
面试加分点
- 知道Bitmap像素格式:ARGB_8888(4字节)、RGB_565(2字节)、ALPHA_8(1字节)
- 理解inPreferredConfig对内存的影响
- 知道Bitmap的 recycle() 什么时候该调用,什么时候不该调用(Android 3.0后不需要手动recycle)
9. 包体积优化:瘦身方案有哪些?怎么衡量效果?
核心回答
包体积优化不是抠门,是下载转化率 和用户留存的问题。100MB的包比10MB的包,下载转化率可能差20%。
核心方案
表格
| 方案 | 原理 | 收益 |
|---|---|---|
| ProGuard/R8混淆 | 移除未使用代码 + 混淆 | 20-30% |
| 资源压缩 | 移除未使用资源 + PNG压缩 | 10-15% |
| 动态下发 | DEX/So/资源懒加载 | 30-50% |
| WebP图片 | 比PNG小30% | 5-10% |
| ABI过滤 | 只打包目标架构的So | 50%+ |
| AAR vs JAR | AAR包含资源但不会重复打包 | - |
原理/代码
groovy
java
// 1. build.gradle配置
android {
buildTypes {
release {
minifyEnabled true // 开启混淆
shrinkResources true // 移除未使用资源
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// ABI过滤(如果不需要x86模拟器)
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}
}
// 2. multidex配置(Android 5.0以下需要)
android {
defaultConfig {
multiDexEnabled true
multiDexKeepProguard File('multidex-config.pro')
}
}
// 3. bundletool动态下发
// google play app bundle
Android实战场景
kotlin
kotlin
// 动态模块加载(按需下载)
class DynamicModuleManager {
// 安装时不解压,按需加载
private val splitCompatManager = SplitCompatManager()
fun loadFeature(featureName: String) {
lifecycleScope.launch {
try {
// 动态模块名称必须在build.gradle中配置
val splitInstallRequest = SplitInstallRequest.newBuilder()
.addModule(featureName)
.build()
splitInstallManager.startSplitInstall(splitInstallRequest)
.addOnSuccessListener { sessionId ->
// 模块已安装,立即可用
}
.addOnFailureListener { exception ->
// 处理失败
}
} catch (e: Exception) {
// 模块未配置或不可用
}
}
}
}
// build.gradle中配置动态模块
/*
android {
bundle {
language {
enableSplit = true
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
}
*/
// 监控包体积
// 1. APK分析器:Android Studio → Build → Analyze APK
// 2. bundlestatus 监控各模块大小趋势
面试加分点
- 能说出增量更新的原理:bsdiff/bspatch
- 知道微信/美团的AndResRes资源混淆方案
- 理解热修复和动态下发的区别:热修复修Bug,动态下发是功能模块
10. 电量优化:Doze模式、JobScheduler、WorkManager怎么选?
核心回答
电量优化有三个层次:减少请求频次、合理利用系统机制、监控耗电异常。
Doze模式
kotlin
// Doze模式的电量的影响
// 进入Doze条件:未充电 + 屏幕关闭 + 静止一段时间
// 影响:
// - 网络访问被限制
// - WakeLock被忽略
// - Alarm被延迟到maintenance window
// - GPS/WiFi扫描被限制
// 处理Doze模式:使用 AlarmManager的精确Alarm
class AlarmHelper {
fun scheduleExactAlarm() {
val intent = Intent(this, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
// Android 12+需要申请精确Alarm权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
}
} else {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
}
}
}
JobScheduler vs WorkManager
kotlin
// JobScheduler(系统级)
class JobSchedulerExample : Application() {
private lateinit var jobScheduler: JobScheduler
fun scheduleJob() {
val jobInfo = JobInfo.Builder(1, ComponentName(this, MyJobService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // WiFi下执行
.setRequiresCharging(true) // 充电时执行
.setPeriodic(15 * 60 * 1000) // 最小15分钟
.setPersisted(true) // 设备重启后保留
.build()
jobScheduler.schedule(jobInfo)
}
}
// JobService实现
class MyJobService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
// 在后台线程执行
thread {
doWork()
jobFinished(params, false) // false=不需要重新调度
}
return true // 返回true表示会有异步操作
}
override fun onStopJob(params: JobParameters?): Boolean {
return true // 返回true表示需要重新调度
}
}
// WorkManager(推荐)
class WorkManagerExample {
fun scheduleWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi
.setRequiresCharging(true) // 充电
.setRequiresBatteryNotLow(true) // 电量不低
.build()
val workRequest = PeriodicWorkRequestBuilder<MyWorker>(
15, TimeUnit.MINUTES // 最小间隔
)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.addTag("sync")
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"sync_work",
ExistingPeriodicWorkPolicy.KEEP,
workRequest
)
}
}
// Worker实现
class MyWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 执行同步任务
syncData()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry() // 自动重试
} else {
Result.failure()
}
}
}
}
怎么选?
表格
| 特性 | JobScheduler | WorkManager |
|---|---|---|
| API版本 | Android 5.0+ | Android 4.0+(向后兼容) |
| 易用性 | 需要写Service | 封装好的Worker |
| 可靠性 | 设备重启后保留 | 设备重启后保留 |
| 约束条件 | 基本支持 | 更丰富 |
| 推荐场景 | 系统级任务 | 应用内后台任务 |
面试加分点
- 能说出Battery Historian怎么分析耗电
- 知道WakeLock的正确用法 和Doze模式的冲突处理
- 理解App Standby Bucket:不同使用强度有不同的后台限制
总结
性能优化这事儿,不是背方案,是理解原理。
面试官想听的不是"我们用了XX框架优化了YY",而是:
- 怎么衡量:有没有量化指标
- 为什么这样:理解底层机制
- Trade-off:知道收益和风险
比如你优化了冷启动,你得知道:
- 冷启动分哪几个阶段
- 每个阶段耗时多少算正常
- 为什么异步初始化能优化
- 异步初始化的坑在哪
理解原理,才能举一反三。只会背方案,面试官换个问法就懵了。
💡 预告:下一篇是「Android架构设计面试题」,讲MVC/MVP/MVVM/Clean架构的理解和实战选型。
如果这篇对你有帮助,点个赞让我知道。下篇见。