Android内存面试题:OOM都解决不了,性能优化从何谈起?

Android内存面试题:OOM都解决不了,性能优化从何谈起?

内存问题是Android开发中永恒的话题。一个再优雅的架构,如果逃不过OOM的制裁,终将沦为用户的"应用已崩溃"。这篇文章,我们把内存相关的核心面试题讲透。

题目一:Java内存模型与GC机制(年轻代为何频繁回收,老年代为何内存碎片?)

核心回答

JVM堆内存分为年轻代和老年代,新对象在Eden区分配,经历多次Minor GC仍存活的对象进入老年代。GC算法根据场景选择标记-清除、标记-整理或复制算法。Android的ART虚拟机演进出了并发标记清除(CMS)和并发复制(CC/GC)策略来减少停顿。

原理与代码

JVM堆内存分代
markdown 复制代码
堆内存布局:
┌─────────────────────────────────────────┐
│                   老年代                 │
│  ┌─────────────────────────────────────┐│
│  │         Old Generation              ││
│  └─────────────────────────────────────┘│
│                    │                     │
│         ┌──────────┴──────────┐          │
│  ┌──────┴──────┐              │          │
│  │ 年轻代       │ Survivor 0  │          │
│  │ Eden Space  │              │          │
│  └─────────────┴──────────────┘          │
└─────────────────────────────────────────┘

对象分配流程:
1. 新对象优先在Eden区分配
2. Eden区满时触发Minor GC,存活对象复制到Survivor区
3. 对象在Survivor区之间移动,每经历一次GC年龄+1
4. 年龄达到阈值(默认15)的对象进入老年代
Android GC演进

Dalvik时代:使用标记-清除算法,GC时必须Stop The World。

ART初期(Android 4.4-6.0) :引入CMS并发标记清除,大部分标记工作与应用并发执行,减少了停顿时间,但清理阶段仍可能需要短暂停顿。

ART现代化(Android 7.0+) :引入CC并发复制算法,使用复制算法而非标记-清除,完全消除了内存碎片问题,且大部分GC工作与应用并发进行。

kotlin 复制代码
/**
 * 观察不同对象的内存分配位置
 */
class MemoryAllocationDemo {
    
    companion object {
        val cachedItems = mutableListOf<HeavyObject>()  // 长期持有引用
    }
    
    fun demonstrateAllocation() {
        val results = mutableListOf<String>()
        
        // 场景1:循环中创建大量短期对象 - 在年轻代频繁回收
        for (i in 0 until 10000) {
            val tempObject = TemporaryObject("Temp_$i")
            results.add(tempObject.process())
        }
        
        // 场景2:字符串拼接 - 错误写法产生大量临时String
        var badString = ""
        for (i in 0 until 1000) {
            badString += "item$i,"  // 每次拼接创建新String
        }
        
        // 正确写法:使用StringBuilder
        val goodString = StringBuilder().apply {
            for (i in 0 until 1000) {
                append("item").append(i).append(",")
            }
        }.toString()
    }
    
    // 对象池模式 - 避免频繁分配
    private val objectPool = ArrayDeque<ReusableObject>(initialCapacity = 100)
    
    fun usePooledObject() {
        val obj = objectPool.pollFirst() ?: ReusableObject()
        try {
            obj.process()
        } finally {
            objectPool.addLast(obj)  // 归还而非等待GC
        }
    }
}

class TemporaryObject(private val name: String) {
    fun process(): String = "Processed: $name"
}

class ReusableObject {
    var data: Any? = null
    fun process() { data?.let { /* 处理 */ } }
    fun reset() { data = null }
}

/** 
 * GC日志分析示例
 * 日志格式:
 * 10.123: [GC (Allocation Failure) [PSYoungGen: 6144K->512K(7168K)] 12288K->6656K(20480K), 0.0156789 secs]
 */
object GCLogAnalyzer {
    // 含义:
    // - Allocation Failure: 触发原因(年轻代空间不足)
    // - 6144K->512K: 年轻代GC前 -> GC后
    // - 0.0156789 secs: GC耗时
    
    fun detectMemoryChurn(events: List<GCEvent>, thresholdSeconds: Double = 1.0): Boolean {
        if (events.size < 2) return false
        var recentGCount = 0
        var lastTimestamp = events.first().timestamp
        
        for (event in events) {
            if (event.timestamp - lastTimestamp < thresholdSeconds) {
                recentGCount++
                if (recentGCount > 3) return true  // 检测到频繁GC
            } else {
                recentGCount = 0
            }
            lastTimestamp = event.timestamp
        }
        return false
    }
}

data class GCEvent(
    val timestamp: Double,
    val gcType: String,
    val cause: String,
    val duration: Double
)

Android实战场景

列表滑动中的内存问题:RecyclerView快速滑动时,每个item创建新的Bitmap或复杂对象会导致内存飙升后迅速回落(内存抖动)。正确做法是使用ViewHolder复用机制,配合图片缓存。

kotlin 复制代码
class StartupMemoryOptimization {
    
    // 反面:启动时加载所有数据
    fun badStartup(dataRepository: DataRepository) {
        val allData = dataRepository.getAllData()  // 触发大量对象分配
        processData(allData)
    }
    
    // 正确:分批加载
    suspend fun goodStartup(dataRepository: DataRepository) {
        val pageSize = 100
        var page = 0
        
        while (true) {
            val batch = dataRepository.getDataPage(page, pageSize)
            if (batch.isEmpty()) break
            processData(batch)
            page++
        }
    }
}

面试加分点

  1. Zygote预加载机制:Android启动时,Zygote进程会预加载常用Java类和资源到共享内存,所有应用进程fork继承。理解这个机制有助于理解应用启动时的内存开销。
  2. Read-Only共享区域:ART将预加载的类信息放在只读共享区域,对象实际分配在私有读写区域。这解释了为什么同一个类的不同实例会共享元数据。
  3. GC配合编译器优化:ART的AOT/JIT编译会识别热点代码进行优化,如逃逸分析------如果对象不会逃逸出方法作用域,可以在栈上分配而不是堆上。

题目二:常见内存泄漏场景(静态引用、单例、Handler,根源是什么?)

核心回答

内存泄漏的本质是对象持有超出其生命周期所需的引用,导致GC无法回收本应销毁的对象。Android中常见泄漏场景包括静态持有Context、非静态内部类隐式持有外部引用、未取消的监听器回调等。

原理与代码

静态引用持有Activity Context
kotlin 复制代码
// 错误写法:静态变量持有Activity引用
class BadStaticContext {
    companion object {
        val activityCache = mutableListOf<Activity>()  // Activity永远无法释放
        var lastActivityContext: Context? = null  // 静态持有
    }
    
    fun onCreate(activity: Activity) {
        activityCache.add(activity)  // 泄漏
        lastActivityContext = activity
    }
}

// 正确写法:使用弱引用
class GoodStaticContext {
    companion object {
        private val activityCache = mutableListOf<WeakReference<Activity>>()
        
        fun cleanExpired() {
            activityCache.removeIf { it.get() == null }
        }
        
        fun getLastActivity(): Activity? = activityCache.lastOrNull()?.get()
    }
}
非静态内部类泄漏
kotlin 复制代码
// 错误写法:Handler作为非静态内部类
class LeakHandlerActivity : AppCompatActivity() {
    
    private val handler = object : Handler(Looper.getMainLooper()) {
        // 匿名内部类隐式持有Activity引用
        // Activity.onDestroy()后消息队列中还有消息 -> 泄漏
        override fun handleMessage(msg: Message) {
            // 可直接访问Activity成员
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handler.sendMessageDelayed(Message.obtain().apply { what = 1 }, 10000)
    }
    
    // 正确做法1:静态内部类 + 弱引用
    class SafeHandler(private val activity: WeakReference<Activity>) : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            activity.get()?.let { act ->
                // 安全访问
            }
        }
    }
    
    // 正确做法2:使用lifecycle-aware协程(推荐)
    class LifecycleAwareActivity : AppCompatActivity() {
        private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            scope.launch {
                delay(10000)
                // 无需手动取消,lifecycle自动管理
            }
        }
    }
}

