1. 内存泄漏基础:什么是内存泄漏
说实话,我做了这么多年移动端开发,最头疼的问题之一就是内存泄漏。你想想看,一个App跑着跑着突然卡顿、闪退,用户直接就给差评了。而混合开发里,这个问题更隐蔽、更难抓。
今天咱们就来聊聊内存泄漏到底是个什么东西。
1.1 什么是内存泄漏
内存泄漏,说白了就是「该回收的内存没回收」。
我习惯用一个比喻来解释:你租了一间仓库,用完东西后应该把仓库还回去。但如果你一直占着仓库不还,仓库管理员就没法把它租给别人。久而久之,仓库就不够用了。
程序里也一样。对象创建后占用一块内存,用完后应该被垃圾回收器(GC)回收。但如果有某个引用一直拽着它不放,GC就认为「这个对象还有人用」,于是就不回收。这就是内存泄漏。
核心定义:不再需要的对象,因为被意外引用而无法被GC回收,导致内存占用持续增长。
1.2 内存泄漏的常见表现
我在项目中遇到过好几次这样的情况:测试同事跑过来跟我说「页面越用越卡,滑动都掉帧了」。我一看内存监控,好家伙,内存占用曲线一路往上走,根本停不下来。
内存泄漏的表现其实挺明显的,我总结了几条:
- 内存只增不减:打开一个页面,内存涨了;关闭页面,内存没降。反复操作几次,内存就爆了。
- 操作越来越卡:GC频繁触发,导致UI线程被频繁打断,用户能明显感觉到卡顿。
- OOM崩溃:内存撑爆了,系统直接杀掉进程。这种崩溃在低端机上尤其常见。
- App被系统回收:切到后台再切回来,发现App重启了。这是因为你的App占内存太多,系统优先杀你。
嗯,这里要注意:不是所有内存增长都是泄漏。有些缓存、图片加载也会让内存暂时升高,但用完应该能降下来。如果只升不降,那基本就是泄漏了。
1.3 为什么混合开发更容易泄漏
这个问题我思考了很久。混合开发(比如React Native、Flutter、小程序、Hybrid App)之所以容易出内存泄漏,核心原因就一个:多了一层「桥」。
你想想看,原生代码和JS代码之间通信,需要经过一个桥梁。这个桥梁如果没处理好,两边互相引用,GC就傻眼了。
我画了一张图,帮你理解这个结构:
你看这张图,泄漏点主要出现在两个地方:
- 原生层和JS层互相持有引用:比如原生Activity持有了WebView,WebView里的JS又通过addJavascriptInterface持有了Activity的引用。两边都不放手,GC就没办法回收。
- 桥接层的回调没清理:JS发起一个异步请求,注册了一个回调。页面关闭了,但回调还在。等请求回来,回调执行时发现页面已经没了,但引用还在,泄漏了。
我遇到过最典型的案例:一个React Native项目,页面里注册了DeviceEventEmitter监听。页面销毁时忘了移除监听,结果每次打开页面都多注册一个。用户操作几次后,内存直接飙到200MB+。
我的经验:混合开发里,90%的内存泄漏都跟「生命周期不匹配」有关。原生组件的生命周期和JS组件的生命周期不是一一对应的,你必须在合适的时机做清理。
1.4 混合开发特有的泄漏场景
除了上面说的,还有几个场景是混合开发特有的:
| 场景 | 原因 | 后果 |
|---|---|---|
| WebView泄漏 | WebView持有Activity引用,Activity销毁时WebView未释放 | Activity无法被GC回收,每次打开都新增一个 |
| JSBridge回调泄漏 | JS注册的回调函数被原生持有,页面关闭后未移除 | 回调函数及其闭包中的对象无法回收 |
| 定时器未清除 | JS中setInterval/setTimeout在页面卸载后仍在执行 | 定时器回调引用的DOM或组件无法释放 |
| 全局事件监听 | 在JS层注册了全局事件(如resize、scroll),未在销毁时移除 | 监听器持有组件引用,导致整个组件树泄漏 |
| 图片/大对象缓存 | 混合框架的图片缓存策略不当,导致内存持续增长 | 低端机频繁OOM |
警告:WebView泄漏是混合开发里最致命的。Android 4.4以上虽然修复了部分问题,但如果你在XML中直接声明WebView,或者没有在onDestroy中主动removeAllViews(),泄漏依然会发生。我曾经在一个项目里排查了三天,最后发现是WebView的父容器没有及时释放。
1.5 怎么判断是不是内存泄漏
我一般用三个方法快速判断:
- 看内存曲线:反复进入退出同一个页面,如果内存只升不降,基本就是泄漏了。
- 用工具抓堆转储:Android Studio的Memory Profiler、Xcode的Instruments都能抓。抓两次对比,看哪些对象数量在增长。
- 手动触发GC:在页面退出后手动调用System.gc(),然后看内存有没有降下来。降了说明没问题,没降说明有泄漏。
嗯,这里要提醒一下:手动GC只是辅助判断,别在生产环境里用。我见过有人把System.gc()写在业务代码里,结果性能反而更差了。
好了,这一章我们搞清楚了内存泄漏是什么、有什么表现、为什么混合开发更容易踩坑。下一章我会带你看看具体的排查工具和实战技巧,到时候咱们拿真实案例来练手。
公众号:蓝海资料掘金营,微信deep3321