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++
}
}
}
面试加分点
- Zygote预加载机制:Android启动时,Zygote进程会预加载常用Java类和资源到共享内存,所有应用进程fork继承。理解这个机制有助于理解应用启动时的内存开销。
- Read-Only共享区域:ART将预加载的类信息放在只读共享区域,对象实际分配在私有读写区域。这解释了为什么同一个类的不同实例会共享元数据。
- 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() { /* 注销监听、取消订阅 */ }
}
面试加分点
- finalize方法的延迟:对象即使不可达,如果finalize方法耗时较长或等待队列,会延迟回收。
- onTerminate不靠谱:Activity.onTerminate()在真实设备上几乎不会调用,清理逻辑应在onDestroy或onPause中执行。
- Kotlin协程的Structured Concurrency:Kotlin 1.3+的协程使用结构化并发,协程作用域被取消时所有子协程自动取消。
- 第三方库也需关注:某些第三方库自身可能存在泄漏,使用时也要关注其内存行为。
题目三: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堆泄漏:
- Native内存不在Java堆中:Native内存直接由系统分配,不经过JVM,无法用WeakReference追踪。
- 没有JVM元数据:Native对象没有对象头和引用信息。
- 生命周期不确定:Native代码可能持有对象指针,但JVM无法感知。
kotlin
// 实际项目中的LeakCanary配置
class LeakCanaryConfig {
fun createConfig() = LeakCanary.config.copy(
maxStoredHeapDumps = 7,
heapDumpRetentionSeconds = 5 * 60,
automaticAnalysis = true,
// 排除已知泄漏
excludedRefs = AndroidExcludedRefs.createAppDefaults().build()
)
}
面试加分点
- Shark引擎优势:相比老版本HAHA,Shark使用更高效的hprof解析算法,内存占用更小,分析速度更快。Shark直接读取hprof二进制格式,不需要将整个文件加载到内存。
- LeakCanary 2.0改进 :从反射手动dump变为使用
Debug.dumpHprofData()官方API,从HAHA解析变为Shark,更稳定更快。 - Android 10+隐私变化:heap dump需要应用持有READ_LOGS权限或使用官方API,LeakCanary已适配。
- 可自定义分析规则: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)
}
}
}
面试加分点
- GPU纹理限制:GPU对纹理有最大尺寸限制(通常2048x2048或4096x4096),超出限制的Bitmap无法直接用于硬件加速渲染。
- inBitmap参数限制:从Android 4.4开始可复用已有Bitmap,但必须大小相等(或更小)且像素格式兼容。
- WebP格式优势:相比PNG/JPEG,WebP文件更小(通常减少25-35%),解码后内存占用相同。
- 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
}
}
}
面试加分点
- Kotlin的inline :标准库函数如
repeat、let大部分是inline的,不创建额外对象。自定义高阶函数要加inline修饰符才能享受优化。 - ArrayMap vs HashMap:小数据量(<1000)ArrayMap更省内存,大数据量HashMap的O(1)查找更有优势。
- 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)
面试加分点
- **Android 8.0的GraphicBuffer **:使用ashmem共享内存减少Native泄漏可能性,但老Native代码仍可能有问题。
- **ASan vs Valgrind **:Valgrind开销10-50x,ASan开销约2x。ASan更适合集成测试。
- **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)
面试加分点
- **hprof版本差异 **:HPROF格式有多个版本,Android使用的格式与标准JDK略有不同,需要Android Studio或Shark解析。
- **Shark vs HAHA **:Shark性能更高内存占用更小,核心改进包括增量解析和更好的泄漏检测算法。
- **Retained Size计算陷阱 **:存在循环引用时Retained Size可能不准确,某些实现使用近似算法。
- **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() { /* 上报问题 */ }
}
面试加分点
- LMK机制:Android进程优先级分为前台、可见、服务、后台、空进程。LMK根据oom_adj值杀死进程,oom_adj越高越容易被杀。理解这个机制有助于设计合理的应用架构,避免被系统优先杀死。
- largeHeap的代价:申请largeHeap会让应用进入不同的LMK优先级组。实际上largeHeap应用在内存紧张时可能更容易被杀,因为它们占用的内存更多,对系统压力大。
- Memory Thrashing检测:如果GC非常频繁但内存释放很少,说明存在内存抖动。需要分析分配热点,减少短期对象的创建。
- Android 12+的内存建议API:Android 12引入了更细粒度的内存压力通知,AppHintCallback可以让应用更早地收到内存压力信号,有更多时间做出响应。
- 内存效率优化不只是减少使用:有时候使用更高效的算法(如缓存复用)反而会占用更多内存,但能提升性能。需要根据实际场景权衡。
- 不同设备的内存限制差异:低端设备可能只有96MB堆内存限制,高端设备可能是512MB。在开发时应该考虑最低支持的设备配置。
- MMKV等高性能存储的内存考虑:MMKV使用mmap,虽然存储在磁盘上,但访问时也会占用内存。大量数据存储时需要注意这个问题。
- 协程与内存:虽然协程本身很轻量,但协程中持有的对象引用可能会导致这些对象无法被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层、框架设计都密切相关。好的内存管理需要:
- 理解底层原理:知道GC是如何工作的,内存是如何分配的
- 养成良好习惯:避免泄漏,正确释放资源
- 善用工具:Memory Profiler、LeakCanary都是利器
- 响应系统信号:正确处理onTrimMemory,让应用与系统和谐相处
- 线上监控:在发布后持续监控内存指标,及时发现线上问题
记住:OOM只是症状,不是病因。找到病因,才能真正解决问题。