// 错误写法:Timer + Runnable
class BadRunnableDemo : AppCompatActivity() {
    private val runnable = object : Runnable {
        override fun run() { /* 访问UI */ }
    }
    
    fun scheduleWork() {
        Timer().schedule(runnable, 5000)  // 5秒后执行,Activity已销毁则泄漏
    }
}

// 正确做法:确保任务可取消
class GoodRunnableDemo : AppCompatActivity() {
    private val cancellableRunnable = CancellableRunnable { updateProgress() }
    private var timer: Timer? = null
    
    override fun onDestroy() {
        super.onDestroy()
        cancellableRunnable.cancel()
        timer?.cancel()
    }
}

abstract class CancellableRunnable(private val block: () -> Unit) : Runnable {
    @Volatile private var cancelled = false
    
    override fun run() { if (!cancelled) block() }
    fun cancel() { cancelled = true }
}
单例模式泄漏
kotlin 复制代码
// 错误:单例持有Activity Context
class BadSingleton private constructor(private val context: Context) {
    companion object {
        @Volatile private var instance: BadSingleton? = null
        fun getInstance(context: Context) = instance ?: synchronized(this) {
            instance ?: BadSingleton(context.applicationContext).also { instance = it }
        }
    }
}

// 正确:使用Application Context
class GoodSingleton private constructor() {
    companion object {
        @Volatile private var instance: GoodSingleton? = null
        fun getInstance() = instance ?: synchronized(this) {
            instance ?: GoodSingleton().also { instance = it }
        }
    }
}
监听器和广播未注销
kotlin 复制代码
class BroadcastLeakDemo : AppCompatActivity() {
    
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            when (intent?.action) {
                Intent.ACTION_SCREEN_OFF -> handleScreenOff()
            }
        }
    }
    
    override fun onResume() {
        super.onResume()
        registerReceiver(receiver, IntentFilter().apply {
            addAction(Intent.ACTION_SCREEN_OFF)
        })
    }
    
    override fun onPause() {
        super.onPause()
        unregisterReceiver(receiver)  // 正确做法:及时注销
    }
}
ViewModel中的协程泄漏
kotlin 复制代码
// 错误:使用普通CoroutineScope
class LeakingViewModel : ViewModel() {
    private val scope = CoroutineScope(Dispatchers.Main)
    
    fun loadData() {
        scope.launch { _liveData.value = repository.fetchData() }
    }
    // ViewModel.onCleared()不被调用时,协程不会取消
}

// 正确:使用viewModelScope
class CorrectViewModel(private val repository: DataRepository) : ViewModel() {
    private val _data = MutableLiveData<List<Item>>()
    val data: LiveData<List<Item>> = _data
    
    fun loadData() {
        viewModelScope.launch {
            _data.value = repository.fetchData()
        }
    }
}

Android实战场景

kotlin 复制代码
// 封装生命周期感知的组件
class LifecycleAwareComponent<T>(
    private val lifecycleOwner: LifecycleOwner,
    private val onDestroy: () -> Unit
) {
    init {
        lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) { onDestroy() }
        })
    }
}

class UsageInActivity : AppCompatActivity() {
    private lateinit var component: LifecycleAwareComponent<Unit>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        component = LifecycleAwareComponent(this) { releaseResources() }
    }
    
    private fun releaseResources() { /* 注销监听、取消订阅 */ }
}

面试加分点

  1. finalize方法的延迟:对象即使不可达,如果finalize方法耗时较长或等待队列,会延迟回收。
  2. onTerminate不靠谱:Activity.onTerminate()在真实设备上几乎不会调用,清理逻辑应在onDestroy或onPause中执行。
  3. Kotlin协程的Structured Concurrency:Kotlin 1.3+的协程使用结构化并发,协程作用域被取消时所有子协程自动取消。
  4. 第三方库也需关注:某些第三方库自身可能存在泄漏,使用时也要关注其内存行为。

题目三:LeakCanary原理(WeakReference如何检测泄漏,引用链如何构建?)

核心回答

LeakCanary通过WeakReference+ReferenceQueue机制监听对象回收情况。当对象应该被回收却没有被回收时,触发heap dump并使用Shark引擎分析引用链,最终定位泄漏源头。

原理与代码

WeakReference与ReferenceQueue机制
kotlin 复制代码
// 基础WeakReference示例
class WeakReferenceDemo {
    fun basicWeakReference() {
        val strongRef = Any()
        val queue = ReferenceQueue<Any>()
        val weakRef = WeakReference(strongRef, queue)
        
        println(weakRef.get())  // 正常访问
        
        // 切断强引用后,对象可能被GC回收
        // weakRef.get() 可能返回null
        // ref会被加入ReferenceQueue
    }
}

// 自定义简化版RefWatcher
class SimpleRefWatcher(
    private val watchedExecutor: Executor = Executors.newSingleThreadExecutor()
) {
    private val watchedReferences = mutableMapOf<String, WeakReference<Any>>()
    private val queue = ReferenceQueue<Any>()
    private val retainedReferences = mutableMapOf<String, WeakReference<Any>>()
    
    fun watch(obj: Any, name: String) {
        val reference = WeakReference(obj, queue)
        watchedReferences[name] = reference
        
        watchedExecutor.execute {
            checkWeaklyReachable(name, reference)
        }
    }
    
    private fun checkWeaklyReachable(name: String, reference: WeakReference<Any>) {
        // 从引用队列中取出已被回收的引用
        while (queue.poll() != null) {
            watchedReferences.entries.removeIf { (_, v) -> v === queue.poll() }
        }
        
        val watchedObj = reference.get()
        if (watchedObj != null) {
            // 对象仍存在,可能泄漏
            retainedReferences[name] = reference
            analyzeReference(name, watchedObj)
        }
    }
    
    private fun analyzeReference(name: String, leakedObject: Any) {
        // 1. 生成hprof文件
        val hprofPath = dumpHeap()
        
        // 2. 解析hprof,构建引用链
        val referenceChain = findReferenceChain(leakedObject)
        
        // 3. 报告泄漏
        reportLeak(name, referenceChain)
    }
    
    private fun dumpHeap(): String {
        val path = File.createTempFile("leakcanary-hprof", ".hprof")
        Debug.dumpHprofData(path.absolutePath)
        return path.absolutePath
    }
    
    private fun findReferenceChain(obj: Any): List<ReferenceInfo> {
        // Shark引擎的核心功能:
        // 识别强引用、软引用、弱引用、静态变量引用、Finalizer引用
        return listOf(
            ReferenceInfo("java.lang.Thread", "thread", ReferenceType.STATIC),
            ReferenceInfo("com.example.MyActivity", "singleton", ReferenceType.STRONG),
            ReferenceInfo("com.example.MyActivity", "handler", ReferenceType.FIELD)
        )
    }
    
    private fun reportLeak(name: String, referenceChain: List<ReferenceInfo>) {
        println("=== Leak Detected ===")
        println("Leaking: $name")
        referenceChain.forEachIndexed { index, info ->
            val indent = "  ".repeat(index)
            println("$indent${info.type} ${info.className}.${info.fieldName}")
        }
    }
}

data class ReferenceInfo(
    val className: String,
    val fieldName: String,
    val type: ReferenceType
)

enum class ReferenceType { STATIC, FIELD, LOCAL, FINALIZER }
ActivityRefWatcher实现
kotlin 复制代码
class ActivityRefWatcher(
    private val application: Application,
    private val refWatcher: SimpleRefWatcher
) {
    private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
        override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
        override fun onActivityStarted(activity: Activity) {}
        override fun onActivityResumed(activity: Activity) {}
        override fun onActivityPaused(activity: Activity) {}
        override fun onActivityStopped(activity: Activity) {}
        override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
        override fun onActivityDestroyed(activity: Activity) {
            // 关键:Activity销毁时开始监听
            refWatcher.watch(activity, "${activity.javaClass.simpleName}")
        }
    }
    
    fun startWatching() {
        application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
    }
}

