3. 内存泄漏检测与修复:AMS托管对象引用链、弱引用机制、dlopen泄漏

3.1 原理:AMS/WMS中的引用链与泄漏根源

在Android Framework层,内存泄漏多由长生命周期对象(如ActivityManagerService、WindowManagerService)持有短生命周期对象(如Activity、View)的强引用所致。AMS通过ActivityRecord、ProcessRecord等托管对象维护应用状态,WMS通过WindowState、Session管理窗口层级。一旦这些托管对象的引用链未在预期时机清除,GC便无法回收目标对象,造成内存泄漏。

引用链视角:从GC Roots(如AMS、WMS自身)出发,遍历可达对象图。若对象A被AMS强引用,A又持有Activity的强引用,Activity即便已finish也无法回收。常见泄漏模式包括:
  • 内部类(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托管对象泄漏检测与修复步骤

  1. 确认泄漏对象:使用MAT(Memory Analyzer Tool)或Android Studio Profiler在内存快照中查找Activity/View实例数过多的类。
  2. 分析引用链:查看GC Roots到泄漏对象的最短强引用路径,识别出AMS或WMS中的持有对象。
  3. 判断引用类型:若持有方是Framework内部类(如mHandler、mBroadcastQueue),考虑将强引用替换为WeakReference,或手动清空引用(在onDestroy/remove操作中置null)。
  4. 修复AMS端:针对ActivityManagerService,检查ActivityRecord、BroadcastRecord等对象的强引用是否在Activity destroy后仍存在。常见修复是在activityDestroyed()中清空mHandler的回调。
  5. 修复WMS端:针对WindowManagerService,检查WindowState、Session的强引用是否在窗口移除后仍保留。例如,removeWindowLocked()后需将对应mTouchedWindow引用置null或改用WeakReference。
  6. 验证:使用hprof快照对比修复前后,确认泄漏对象实例数下降,且GC后内存回收正常。

3.4 dlopen泄漏:Native层的特殊内存泄漏

dlopen泄漏指通过dlopen()动态加载Native库(.so)后,未调用dlclose()完全卸载,导致库的代码段、数据段、bss段持续占用内存。在Framework中,JNI层加载的库若未合理管理引用计数,会造成泄漏。此外,部分系统服务(如SurfaceFlinger)的Native组件也可能因dlopen后未调用dlclose产生泄漏。

dlopen泄漏检测命令:
# 查看当前进程已加载的共享库及其引用计数
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个则视为异常)。
AMS/WMS 内存泄漏检测与弱引用机制流程 AMS/WMS 托管对象引用链及泄漏检测流程 GC Roots (AMS/WMS) 强引用 mActivityRecord
mWindowState 强引用 Activity / View WeakReference<T> 弱引用 可回收对象 dlopen() 加载.so 未dlclose .so 映射泄漏 MAT / Memory Profiler /proc/<PID>/maps lsof / strace 修复:将强引用改为弱引用、onDestroy中清空引用、确保dlopen/dlclose成对调用