Android性能优化面试题:你说你会优化,结果连ANR都排查不了

Android性能优化面试题:你说你会优化,结果连ANR都排查不了?

性能优化这块,问深了特别容易暴露功底。今天咱们把Android性能优化的高频面试题过一遍,每道题都掰开了揉碎了讲,保证你面试不再心虚。

1. 冷启动优化:你做了哪些?怎么衡量?从点击图标到首帧渲染经历了什么?

核心回答

冷启动优化不是玄学,你得先知道它经历了什么,才能知道优化什么。

冷启动分为三个阶段:

  1. Launcher进程创建 → 点击图标,Launcher进程fork出APP进程
  2. Application初始化 → 执行attachBaseContext、onCreate,还有各种SDK的init
  3. 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 + 反射
    }
}

优化思路:

  1. 异步初始化 → 不阻塞主线程
  2. 延迟初始化 → 不在Application做,按需加载
  3. 预加载 → Splash阶段提前初始化
  4. 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开源的内存泄漏检测工具,原理很巧妙:

  1. ObjectWatcher:WeakReference + ReferenceQueue 监控对象
  2. WeakReference + GCWeakReference被GC回收时会进入ReferenceQueue,如果5秒后对象还在,说明泄漏了
  3. Hprof解析:dump heap,分析泄漏路径
  4. 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",而是:

  1. 怎么衡量:有没有量化指标
  2. 为什么这样:理解底层机制
  3. Trade-off:知道收益和风险

比如你优化了冷启动,你得知道:

  • 冷启动分哪几个阶段
  • 每个阶段耗时多少算正常
  • 为什么异步初始化能优化
  • 异步初始化的坑在哪

理解原理,才能举一反三。只会背方案,面试官换个问法就懵了。

💡 预告:下一篇是「Android架构设计面试题」,讲MVC/MVP/MVVM/Clean架构的理解和实战选型。

如果这篇对你有帮助,点个赞让我知道。下篇见。

相关推荐
richard_yuu1 小时前
鸿蒙本地数据存储实战|Preferences 封装、数据隔离与隐私合规存储方案
android·华为·harmonyos
Mahir081 小时前
Spring 事务深度解析:核心原理与 12 种事务失效场景全解
java·spring·面试·事务失效
木易 士心2 小时前
深入理解 OKHttp:设计模式、核心机制与架构优势
android·设计模式·架构
JAVA面经实录9172 小时前
Java 多线程完整版学习文档(无遗漏终版)
java·面试
Ehtan_Zheng2 小时前
Jetpack Compose `@ReadOnlyComposable` 的“魔法”
android
沐言人生2 小时前
ReactNative 源码分析11——Native View创建流程setChildren和manageChildren
android·react native
诸神黄昏EX2 小时前
Android Build系列专题【篇七:VINTF源码解析】
android
plainGeekDev2 小时前
Android Framework 面试题:Binder都说不清楚,简历别写精通了
android·java
萌新杰少2 小时前
安卓原生项目迁移KMP——核心迁移
android·kotlin·jetbrains