// 初始化
class LeakCanaryInit {
    fun init(application: Application) {
        val refWatcher = SimpleRefWatcher()
        val activityWatcher = ActivityRefWatcher(application, refWatcher)
        activityWatcher.startWatching()
    }
}

Android实战场景

为什么LeakCanary只能检测Java堆泄漏

  1. Native内存不在Java堆中:Native内存直接由系统分配,不经过JVM,无法用WeakReference追踪。
  2. 没有JVM元数据:Native对象没有对象头和引用信息。
  3. 生命周期不确定:Native代码可能持有对象指针,但JVM无法感知。
kotlin 复制代码
// 实际项目中的LeakCanary配置
class LeakCanaryConfig {
    fun createConfig() = LeakCanary.config.copy(
        maxStoredHeapDumps = 7,
        heapDumpRetentionSeconds = 5 * 60,
        automaticAnalysis = true,
        // 排除已知泄漏
        excludedRefs = AndroidExcludedRefs.createAppDefaults().build()
    )
}

面试加分点

  1. Shark引擎优势:相比老版本HAHA,Shark使用更高效的hprof解析算法,内存占用更小,分析速度更快。Shark直接读取hprof二进制格式,不需要将整个文件加载到内存。
  2. LeakCanary 2.0改进 :从反射手动dump变为使用Debug.dumpHprofData()官方API,从HAHA解析变为Shark,更稳定更快。
  3. Android 10+隐私变化:heap dump需要应用持有READ_LOGS权限或使用官方API,LeakCanary已适配。
  4. 可自定义分析规则:LeakCanary提供ExcludedRefs API,可忽略已知系统泄漏或第三方库泄漏,避免误报。

题目四:大图加载与Bitmap优化(一张1920x1080的Bitmap占多少内存?)

核心回答

Bitmap内存占用 = 宽 x 高 x 每像素字节数。ARGB_8888格式每像素4字节,一张1080P全彩Bitmap约8MB。优化策略包括采样压缩、选择合适像素格式、BitmapPool复用、双层缓存等。

原理与代码

Bitmap内存计算
kotlin 复制代码
/**
 * 内存占用公式:bytes = width × height × bytesPerPixel
 * 
 * 像素格式:
 * - ARGB_8888: 4字节(默认,质量最高)
 * - RGB_565: 2字节(无Alpha通道)
 * - ALPHA_8: 1字节(只有Alpha)
 */
object BitmapMemoryCalculator {
    fun calculateMemorySize(width: Int, height: Int, config: Bitmap.Config): Long {
        val bytesPerPixel = when (config) {
            Bitmap.Config.ARGB_8888 -> 4L
            Bitmap.Config.RGB_565 -> 2L
            Bitmap.Config.ALPHA_8 -> 1L
            else -> 4L
        }
        return width.toLong() * height * bytesPerPixel
    }
    
    fun showExamples() {
        // Full HD ARGB_8888: 1920×1080×4 = 8.3MB
        // Full HD RGB_565: 1920×1080×2 = 4.2MB
        // 4K ARGB_8888: 3840×2160×4 = 33MB
        // Thumbnail 100x100: 39KB
    }
}

/** 
 * 采样压缩核心参数:inSampleSize
 */
class BitmapSampler {
    fun calculateInSampleSize(
        srcWidth: Int, srcHeight: Int,
        reqWidth: Int, reqHeight: Int
    ): Int {
        var inSampleSize = 1
        if (srcHeight > reqHeight || srcWidth > reqWidth) {
            val halfHeight = srcHeight / 2
            val halfWidth = srcWidth / 2
            while ((halfHeight / inSampleSize) >= reqHeight &&
                   (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2
            }
        }
        return inSampleSize
    }
    
    fun loadSampledBitmap(context: Context, resId: Int, reqWidth: Int, reqHeight: Int): Bitmap? {
        val options = BitmapFactory.Options().apply {
            inJustDecodeBounds = true
        }
        BitmapFactory.decodeResource(context.resources, resId, options)
        
        options.inSampleSize = calculateInSampleSize(
            options.outWidth, options.outHeight, reqWidth, reqHeight)
        options.inJustDecodeBounds = false
        options.inPreferredConfig = Bitmap.Config.RGB_565  // 无透明用RGB_565省内存
        
        return BitmapFactory.decodeResource(context.resources, resId, options)
    }
}
安全的图片加载工具
kotlin 复制代码
class SafeBitmapLoader private constructor(private val context: Context) {
    
    private val memoryCache: LruCache<String, Bitmap>
    private val bitmapPool: BitmapPool
    
    init {
        val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
        val cacheSize = maxMemory / 8
        memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
            override fun sizeOf(key: String, bitmap: Bitmap) = bitmap.allocationByteCount / 1024
        }
        bitmapPool = BitmapPool(maxMemory / 4)
    }
    
    companion object {
        @Volatile private var instance: SafeBitmapLoader? = null
        fun getInstance(context: Context) = instance ?: synchronized(this) {
            instance ?: SafeBitmapLoader(context.applicationContext).also { instance = it }
        }
    }
    
    fun loadImage(source: ImageSource, targetWidth: Int, targetHeight: Int): Bitmap? {
        val cacheKey = source.getCacheKey(targetWidth, targetHeight)
        memoryCache.get(cacheKey)?.let { return it }
        
        val bitmap = when (source) {
            is ImageSource.Resource -> loadFromResource(source.resId, targetWidth, targetHeight)
            is ImageSource.File -> loadFromFile(source.file, targetWidth, targetHeight)
            is ImageSource.Url -> loadFromUrl(source.url, targetWidth, targetHeight)
        }
        
        memoryCache.put(cacheKey, bitmap)
        return bitmap
    }
    
    private fun loadFromResource(resId: Int, reqWidth: Int, reqHeight: Int): Bitmap {
        val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
        BitmapFactory.decodeResource(context.resources, resId, options)
        
        options.inSampleSize = calculateInSampleSize(
            options.outWidth, options.outHeight, reqWidth, reqHeight)
        options.inJustDecodeBounds = false
        
        // BitmapPool复用
        val reusableBitmap = bitmapPool.get(
            options.outWidth / options.inSampleSize,
            options.outHeight / options.inSampleSize,
            options.inPreferredConfig
        )
        if (reusableBitmap != null) options.inBitmap = reusableBitmap
        
        options.inPreferredConfig = Bitmap.Config.RGB_565
        return BitmapFactory.decodeResource(context.resources, resId, options)
    }
    
    private fun loadFromFile(file: File, reqWidth: Int, reqHeight: Int): Bitmap {
        val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
        BitmapFactory.decodeFile(file.absolutePath, options)
        options.inSampleSize = calculateInSampleSize(
            options.outWidth, options.outHeight, reqWidth, reqHeight)
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeFile(file.absolutePath, options)
    }
    
    private suspend fun loadFromUrl(url: String, reqWidth: Int, reqHeight: Int): Bitmap {
        val bytes = downloadImage(url)
        return decodeBytes(bytes, reqWidth, reqHeight)
    }
    
    private fun downloadImage(url: String): ByteArray { return byteArrayOf() }
    
    private fun decodeBytes(bytes: ByteArray, reqWidth: Int, reqHeight: Int): Bitmap {
        val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
        BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
        options.inSampleSize = calculateInSampleSize(
            options.outWidth, options.outHeight, reqWidth, reqHeight)
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
    }
    
    inner class BitmapPool(private val maxSize: Int) {
        private val pool = object : LinkedHashMap<String, Bitmap>(16, 0.75f, true) {
            override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Bitmap>?) = size > maxSize
        }
        
