Android 内存泄漏分析思路和案例剖析,腾讯+字节+阿里面经真题汇总

39 阅读10分钟
} else {
    Bitmap screenShotBitmap = var14;
    Intent var4 = new Intent();
    int var6 = false;
    Bundle bundle = new Bundle();
    // 内部类创建位置:
    bundle.putBinder("screenShotBinder", (IBinder)(new BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1($this$startActivity2WithBitmap, screenShotBitmap)));
    var4.putExtra("question_screenshot_bitmap", bundle);
    Unit var9 = Unit.INSTANCE;
    $this$startActivity2WithBitmap.startActivity(var4);
    return Unit.INSTANCE;
}

}

// 这是kotlin compiler自动生成的一个普通类: public final class BitmapExtKtstartActivity2WithBitmap$$inlinedapplylambdalambda1 extends IBitmapInterface.Stub { // FF:syntheticfieldfinalActivityFF: synthetic field final Activity this_startActivity2WithBitmapinlined;//引用了activity//inlined; // 引用了activity // FF: synthetic field final Bitmap screenShotBitmapscreenShotBitmapinlined;

BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1(Activity var1, Bitmap var2) {
    this.$this_startActivity2WithBitmap$inlined = var1;
    this.$screenShotBitmap$inlined = var2;
}
@NotNull
public Bitmap getIntentBitmap() {
    return this.$screenShotBitmap$inlined;
}

}

Kotlin Compiler 编译生成的 Java 文件中,`IBitmapInterface` 匿名内部类被替换为普通类 `BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1`,并且这个普通类持有了 Activity出现这个情况的原因是,Kotlin 为了在该类的内部能正常使用方法内的变量,把方法的入参以及内部类代码以上创建的所有变量都写进了该类的成员变量中;因此 Activity 被该类引用;另外 Binder 本身生命周期长于 Activity,因此产生内存泄漏


解决方法是,直接声明一个普通类,即可绕过 Kotlin Compiler 的“优化”,移除 Activity 的引用



class BitmapBinder(private val bitmap: Bitmap): IBitmapInterface.Stub() { override fun getIntentBitmap( ) = bitmap }

// 使用: bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinder(screenShotBitmap))


接下来,问题是 BitmapBinder 会反复创建且无法回收的问题,内存现象如图,每次跳转再关闭,内存都会上涨一点,如同阶梯;GC 后无法释放;


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bebea59019164bbd963992d105141353~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=6nDRQ5gKJHkWaDJofyVrCfkEc7M%3D)


heap 中,通过 Bitmap 尺寸 `2560x1600, 320density` 可以推断,这些都是未能回收的截图 Bitmap 对象,被 Binder 持有;但查看 Binder 的引用链,却并没有发现任何被我们应用相关的引用;


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8c61c420c0754b88913281f1a787f429~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=EJub3JBwwYwcA2QLNFS1d%2Bb2TS8%3D)


我们推测 Binder 应该是被生命周期较长的 Native 层引用了,与 Binder 的实现有关,但没找到回收 Binder 的有效方法;


一种解决办法是,复用 Binder,确保每次打开 Activity2 时,Binder 不会重复创建;另外将 `BitmapBinder` 的 Bitmap 改为弱引用,这样即使 Binder 不能回收,Bitmap 也能被及时回收,毕竟 Bitmap 才是内存大户。



object BitmapBinderHolder { private var mBinder: BitmapBinder? = null // 保证全局只有一个BitmapBinder

fun of(bitmap: Bitmap): BitmapBinder {
    return mBinder ?: BitmapBinder(WeakReference(bitmap)).apply { mBinder = this }
}

}

class BitmapBinder(var bitmapRef: WeakReference?): IBitmapInterface.Stub() { override fun getIntentBitmap() = bitmapRef?.get() }

// 使用: bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinderHolder.of(screenShotBitmap))


验证:如内存图,一次 GC 后,创建的所有 Bitmap 都可以正常回收。


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a61e1a9837d44ca99107f9ab7ecfdf52~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=9mEvQo70meDTiQwBggZ%2F64RQxxw%3D)


##### 案例2:Flutter 多引擎场景 插件内存泄漏


有不少项目使用了多引擎方案实现 Flutter 混合开发,在 Flutter 页面关闭时,为避免内存泄漏,不但要将 `FlutterView``FlutterEngine``MessageChannel` 等相关组件及时解绑销毁,同时也需要关注各个 Flutter 插件是否有正常的释放操作。


