3. 内存泄漏检测与修复:AMS托管对象引用链、弱引用机制、dlopen泄漏
3.1 原理:AMS/WMS中的引用链与泄漏根源
在Android Framework层,内存泄漏多由长生命周期对象(如ActivityManagerService、WindowManagerService)持有短生命周期对象(如Activity、View)的强引用所致。AMS通过ActivityRecord、ProcessRecord等托管对象维护应用状态,WMS通过WindowState、Session管理窗口层级。一旦这些托管对象的引用链未在预期时机清除,GC便无法回收目标对象,造成内存泄漏。
- 内部类(Handler、Runnable)隐式持有外部Activity引用。
- 静态变量持有Context或View。
- 注册的Listener/Callback未及时注销。
- Binder代理对象(如
IApplicationThread)未释放。
3.2 关键机制:弱引用(WeakReference)在Framework中的使用
弱引用是打破长生命对象与短生命对象强引用链的标准手段。在AMS/WMS中,WeakReference被广泛用于指向Activity、Context、View等短生命周期对象。例如,ActivityManagerService中关于ActivityRecord与ActivityThread的关联,有时采用弱引用来避免Activity因AMS持有而无法销毁。
| 引用类型 | GC回收时机 | Framework典型场景 |
|---|---|---|
| 强引用(Strong Reference) | 永不回收(除非引用链断开) | AMS持有ProcessRecord、WMS持有DisplayContent |
| 软引用(Soft Reference) | 内存不足时回收 | Cache实现(较少用于核心托付对象) |
| 弱引用(Weak Reference) | 下一次GC时回收 | ActivityThread内部mActivities中ActivityRecord的引用、ViewRootImpl中的AttachInfo |
| 虚引用(Phantom Reference) | 对象被回收后触发 | DirectByteBuffer回收通知(非Framework主流) |
在自定义AMS/WMS调试或扩展时,应优先将回调、监听器定义为WeakReference<T>,以避免意外延长目标对象生命周期。例如,若WMS中需要保存某个View的引用,应使用WeakReference<View>,而不是直接持有View强引用。
// 在WMS的某个监听器注册处
private WeakReference<WindowState> mTouchedWindow;
public void setTouchedWindow(WindowState window) {
mTouchedWindow = new WeakReference<>(window);
}
public WindowState getTouchedWindow() {
return mTouchedWindow != null ? mTouchedWindow.get() : null;
}
3.3 实现流程:AMS托管对象泄漏检测与修复步骤
- 确认泄漏对象:使用MAT(Memory Analyzer Tool)或Android Studio Profiler在内存快照中查找Activity/View实例数过多的类。
- 分析引用链:查看GC Roots到泄漏对象的最短强引用路径,识别出AMS或WMS中的持有对象。
- 判断引用类型:若持有方是Framework内部类(如
mHandler、mBroadcastQueue),考虑将强引用替换为WeakReference,或手动清空引用(在onDestroy/remove操作中置null)。 - 修复AMS端:针对
ActivityManagerService,检查ActivityRecord、BroadcastRecord等对象的强引用是否在Activity destroy后仍存在。常见修复是在activityDestroyed()中清空mHandler的回调。 - 修复WMS端:针对
WindowManagerService,检查WindowState、Session的强引用是否在窗口移除后仍保留。例如,removeWindowLocked()后需将对应mTouchedWindow引用置null或改用WeakReference。 - 验证:使用hprof快照对比修复前后,确认泄漏对象实例数下降,且GC后内存回收正常。
3.4 dlopen泄漏:Native层的特殊内存泄漏
dlopen泄漏指通过dlopen()动态加载Native库(.so)后,未调用dlclose()完全卸载,导致库的代码段、数据段、bss段持续占用内存。在Framework中,JNI层加载的库若未合理管理引用计数,会造成泄漏。此外,部分系统服务(如SurfaceFlinger)的Native组件也可能因dlopen后未调用dlclose产生泄漏。
# 查看当前进程已加载的共享库及其引用计数
cat /proc/<PID>/maps | grep .so | wc -l
# 使用dumpsys meminfo查看native heap
adb shell dumpsys meminfo <PID> | grep "Native Heap"
# 使用lsof查看.so文件句柄
adb shell lsof -p <PID> | grep .so
在AMS/WMS上下文中,若某个系统服务通过System.loadLibrary()加载自定义库,且未在合适时机dlclose()(或未释放JNI全局引用),会导致库映射区域无法回收。修复方法为:在onTerminate()或服务销毁回调中显式调用dlclose(),并确保所有JNI全局引用先释放。
3.5 调试工具与常用命令
- Android Studio Memory Profiler:实时监控Java heap,记录分配点,导出hprof快照。
- MAT(Memory Analyzer Tool):导入hprof后使用“Histogram”查看对象数,右键“Path to GC Roots”找出泄漏引用链。
- /proc/maps与lsof:用于检测dlopen泄漏,查看进程加载的.so映射数量是否持续增长。
- adb shell dumpsys meminfo:输出Native Heap、Dalvik Heap、.so mmap等内存区域占用。
- strace:跟踪dlopen/dlclose调用,确认是否成对出现。命令示例:
adb shell strace -p <PID> -e trace=dlopen,dlclose。
3.6 常见错误与优化策略
| 错误类别 | 具体表现 | 修复建议 |
|---|---|---|
| Handler泄漏 | 内部Handler类隐式持有Activity,导致Activity无法GC | 将Handler定义为静态内部类,并使用WeakReference<Activity> |
| BroadcastReceiver未注销 | AMS持有Receiver的强引用,导致Activity泄漏 | 在Activity.onPause/onDestroy中调用unregisterReceiver() |
| WMS中View引用未清空 | 窗口移除后仍持有View强引用 | 改为WeakReference<View>,或removeWindow后置null |
| dlopen次数与dlclose不匹配 | .so映射数量持续增长,Native heap占用上升 | 确保每个dlopen对应一个dlclose,可使用RAII封装 |
| JNI全局引用泄漏 | JNI层创建的全局引用未删除,导致Java对象无法GC | 使用DeleteGlobalRef()清理,或采用Scoped局部引用 |
3.7 性能与稳定性优化建议
- 对所有AMS/WMS中回调、监听器、内部类引用,默认使用WeakReference包装。
- 在AMS的
activityDestroyed()或cleanUpActivityRecord()中,主动清空所有指向Activity的强引用。 - 使用
LeakCanary或Android Memory Profiler定期对系统服务进行泄漏检测。 - 对Native代码层,封装
SoRefCount类,确保dlopen与dlclose成对调用,并在进程退出时finalize中调用dlclose兜底。 - 监控
/proc/<PID>/maps中.so条目数量,设置自定义报警阈值(如连续超出200个则视为异常)。