        @Synchronized
        fun get(width: Int, height: Int, config: Bitmap.Config?): Bitmap? {
            val key = "${width}x${height}_${config ?: "unknown"}"
            return pool.remove(key)?.takeIf { !it.isRecycled && it.isMutable }
        }
        
        @Synchronized
        fun put(bitmap: Bitmap) {
            if (bitmap.isRecycled) return
            val key = "${bitmap.width}x${bitmap.height}_${bitmap.config}"
            pool[key] = bitmap
        }
    }
}

sealed class ImageSource {
    abstract fun getCacheKey(targetWidth: Int, targetHeight: Int): String
    
    class Resource(val resId: Int) : ImageSource() {
        override fun getCacheKey(tw: Int, th: Int) = "res_${resId}_${tw}x${th}"
    }
    class File(val file: File) : ImageSource() {
        override fun getCacheKey(tw: Int, th: Int) = "file_${file.absolutePath}_${tw}x${th}"
    }
    class Url(val url: String) : ImageSource() {
        override fun getCacheKey(tw: Int, th: Int) = "url_${url.hashCode()}_${tw}x${th}"
    }
}
大图加载方案
kotlin 复制代码
/**
 * BitmapRegionDecoder:只加载图片的指定区域
 * 适合超长图:只加载可见部分
 */
class LargeImageLoader(private val context: Context) {
    fun loadRegion(imagePath: String, rect: Rect, reqWidth: Int, reqHeight: Int): Bitmap? {
        val decoder = BitmapRegionDecoder.newInstance(imagePath, false) ?: return null
        val options = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.ARGB_8888 }
        val bitmap = decoder.decodeRegion(rect, options)
        decoder.recycle()
        
        // 区域太大则缩放
        return if (bitmap.width > reqWidth || bitmap.height > reqHeight) {
            scaleBitmap(bitmap, reqWidth, reqHeight)
        } else bitmap
    }
    
    private fun scaleBitmap(bitmap: Bitmap, reqWidth: Int, reqHeight: Int): Bitmap {
        val matrix = Matrix()
        val scale = minOf(reqWidth.toFloat() / bitmap.width, reqHeight.toFloat() / bitmap.height)
        matrix.postScale(scale, scale)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
    }
}

/** 
 * Glide vs Coil 缓存策略对比
 * 
 * Glide:
 * - ActiveResources: 弱引用持有正在使用的Bitmap
 * - MemoryCache: LRU缓存完整Bitmap
 * - BitmapPool: 复用已回收Bitmap
 * - DiskCache: 二级缓存(原始+处理后)
 * 
 * Coil:
 * - referenceCache: 弱引用缓存
 * - memoryCache: LRU缓存
 * - bitmapPool: 高效ArrayDeque实现
 * - 更激进地缓存downsampled数据节省磁盘
 */

Android实战场景

kotlin 复制代码
// Glide最佳实践
class ProductViewHolder(private val binding: ProductItemBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(product: Product) {
        Glide.with(productImage)
            .load(product.imageUrl)
            .transition(DrawableTransitionOptions.withCrossFade())
            .centerCrop()
            .override(300, 300)  // 限制尺寸
            .into(productImage)
    }
}

// Coil最佳实践(更简洁)
class ProductViewHolderCoil(private val binding: ProductItemBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(product: Product) {
        binding.productImage.load(product.imageUrl) {
            crossfade(true)
            size(300, 300)
        }
    }
}

面试加分点

  1. GPU纹理限制:GPU对纹理有最大尺寸限制(通常2048x2048或4096x4096),超出限制的Bitmap无法直接用于硬件加速渲染。
  2. inBitmap参数限制:从Android 4.4开始可复用已有Bitmap,但必须大小相等(或更小)且像素格式兼容。
  3. WebP格式优势:相比PNG/JPEG,WebP文件更小(通常减少25-35%),解码后内存占用相同。
  4. SubsamplingScaleImageView:对于超长图,Google官方库使用Tiles机制只渲染可见区域,是更专业的解决方案。

题目五:Memory Churn(为什么Profiler里内存曲线像锯齿?)

核心回答

Memory Churn是指在短时间内频繁创建和回收大量短期对象,导致GC频繁触发和UI停顿。常见于onDraw中创建对象、循环中字符串拼接、自动装箱等场景。在Profiler中表现为锯齿形的内存曲线。

原理与代码

onDraw中创建对象
kotlin 复制代码
// 错误:在onDraw中创建对象
class BadCustomView : View {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        
        // 错误1:每次重绘创建Paint
        val paint = Paint().apply { color = Color.RED }
        
        // 错误2:每次重绘创建Path
        val path = Path().apply { moveTo(0f, 0f); lineTo(width.toFloat(), height.toFloat()) }
        
        // 错误3:字符串拼接创建新String
        val text = "Value: " + System.currentTimeMillis()
        
        // 错误4:创建数组
        val points = floatArrayOf(0f, 0f, width.toFloat(), height.toFloat())
        
        canvas.drawText(text, 0f, 50f, paint)
    }
}

// 正确:对象提到类成员变量
class GoodCustomView : View {
    private val paint = Paint().apply { color = Color.RED }
    private val path = Path()
    private val textBuilder = StringBuilder(50)
    
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        
        // 只更新需要变化的部分
        textBuilder.setLength(0)
        textBuilder.append("Value: ").append(System.currentTimeMillis())
        
        path.reset()
        path.moveTo(0f, 0f)
        path.lineTo(width.toFloat(), height.toFloat())
        
        canvas.drawText(textBuilder, 0f, 50f, paint)
    }
}
字符串拼接与自动装箱
kotlin 复制代码
// 错误:循环中字符串拼接
class StringChurnDemo {
    fun badStringConcat() {
        var result = ""
        for (i in 0 until 1000) {
            result += "item$i,"  // 1000个临时String
        }
    }
    
    // 正确:StringBuilder
    fun goodStringConcat() = StringBuilder(2000).apply {
        for (i in 0 until 1000) append("item").append(i).append(",")
    }.toString()
    
    // 正确:joinToString
    fun bestStringConcat() = (0 until 1000).map { "item$it" }.joinToString(",")
}

// 错误:大量自动装箱
class AutoboxingDemo {
    fun bad() {
        var sum: Long = 0L
        for (i in 0 until 1000000) {
            sum += i  // 每次+创建新Long
        }
        val list = ArrayList<Int>()
        for (i in 0 until 10000) {
            list.add(i)  // 每次add都装箱
        }
    }
    
    // 正确:使用基本类型数组
    fun good() {
        val array = LongArray(1000000)
        var sum = 0L
        for (i in array.indices) sum += array[i]
    }
}

Android实战场景

kotlin 复制代码
// RecyclerView中的优化
class GoodAdapter : RecyclerView.Adapter<GoodAdapter.ViewHolder>() {
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.textView)
        private val paint = Paint().apply { isAntiAlias = true }
        private val textBounds = Rect()
        
        fun bind(data: ItemData) {
            paint.textSize = data.textSize
            textView.paint = paint
            textView.text = data.text
        }
    }
}

面试加分点

  1. Kotlin的inline :标准库函数如repeatlet大部分是inline的,不创建额外对象。自定义高阶函数要加inline修饰符才能享受优化。
  2. ArrayMap vs HashMap:小数据量(<1000)ArrayMap更省内存,大数据量HashMap的O(1)查找更有优势。
  3. SparseArray系列:SparseIntArray、SparseLongArray避免自动装箱,Integer到Object映射用SparseArray比HashMap更省内存。

题目六:Native内存泄漏(Bitmap回收了,Native内存还在涨?)

核心回答

Native内存不在Java堆中,不受GC管理,由手动分配释放。泄漏后内存持续增长直到OOM。常见来源包括Bitmap的native层、JNI直接分配、mmap映射等。

原理与代码