例如在我们的一个多引擎项目中,通过反复打开关闭一个页面,发现了一个内存泄漏点:


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/966609dd5d2f40d7b2e7fc14aae11e2f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=ro3aCcvzBOkr4j1CDnr%2BcMh2JBk%3D)


这个activity是一个二级页面,使用多引擎方案,在上面跑了一个 `FlutterView` ;看样子是一个『单实例』的内存泄漏,即无论开关多少次,Activity 只会保留一个实例在heap中无法释放,常见的场景是全局静态变量的引用。这种内存泄漏对内存的影响比多实例泄漏略轻一点,但如果这个 Activity 体量很大,持有较多的 Fragment、View,这些相关组件一起泄漏的话,也是要着重优化的。


从引用链来看,这是 `FlutterEngine` 内的一个通信 Channel 引起的内存泄漏;当 `FlutterEngine` 被创建时,引擎内的每个插件会创建出自己的`MessageChannel`并注册到`FlutterEngine.dartExecutor.binaryMessenger`中,以便每个插件都能独立和 Native 通信。


例如一个普通插件的写法可能是这样:



class XXPlugin: FlutterPlugin { private val mChannel: BasicMessageChannel? = null

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { // 引擎创建时回调
    mChannel = BasicMessageChannel(flutterPluginBinding.flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME, JSONMessageCodec.INSTANCE)
    mChannel?.setMessageHandler { message, reply ->
        ...
    }
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { // 引擎销毁时回调
    mChannel?.setMessageHandler(null)
    mChannel = null
}

}


可以看到其实 `FlutterPlugin` 其实是会持有 `binaryMessenger` 的引用的,而 `binaryMessenger` 又会有 `FlutterJNI` 的引用… 这一系列引用链最终会使 `FlutterPlugin` 持有 `Context`,因此如果插件没有正确释放引用,就必然会出现内存泄漏。


我们看下上图引用链中 `loggerChannel` 的写法是怎么样的:



class LoggerPlugin: FlutterPlugin { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { LoggerChannelImpl.init(flutterPluginBinding.getFlutterEngine()) }

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
}

}

object LoggerChannelImpl { // 这是一个单例 private var loggerChannel: BasicMessageChannel?= null

fun init(flutterEngine: FlutterEngine) {
    loggerChannel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, LOGGER_CHANNEL, JSONMessageCodec.INSTANCE)
    loggerChannel?.setMessageHandler { messageJO, reply ->
        ...
    }
}

}

`LoggerPlugin.onAttachedToEngine` 中,将 `FlutterEngine` 传入到了单例 `LoggerChannelImpl` 里面,`binaryMessenger` 被单例持有,且 `onDetachedFromEngine` 方法未做销毁操作,因此一直被单例引用,context无法释放。


这个插件可能在设计时,没有考虑到多引擎的场景;单引擎时,插件的 `onAttachedToEngine``onDetachedFromEngine` 相当于是跟着应用的生命周期走的,因此不会出现内存泄漏;但在多引擎场景下,`DartVM` 会为每个引擎分配 isolate,和进程有些类似;isolate 的 dart 堆内存是完全独立的,因此引擎之间任何对象(包括静态对象)都不互通;因此 `FlutterEngine` 会在自己的 isolate 中创建各自的 `FlutterPlugin` 实例,这使得每次创建引擎,插件的生命周期都会重走一遍。当销毁一个引擎时,插件没有正常回收,没有及时释放 `Context``FlutterEngine` 的相关引用,就会出现内存泄漏。


修改方案:


1. `LoggerChannelImpl` 无需使用单例写法,替换为普通类即可,确保每个引擎的 `MessageChannel`都是独立的;
2. `LoggerPlugin.onDetachedFromEngine` 需要对 `MessageChannel` 做销毁和置空操作;


##### 案例3:三方库 Native 引用 内存泄漏


项目中接入了一个三方阅读器 SDK,在一次内存分析时,发现每次打开该阅读器,内存便会上升一截并且无法下降;从 heap dump 文件看,Profiler 并未指出项目中存在内存泄漏,但可以看到 app heap 中有一个 Activity 未能回收的实例个数非常多,且内存占用较大。