Bitmap的Native层泄漏
kotlin 复制代码
/**
 * Bitmap内存组成:
 * - Android 8.0前:Java堆(像素)+ Native堆(元数据)
 * - Android 8.0+:ashmem共享内存
 * 
 * recycle()在8.0前释放Native内存,8.0+由GC管理
 */
class BitmapLeakDemo {
    private val bitmaps = mutableListOf<Bitmap>()
    
    fun loadManyBitmaps(context: Context) {
        for (i in 0 until 100) {
            val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.image)
            bitmaps.add(bitmap)  // 持有引用,从不释放
        }
    }
    
    fun releaseAll() {
        bitmaps.forEach { it.recycle() }
        bitmaps.clear()
    }
}
JNI内存管理
kotlin 复制代码
/** 
 * JNI引用类型:
 * 1. Local Reference:方法返回后自动释放
 * 2. Global Reference:手动管理,NewGlobalRef/DeleteGlobalRef
 * 3. Weak Global Reference:可能GC回收
 */

/**
 * Kotlin调用Native的正确姿势
 */
class NativeBitmapProcessor {
    companion object {
        init { System.loadLibrary("bitmap_processor") }
    }
    
    external fun createBitmap(width: Int, height: Int): Long
    external fun releaseBitmap(handle: Long)
    
    fun kotlinUsage(): Bitmap? {
        val handle = createBitmap(1920, 1080)
        return try {
            // 处理
            createKotlinBitmap(handle)
        } finally {
            releaseBitmap(handle)  // 必须释放
        }
    }
    
    private fun createKotlinBitmap(handle: Long): Bitmap? = null
}

/** 
 * RAII模式管理Native资源
 */
class NativeLibWrapper : Closeable {
    private val resource: NativeResource
    
    init {
        System.loadLibrary("mynative")
        resource = NativeResourceImpl(nativeCreate())
    }
    
    fun process() = resource.process()
    
    override fun close() = resource.release()
    
    private external fun nativeCreate(): Long
    private external fun nativeProcess(handle: Long)
    private external fun nativeRelease(handle: Long)
    
    class NativeResourceImpl(private val handle: Long) : NativeResource {
        init { require(handle != 0L) { "Failed to create" } }
        override fun process() = nativeProcess(handle)
        override fun release() = nativeRelease(handle)
    }
}

interface NativeResource { fun process(); fun release() }
mmap内存泄漏
kotlin 复制代码
/**
 * mmap泄漏场景:
 * - 加载大文件后忘记munmap
 * - Native库临时文件映射
 * - 进程异常退出时映射未清理
 */

/** 
 * 手动检查/proc/self/maps
 */
object ProcMemAnalyzer {
    fun findLargeAnonymousMappings(thresholdMB: Int = 50): List<MappingInfo> {
        val threshold = thresholdMB * 1024 * 1024L
        return File("/proc/self/maps")
            .readLines()
            .mapNotNull { parseLine(it) }
            .filter { 
                it.permissions.startsWith("rw") && 
                it.pathname.isEmpty() &&
                isLargeMapping(it.addressRange, threshold)
            }
    }
    
    private fun parseLine(line: String): MappingInfo? {
        val parts = line.trim().split("\s+".toRegex())
        return if (parts.size >= 6) MappingInfo(
            parts[0], parts[1], parts[2], parts[3], parts[4],
            if (parts.size > 5) parts[5] else ""
        ) else null
    }
    
    private fun isLargeMapping(range: String, threshold: Long) = try {
        val parts = range.split("-")
        (parts[1].toLong(16) - parts[0].toLong(16)) >= threshold
    } catch (e: Exception) { false }
}

data class MappingInfo(
    val addressRange: String,
    val permissions: String,
    val offset: String,
    val device: String,
    val inode: String,
    val pathname: String
)

Android实战场景

kotlin 复制代码
// 实际项目中Native泄漏排查
class NativeLeakInvestigation {
    // 步骤1:确认是Native泄漏还是Java堆泄漏
    // 观察Memory Profiler:Java堆稳定但Native增长 -> Native泄漏
    
    // 步骤2:检查Bitmap使用
    // 统计当前Bitmap数量和内存占用
    
    // 步骤3:检查是否有图片没有recycle
    // 图片加载框架缓存了Bitmap但退出时没清理
}

/**
 * Bitmap追踪器
 */
object BitmapTracker {
    private val trackedBitmaps = ConcurrentHashMap<Int, BitmapInfo>()
    private var nextId = 0
    
    fun track(bitmap: Bitmap, tag: String) {
        trackedBitmaps[nextId++] = BitmapInfo(
            bitmap = bitmap, tag = tag,
            size = bitmap.allocationByteCount, createdAt = System.currentTimeMillis()
        )
    }
    
    fun getTotalMemory() = trackedBitmaps.values.sumOf { it.size }
}

data class BitmapInfo(val bitmap: Bitmap, val tag: String, val size: Long, val createdAt: Long)

面试加分点

  1. **Android 8.0的GraphicBuffer **:使用ashmem共享内存减少Native泄漏可能性,但老Native代码仍可能有问题。
  2. **ASan vs Valgrind **:Valgrind开销10-50x,ASan开销约2x。ASan更适合集成测试。
  3. **JNI Local Reference限制 **:循环中创建的Local Reference必须手动DeleteLocalRef,Native方法栈内存有限。

题目七:Memory Profiler实战(如何dump heap,如何分析引用链?)

核心回答

Memory Profiler通过Heap Dump分析对象占用和引用关系,通过Allocation Tracker跟踪对象分配来源。两者结合可精确定位内存泄漏和内存抖动。

原理与代码

Heap Dump分析流程
kotlin 复制代码
/** 
 * Hprof文件解析
 * Shark引擎流程:
 * 1. 读取hprof
 * 2. 构建对象图
 * 3. 从GC Root标记可达对象
 * 4. 不可达对象即为泄漏
 * 5. 分析引用链
 */

/**
 * GC Root类型:
 * - 线程栈局部变量
 * - JNI全局引用
 * - 启动类加载器加载的类
 * - 监视器对象
 * - 系统类加载器加载的类
 */
class HeapDumpAnalyzer {
    
    fun analyze(hprofFile: File): LeakAnalysisResult {
        val parser = HprofParser(hprofFile)
        val instances = parser.parseInstances()
        val gcRoots = parser.parseGCRoots()
        
        // 构建可达图
        val reachable = buildReachableSet(instances, gcRoots)
        
        // 找出泄漏对象
        val leaks = instances.filter { it !in reachable }
            .map { calculateRetainedSize(it, reachable) }
            .sortedByDescending { it.retainedSize }
            .take(100)
        
        return LeakAnalysisResult(leaks = leaks, totalInstances = instances.size)
    }
    
    private fun buildReachableSet(
        instances: List<HprofInstance>,
        gcRoots: List<GCRoot>
    ): Set<HprofInstance> {
        val reachable = mutableSetOf<HprofInstance>()
        val queue = ArrayDeque<HprofInstance>()
        
        gcRoots.flatMap { it.references }
            .filterIsInstance<HprofInstance>()
            .forEach { queue.add(it) }
        
        while (queue.isNotEmpty()) {
            val instance = queue.pollFirst()
            if (instance !in reachable) {
                reachable.add(instance)
                instance.references.filterIsInstance<HprofInstance>().forEach { queue.add(it) }
            }
        }
        return reachable
    }
    
    private fun calculateRetainedSize(instance: HprofInstance, reachable: Set<HprofInstance>): LeakInfo {
        var size = instance.shallowSize
        instance.references.filterIsInstance<HprofInstance>()
            .filter { it in reachable }
            .forEach { size += calculateRetainedSize(it, reachable) }
        return LeakInfo(instance, size)
    }
}

data class LeakAnalysisResult(val leaks: List<LeakInfo>, val totalInstances: Int)
data class LeakInfo(val instance: HprofInstance, val retainedSize: Long)

class HprofParser(val file: File) {
    fun parseInstances(): List<HprofInstance> = emptyList()
    fun parseGCRoots(): List<GCRoot> = emptyList()
}

open class HprofInstance {
    val shallowSize: Long = 0
    val references: List<Any> = emptyList()
}

class GCRoot {
    val references: List<Any> = emptyList()
}
Allocation Tracker分析
kotlin 复制代码
/** 
 * Allocation Tracker记录一段时间内所有对象分配
 */
class AllocationAnalyzer {
    
    fun findHotspots(records: List<AllocationRecord>): List<AllocationSummary> {
        return records
            .groupBy { it.methodInfo }
            .map { (method, recs) ->
                AllocationSummary(
                    method = method,
                    allocationCount = recs.size,
                    totalSize = recs.sumOf { it.size }
                )
            }
            .sortedByDescending { it.allocationCount }
            .take(20)
    }
    
    // 找出循环中的对象分配
    fun findLoopAllocations(records: List<AllocationRecord>): List<LoopAllocation> {
        return records
            .groupBy { "${it.methodInfo}_${it.threadName}" }
            .mapNotNull { (key, recs) ->
                if (recs.size > 50) {
                    val timeSpan = recs.last().timestamp - recs.first().timestamp
                    if (timeSpan < 1000) {
                        LoopAllocation(recs.first().methodInfo, recs.size, timeSpan)
                    } else null
                } else null
            }
            .sortedByDescending { it.count }
    }
}

data class AllocationRecord(
    val className: String,
    val methodInfo: String,
    val threadName: String,
    val size: Int,
    val timestamp: Long
)

data class AllocationSummary(
    val method: String,
    val allocationCount: Int,
    val totalSize: Long
)

data class LoopAllocation(
    val method: String,
    val count: Int,
    val timeSpan: Long
)
定位Activity泄漏的完整步骤
kotlin 复制代码
/**
 * 实战:定位Activity泄漏
 * 
 * 步骤1:复现问题
 * - Memory Profiler点击Record
 * - 进入目标Activity
 * - 点击返回键退出
 * - 停止录制
 * - 按类名过滤 "MainActivity"
 * 
 * 步骤2:分析Heap Dump
 * - 找出Activity实例
 * - 右键 -> Go to Instance
 * - 查看引用链
 * 
 * 步骤3:验证修复
 */
class ActivityLeakAnalysis {
    
    fun analyze(instance: HprofInstance): LeakAnalysisResult {
        val chain = findReferenceChain(instance)
        
        return LeakAnalysisResult(
            leakingRef = chain.lastOrNull(),
            suggestedFix = suggestFix(chain)
        )
    }
    
    private fun findReferenceChain(instance: HprofInstance): List<RefInfo> {
        val chain = mutableListOf<RefInfo>()
        var current: Any = instance
        while (current is HprofInstance) {
            val ref = findIncomingRef(current) ?: break
            chain.add(ref)
            current = ref.holder
        }
        return chain
    }
    
    private fun findIncomingRef(obj: Any): RefInfo? = null
    
    private fun suggestFix(chain: List<RefInfo>): String {
        if (chain.isEmpty()) return "无法确定"
        val last = chain.last()
        return when {
            last.fieldName.contains("sInstance") -> "单例持有Activity,改用WeakReference"
            last.className.contains("Handler") -> "Handler内部类泄漏,使用静态内部类+弱引用"
            last.fieldName.contains("listener") -> "未注销监听器,在onDestroy注销"
            else -> "检查${last.className}.${last.fieldName}"
        }
    }
}

data class RefInfo(val holder: Any, val className: String, val fieldName: String)

Android实战场景

less 复制代码
/** 
 * Memory Profiler使用技巧
 * 
 * 1. "Arrange by package"分组:快速找到自己包的类
 * 2. "Group by call site":查看同一位置分配的对象
 * 3. "Record stack traces":追踪分配来源(开销大)
 * 4. "Path to GC root":找出对象不被回收的原因
 * 5. 关注Retained Size:Shallow Size是自身大小,Retained Size是对象+可达对象大小
 */

/**
 * 常见内存问题速查
 */
object MemoryCheatSheet {
    val problems = listOf(
        Problem("返回键后Activity仍在内存", "静态变量/Handler持有引用", "使用弱引用,及时注销"),
        Problem("列表滑动内存持续增长", "onBindViewHolder创建对象", "对象提到ViewHolder级别"),
        Problem("内存曲线锯齿形", "onDraw/循环中创建对象", "对象提到方法外"),
        Problem("Bitmap加载后内存翻倍", "未采样直接加载大图", "使用inSampleSize采样"),
        Problem("Native内存持续增长", "Native层分配未释放", "使用RAII模式管理")
    )
}

data class Problem(val symptom: String, val cause: String, val solution: String)

面试加分点

  1. **hprof版本差异 **:HPROF格式有多个版本,Android使用的格式与标准JDK略有不同,需要Android Studio或Shark解析。
  2. **Shark vs HAHA **:Shark性能更高内存占用更小,核心改进包括增量解析和更好的泄漏检测算法。
  3. **Retained Size计算陷阱 **:存在循环引用时Retained Size可能不准确,某些实现使用近似算法。
  4. **Reference Queue妙用 **:不仅用于WeakReference回收通知,ThreadLocal、FinalReference等场景也有应用。

题目八:高级话题(onTrimMemory回调的level到底代表什么?)

核心回答

Android内存管理涉及多个层面:MADV_DONTNEED用于释放不用的内存页,Cooperative GC允许应用主动参与GC,内存限制由系统动态调整,onTrimMemory/onLowMemory是应用响应内存压力的关键回调。

原理与代码

MADV_DONTNEED与ART内存释放
markdown 复制代码
/** 
 * MADV_DONTNEED机制
 * 
 * Linux系统调用,madvise(MADV_DONTNEED)告诉内核"这块内存不用了"
 * 内核会立即释放这些页
 * 
 * ART使用:
 * - Background GC后调用madvise释放空闲页
 * - 这就是GC后内存立即下降的原因
 */
class ARTMemoryRelease {
    /**
     * ART何时释放内存:
     * 1. Background GC后
     * 2. System.gc()触发后
     * 3. 内存压力时
     * 4. Allocation Failure时
     */
}
Cooperative GC

kotlin

kotlin 复制代码
/** 
 * Cooperative GC:
 * - 允许应用主动触发GC
 * - System.gc()是协作式GC入口
 * - 但它只是"建议",不保证立即执行
 */
class CooperativeGCDemo {
    fun demonstrate() {
        val list = mutableListOf<ByteArray>()
        for (i in 0 until 100) {
            list.add(ByteArray(1024 * 1024))
        }
        list.clear()
        System.gc()  // 请求GC,但不保证立即执行
    }
    
    // 正确做法:依靠正常引用管理
    fun processData() {
        val largeData = ByteArray(10 * 1024 * 1024)
        // 处理...
        // 方法结束,对象超出作用域
    }
}
内存限制
kotlin 复制代码
class MemoryLimitDemo {
    fun getMemoryClass(): Int {
        val am = MyApplication.get().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        return am.memoryClass  // 默认64MB或128MB
    }
    
    fun getLargeMemoryClass(): Int {
        val am = MyApplication.get().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        return am.largeMemoryClass  // 需要android:largeHeap="true"
    }
    
    fun getMemoryInfo(): MemoryInfo {
        val am = MyApplication.get().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val info = ActivityManager.MemoryInfo()
        am.getMemoryInfo(info)
        return MemoryInfo(
            totalMemory = info.totalMem,
            availableMemory = info.availMem,
            isLowMemory = info.lowMemory,
            threshold = info.threshold
        )
    }
}

data class MemoryInfo(
    val totalMemory: Long,
    val availableMemory: Long,
    val isLowMemory: Boolean,
    val threshold: Long
)
onTrimMemory与onLowMemory
kotlin 复制代码
class TrimMemoryDemo : AppCompatActivity() {
    
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when (level) {
            // 运行中内存压力
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE -> clearModerateCaches()
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> clearMostCaches()
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                clearAllCaches()
                releaseSomeResources()
            }
            