查看 GCRoot References,发现这些 Activity 没有被任何已知的 GCRoot 引用:


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3d84780b687148c3859c7ae918d7edce~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=SGavCF0MWSnJYydvnaI0DcKxR5Y%3D)


毫无疑问这个 Activity 是存在内存泄漏的,因为操作的时候已经把相关页面都 finish 掉并且手动 GC,因此原因只能是 Activity 被某个不可见的 GCRoot 引用了。


事实上,Profiler 的 Heap Dump 只会显示 Java 堆内存的 GCRoot,而在 Native 堆中的 GCRoot 并不会显示到这个引用列表中。所以,有没有可能是这个Activity被 Native 对象持有了?


我们用动态分析工具 `Allocations Record` 看一下 Java 类在 Native 堆的引用,果然发现了这个 Activity 的一些引用链:


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fce485fff1aa4620a585909655c8147f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=adVRSbqcE9Dli3aovTMMLB0f2gg%3D)


但可惜引用链都是一些内存地址,没有显示类名,没法知道是何处引用到了 Activity;后面用 LeakCanary 试了一下,虽然也明确说明了是 Native 层 `Global Variable` 的引用造成的内存泄漏,但还是没有提供具体的调用位置;


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cb8a2e4c232d45e594ee7f7d87eaf80c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=NmO2rj8QivS4PC8tvCPCP8Pl5JY%3D)


我们只好回到源码去分析下可能的调用处了。这个是 `DownloadActivity` 是我们为了适配阅读器SDK做的一个书籍下载的页面;当本地没有图书时,会先下载书籍文件,随后传入 SDK 中,打开 SDK 自己的 Activity;因此,`DownloadActivity` 的功能就是下载、校验、解压书籍,并处理 SDK 阅读器的一些启动流程。


按常规思路,先检查下载、校验、解压的代码,都没有发现疑点,listener 之类的都做了弱引用封装;因此推测是 SDK 自身的写法导致的内存泄漏。


发现阅读器 SDK 启动时,有一个 context 入参:



class DownloadActivity { ... private fun openBook() { ... ReaderApi.getInstance().startReader(this, bookInfo) } }


由于这个 SDK 的源码都是混淆过的,只能硬啃了,从 `startReader` 方法点进去一路跟踪调用链:



class ReaderApi: void startReader(Activity context, BookInfo bookInfo) ↓ class AppExecutor: void a(Runnable var1) ↓ class ReaderUtils: static void a(Activity var0, BookViewerCallback var1, Bundle var2) ↓ class BookViewer: static void a(Context var0, AssetManager var1) ↓ class NativeCpp: static native void initJNI(Context var0, AssetManager var1);


最后到了 `NativeCpp` 这个类的 `initJNI` 方法,可以看到这个本地方法把我们的 Activity 传进去了,后续处理不得而知,但基于上面的内存分析我们基本可以断定,正是由于这个方法,Activity 的引用被 Native 的长生命周期对象持有,导致 Activity 出现内存泄漏。


至于为什么 Native 需要用到 context 则没法分析了,我们只能将这个问题反馈给 SDK 供应商,让他们做进一步处理。解决办法也不难:


1. 在销毁阅读器时及时置空 Activity 引用;
2. `startReader` 方法不需要指定 Activity 对象,入参声明改为 Context 即可,外部就可以将 `Application Context` 传进去。


**为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的核心笔记(还该底层逻辑):[`https://qr18.cn/FVlo89`]( )**


#### 性能优化核心笔记:[`https://qr18.cn/FVlo89`]( )
### 最后

简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

选定你想去的几家公司后,先去一些小的公司练练,学习下面试技巧,总结下,也算是熟悉下面试氛围,平时和同事或者产品PK时可以讲得头头是道,思路清晰至极,到了现场真的不一样,怎么描述你所做的一切,这绝对是个学术性问题!

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!**在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。**

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/529198cfc28d45b7b835a248cc0df0f2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771890494&x-signature=nKHp5AOGtQw%2BErprGgZKIhietfU%3D)

**本文在开源项目:【[GitHub](https://github.com/a120464/Android-P7/blob/master/Android%E5%BC%80%E5%8F%91%E4%B8%8D%E4%BC%9A%E8%BF%99%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E9%9D%A2%E8%AF%95%E6%8B%BF%E9%AB%98%E8%96%AA%EF%BC%81.md) 】中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…**