            // 应用在后台
            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> releaseBackgroundResources()
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> releaseAllResources()
            
            // UI不可见
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> releaseUIResources()
        }
    }
    
    override fun onLowMemory() {
        super.onLowMemory()
        clearAllCaches()
        releaseAllResources()
    }
    
    private fun clearModerateCaches() { /* 清理不重要的缓存 */ }
    private fun clearMostCaches() { /* 清理更多缓存 */ }
    private fun clearAllCaches() { ImageLoader.clearMemoryCache() }
    private fun releaseSomeResources() { stopNonEssentialServices() }
    private fun releaseBackgroundResources() { /* 释放后台资源 */ }
    private fun releaseAllResources() { ImageLoader.clearAll(); stopAllServices() }
    private fun releaseUIResources() { releaseLargeBitmaps() }
    private fun stopNonEssentialServices() {}
    private fun stopAllServices() {}
    private fun releaseLargeBitmaps() {}
}
内存压力感知的图片缓存
kotlin 复制代码
class MemoryAwareImageCache(context: Context) {
    
    private val memoryCache: LruCache<String, Bitmap>
    
    init {
        val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
        memoryCache = object : LruCache<String, Bitmap>(maxMemory / 8) {
            override fun sizeOf(key: String, bitmap: Bitmap) = bitmap.allocationByteCount / 1024
        }
        
        context.applicationContext.registerComponentCallbacks(object : ComponentCallbacks2 {
            override fun onTrimMemory(level: Int) {
                when (level) {
                    ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE ->
                        memoryCache.trimToSize(memoryCache.maxSize() * 3 / 4)
                    ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
                    ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL ->
                        memoryCache.trimToSize(memoryCache.maxSize() / 2)
                    ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ->
                        memoryCache.trimToSize(memoryCache.maxSize() / 4)
                    ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
                    ComponentCallbacks2.TRIM_MEMORY_MODERATE,
                    ComponentCallbacks2.TRIM_MEMORY_COMPLETE ->
                        memoryCache.evictAll()
                }
            }
            
            override fun onConfigurationChanged(newConfig: Configuration) {}
            override fun onLowMemory() { memoryCache.evictAll() }
        })
    }
    
    fun get(key: String): Bitmap? = memoryCache.get(key)
    fun put(key: String, bitmap: Bitmap) { memoryCache.put(key, bitmap) }
    fun clear() { memoryCache.evictAll() }
}

Android实战场景

kotlin 复制代码
/**
 * 完整的内存压力响应策略
 */
class MemoryPressureStrategy {
    
    fun onTrimMemory(level: Int) {
        when (level) {
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> {
                stopNonEssentialServices()
            }
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                stopNonCoreServices()
            }
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                releaseUIServices()
            }
        }
    }
    
    private fun stopNonEssentialServices() { /* 停止分析、统计服务 */ }
    private fun stopNonCoreServices() { /* 只保留核心功能 */ }
    private fun releaseUIServices() { /* 释放UI相关服务 */ }
}

/** 
 * 内存监控
 */
class MemoryMonitor {
    private var lastGcTime = 0L
    private var gcCount = 0
    
    fun onGcStart() {
        val now = System.currentTimeMillis()
        if (now - lastGcTime < 5000) {
            gcCount++
            if (gcCount > 3) reportMemoryIssue()  // 5秒内超过3次GC
        } else {
            gcCount = 1
        }
        lastGcTime = now
    }
    
    private fun reportMemoryIssue() { /* 上报问题 */ }
}

面试加分点

  1. LMK机制:Android进程优先级分为前台、可见、服务、后台、空进程。LMK根据oom_adj值杀死进程,oom_adj越高越容易被杀。理解这个机制有助于设计合理的应用架构,避免被系统优先杀死。
  2. largeHeap的代价:申请largeHeap会让应用进入不同的LMK优先级组。实际上largeHeap应用在内存紧张时可能更容易被杀,因为它们占用的内存更多,对系统压力大。
  3. Memory Thrashing检测:如果GC非常频繁但内存释放很少,说明存在内存抖动。需要分析分配热点,减少短期对象的创建。
  4. Android 12+的内存建议API:Android 12引入了更细粒度的内存压力通知,AppHintCallback可以让应用更早地收到内存压力信号,有更多时间做出响应。
  5. 内存效率优化不只是减少使用:有时候使用更高效的算法(如缓存复用)反而会占用更多内存,但能提升性能。需要根据实际场景权衡。
  6. 不同设备的内存限制差异:低端设备可能只有96MB堆内存限制,高端设备可能是512MB。在开发时应该考虑最低支持的设备配置。
  7. MMKV等高性能存储的内存考虑:MMKV使用mmap,虽然存储在磁盘上,但访问时也会占用内存。大量数据存储时需要注意这个问题。
  8. 协程与内存:虽然协程本身很轻量,但协程中持有的对象引用可能会导致这些对象无法被GC。如果协程持有大量数据,需要特别注意。

额外话题:线上内存监控与异常检测

核心回答

线上内存监控是发现线上内存问题的关键手段。通过合理的指标采集和异常检测,可以在用户反馈之前发现问题。

原理与代码

线上内存监控指标
kotlin 复制代码
/**
 * 线上内存监控关键指标
 */
class MemoryMonitor {
    
    /** 
     * 关键监控指标
     */
    data class MemoryMetrics(
        val javaHeapUsed: Long,      // Java堆使用量
        val javaHeapMax: Long,       // Java堆最大值
        val nativeHeapUsed: Long,     // Native堆使用量
        val availableMemory: Long,   // 可用内存
        val lowMemoryFlag: Boolean,   // 是否低内存
        val gcCount: Int,             // GC次数
        val gcTime: Long,             // GC总耗时
        val threadCount: Int          // 线程数
    )
    
    /**
     * 采集当前内存指标
     */
    fun collectMetrics(): MemoryMetrics {
        val runtime = Runtime.getRuntime()
        val activityManager = MyApplication.get()
            .getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val memInfo = ActivityManager.MemoryInfo()
        activityManager.getMemoryInfo(memInfo)
        
        return MemoryMetrics(
            javaHeapUsed = runtime.totalMemory() - runtime.freeMemory(),
            javaHeapMax = runtime.maxMemory(),
            nativeHeapUsed = getNativeHeapUsed(),
            availableMemory = memInfo.availMem,
            lowMemoryFlag = memInfo.lowMemory,
            gcCount = Debug.getGlobalGcInstanceCount(),
            gcTime = Debug.getRuntimeStat("art.gc.gc-time").toLongOrNull() ?: 0,
            threadCount = Thread.getAllStackTraces().size
        )
    }
    
    private fun getNativeHeapUsed(): Long {
        val info = Debug.getNativeHeapInfo()
        return info?.let { it[0] } ?: 0L
    }
    
    /** 
     * 内存使用率
     */
    fun getMemoryUsageRatio(): Float {
        val runtime = Runtime.getRuntime()
        return (runtime.totalMemory() - runtime.freeMemory()).toFloat() / runtime.maxMemory()
    }
    
    /**
     * 检测内存是否接近上限
     */
    fun isMemoryPressure(): Boolean {
        return getMemoryUsageRatio() > 0.8f
    }
}

/** 
 * 内存泄漏检测(简化版)
 * 实际线上使用更复杂的算法
 */
class LeakDetector {
    
    private val snapshots = ArrayDeque<MemorySnapshot>()
    private val maxSnapshotCount = 5
    
    /**
     * 记录内存快照
     */
    fun recordSnapshot() {
        val runtime = Runtime.getRuntime()
        val snapshot = MemorySnapshot(
            timestamp = System.currentTimeMillis(),
            usedMemory = runtime.totalMemory() - runtime.freeMemory(),
            objectCount = estimateObjectCount()
        )
        
        snapshots.addLast(snapshot)
        if (snapshots.size > maxSnapshotCount) {
            snapshots.removeFirst()
        }
    }
    
    /** 
     * 检测是否存在内存泄漏
     */
    fun detectLeak(): LeakDetectionResult? {
        if (snapshots.size < 3) return null
        
        val snapshotsList = snapshots.toList()
        
        // 计算内存增长趋势
        val growthRate = calculateGrowthRate(snapshotsList)
        
        // 如果内存持续增长且GC后不回落,可能存在泄漏
        if (growthRate > 0.1f) {  // 增长率超过10%
            return LeakDetectionResult(
                growthRate = growthRate,
                estimatedLeakSize = estimateLeakSize(snapshotsList),
                suggestion = "内存持续增长,建议进行heap dump分析"
            )
        }
        
        return null
    }
    
    private fun calculateGrowthRate(snapshots: List<MemorySnapshot>): Float {
        if (snapshots.size < 2) return 0f
        
        val first = snapshots.first()
        val last = snapshots.last()
        
        return (last.usedMemory - first.usedMemory).toFloat() / first.usedMemory
    }
    
    private fun estimateLeakSize(snapshots: List<MemorySnapshot>): Long {
        val first = snapshots.first()
        val last = snapshots.last()
        val timeDiff = last.timestamp - first.timestamp
        
        // 估算单位时间泄漏量
        return if (timeDiff > 0) {
            (last.usedMemory - first.usedMemory) * 60 * 60 * 1000 / timeDiff
        } else 0L
    }
    
    private fun estimateObjectCount(): Int {
        // 简化估算,实际需要heap dump
        return Debug.getObjectCount()
    }
}

data class MemorySnapshot(
    val timestamp: Long,
    val usedMemory: Long,
    val objectCount: Int
)

data class LeakDetectionResult(
    val growthRate: Float,
    val estimatedLeakSize: Long,
    val suggestion: String
)
异常检测与上报
kotlin 复制代码
/**
 * 内存异常检测与上报
 */
class MemoryAbnormalDetector {
    
    /** 
     * 异常类型
     */
    enum class AbnormalType {
        HIGH_MEMORY_USAGE,      // 内存使用率高
        MEMORY_LEAK,            // 内存泄漏
        FREQUENT_GC,           // GC频繁
        OOM_NEAR               // 接近OOM
    }
    
    data class AbnormalEvent(
        val type: AbnormalType,
        val metrics: MemoryMonitor.MemoryMetrics,
        val detail: String,
        val timestamp: Long = System.currentTimeMillis()
    )
    
    private val listener: ((AbnormalEvent) -> Unit)? = null
    
    /**
     * 检测异常
     */
    fun detect(metrics: MemoryMonitor.MemoryMetrics): AbnormalEvent? {
        // 检测1:内存使用率过高
        val usageRatio = metrics.javaHeapUsed.toFloat() / metrics.javaHeapMax
        if (usageRatio > 0.85f) {
            return AbnormalEvent(
                AbnormalType.HIGH_MEMORY_USAGE,
                metrics,
                "Java堆使用率${(usageRatio * 100).toInt()}%,超过85%"
            )
        }
        
        // 检测2:接近OOM
        val remainingMemory = metrics.javaHeapMax - metrics.javaHeapUsed
        if (remainingMemory < 20 * 1024 * 1024) {  // 小于20MB
            return AbnormalEvent(
                AbnormalType.OOM_NEAR,
                metrics,
                "Java堆剩余${remainingMemory / 1024 / 1024}MB,随时可能OOM"
            )
        }
        
        // 检测3:GC过于频繁
        if (metrics.gcCount > 100) {
            return AbnormalEvent(
                AbnormalType.FREQUENT_GC,
                metrics,
                "GC次数${metrics.gcCount}次,可能存在内存抖动"
            )
        }
        
        return null
    }
    
    /** 
     * 定期检查(应在后台线程执行)
     */
    fun startMonitoring(intervalMs: Long = 5000) {
        CoroutineScope(Dispatchers.IO).launch {
            while (true) {
                val metrics = MemoryMonitor().collectMetrics()
                detect(metrics)?.let { event ->
                    listener?.invoke(event)
                    reportToServer(event)
                }
                delay(intervalMs)
            }
        }
    }
    
    private fun reportToServer(event: AbnormalEvent) {
        // 上报到监控系统
        // Firebase Performance、Crashlytics、自建监控等
    }
}

实战建议

python 复制代码
/**
 * 内存优化检查清单
 */
object MemoryOptimizationChecklist {
    
    /** 
     * 开发阶段检查项
     */
    val devChecklist = listOf(
        "是否使用了LeakCanary进行泄漏检测",
        "是否在onDestroy中注销了所有监听器和广播",
        "是否避免了静态变量持有Context",
        "Handler是否使用了静态内部类+弱引用",
        "大图片是否进行了采样压缩",
        "RecyclerView的onBindViewHolder是否创建了对象",
        "onDraw中是否创建了Paint/Path等对象"
    )
    
    /**
     * 上线前检查项
     */
    val releaseChecklist = listOf(
        "是否进行了内存压力测试",
        "是否在低端设备上测试了内存表现",
        "是否监控了内存指标",
        "Bitmap缓存是否设置了合理的上限",
        "是否实现了onTrimMemory回调"
    )
    
    /** 
     * 常见内存问题快速定位
     */
    fun quickDiagnose(symptom: String): String {
        return when {
            symptom.contains("OOM") -> """
                排查方向:
                1. 检查是否有大图未压缩
                2. 检查是否有内存泄漏
                3. 检查内存限制
                4. 使用heap dump分析大对象
            """.trimIndent()
            
            symptom.contains("卡顿") -> """
                排查方向:
                1. 检查是否有内存抖动
                2. 检查GC是否过于频繁
                3. 使用Allocation Tracker定位热点
            """.trimIndent()
            
            symptom.contains("泄漏") -> """
                排查方向:
                1. 使用LeakCanary定位泄漏点
                2. 检查静态变量和单例
                3. 检查Handler和监听器
                4. 分析heap dump引用链
            """.trimIndent()
            
            else -> "请描述具体症状"
        }
    }
}

总结

内存问题从来不是孤立存在的,它与GC机制、Bitmap管理、Native层、框架设计都密切相关。好的内存管理需要:

  1. 理解底层原理:知道GC是如何工作的,内存是如何分配的
  2. 养成良好习惯:避免泄漏,正确释放资源
  3. 善用工具:Memory Profiler、LeakCanary都是利器
  4. 响应系统信号:正确处理onTrimMemory,让应用与系统和谐相处
  5. 线上监控:在发布后持续监控内存指标,及时发现线上问题

记住:OOM只是症状,不是病因。找到病因,才能真正解决问题。

相关推荐
蝎子莱莱爱打怪4 小时前
👍🏻👍🏻6年381颗芯片+韬定律,华为重新定义半导体,为什么还有人喷???
后端·面试·程序员
JustNow_Man5 小时前
【opencode】安装使用daytona沙箱插件
android·java·javascript
渐儿6 小时前
RAG / GraphRAG / 向量检索 面试题(完整答案版)
面试
渐儿6 小时前
后端 Python / Node 面试题(完整答案版)
面试
ricardo19736 小时前
代码分割 + 路由懒加载 + 字体子集化:前端瘦身三板斧
前端·面试
YIN_尹6 小时前
【Linux 系统编程】手撕一个简易版的shell命令行解释器
android·linux·运维
黄林晴7 小时前
Android CLI 1.0 稳定版发布!官方为 AI Agent 打造专属验证工具,改完自动校验
android
氦客8 小时前
Android Compose 图层的合成 : BlendMode
android·compose·jetpack·layer·blendmode·graphics·图层的合成
I Promise348 小时前
多传感器融合&模型后处理C++工程师面试参考回答
开发语言·c++·面试