一、引起内存泄露的原因
1.1 内存泄露的原因
内存泄露指的是程序在申请内存之后,没有办法释放掉已经申请到内存,它始终占用着内存,即被分配 的对象可达但无用。在 Android 中内存泄漏的原因大多是由于生命周期较⻓的对象持有生命周期较短的 对象的引用。
1.2 哪几种对象可以作为GC root
我们知道在Java虚拟机中判断一个对象是否可以被回收,有一种做法叫可达性分析算法,如果GC Root到 某个对象还有可达的引用链,那么这个对象就不能被回收。即垃圾回收器不会回收GC Roots以及那些被 它们间接引用的对象。对象可达性分析需要依赖GC Root。JVM中可以用来作为可以作为GC Root的对象 如下:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
我们要特别注意这些GC root 对象的使用,以免造成内存泄露。
1.3 可视化工具LeakCanary 的集成
dependencies { // debugImplementation because LeakCanary should only run in debug builds. debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6' }
二、LeakCanary的核心原理SDK的初始化是在自定义的ContentProvider AppWatcherInstaller 的onCreate方法中进行的。源码分
析的入口在AppWatcherInstaller的onCreate()方法。
ContentProvider 的 onCreate() 的调用是介于 Application 的 attachBaseContext(Context) 和 onCreate() 之间所调用的,而Application 的 attachBaseContext(Context) 方法被调用这就意味 着 Application 的 Context 被初始化了,而 ContentProvider 拿到的 Context 也正就是Application,所以可以在 ContentProvider 的 onCreate() 方法中完成相应的初始化操作。
android:name="leakcanary.internal.AppWatcherInstaller$MainProcess" android:authorities="${applicationId}.leakcanary-installer" android:enabled="@bool/leak_canary_watcher_auto_install" android:exported="false" />
2.1 内存泄露的监控
2.1.1 Leakcanary 默认自动监控的对象有
- destroyed Activity instances
- destroyed Fragment instances
- destroyed fragment View instances
- cleared ViewModel instances
- RootViews removed from the windowmanager
- destroyed services instances
Destroyed Activity 的监控
class ActivityWatcher( private val application: Application, private val reachabilityWatcher: ReachabilityWatcher ) : InstallableWatcher { private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() { // Activity执行onDestroy时, 触发对Activity的监听. override fun onActivityDestroyed(activity: Activity) { reachabilityWatcher.expectWeaklyReachable( activity, "${activity::class.java.name} received Activity#onDestroy()callback") } } override fun install() { application.registerActivityLifecycleCallbacks(lifecycleCallbacks) } override fun uninstall() { application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks) } }
Fragment 对象的监听是通过activity拿到fragmentManager,注册fragment的生命周期监听,在 onFragmentViewDestroyed触发fragment view 的监听,在onFragmentDestroyed中触发触发 fragment的监听。
override fun invoke(activity: Activity) { val fragmentManager = activity.fragmentManager fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true) }
2.1.2 LeakCanary 内存泄露的监控流程WeakReference+ReferenceQueue机制, 执行GC(GcTrigger.Default#runGc, 实际执行
Runtime.getRuntime().gc())后, 如果有对象不在ReferenceQueue中, 就认为发生了泄漏。
2.2 内存镜像的采集
发生内存泄漏时调用的onObjectRetained() 最终会调用到HeapDumpTrigger的checkRetainedObjects 方法,核心代码如下:
....
var retainedReferenceCount = objectWatcher.retainedObjectCount
// 1.泄露对象的数量是否大于0if (retainedReferenceCount > 0) {
// 若大于0,执行 GC 操作,再次确认泄露与否gcTrigger.runGc()retainedReferenceCount = objectWatcher.retainedObjectCount
}
// 2.如果泄露的对象数量少于设置的内存镜像采集的阈值(默认是5个),则不进行采集
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
// 3.如果两次内存镜像采集的时间间隔少于WAIT_BETWEEN_HEAP_DUMPS_MILLIS(60秒),则不 进行采集
val now = SystemClock.uptimeMillis()val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillisif (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) { onRetainInstanceListener.onEvent(DumpHappenedRecently) showRetainedCountNotification( objectCount = retainedReferenceCount, contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait) ) scheduleRetainedObjectCheck( delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis ) return } dismissRetainedCountNotification()val visibility = if (applicationVisible) "visible" else "not visible"
// 4.内存镜像采集dumpHeap(retainedReferenceCount = retainedReferenceCount, retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility" )
...
dumpHeap() 方法最终调用到Android 系统提供的 Debug.dumpHprofData(heapDumpFile.absolutePath) 方法,将内存中的数据按照hprof的二进制协议 保存在磁盘中。进而获取到内存泄露时的内存映射文件。
!!! 需要注意的是dumpHprofData方法会冻结当前的APP,因为执行hprof.Dump()前,系统会通过 ScopedSuspendAll执行了暂停所有java线程的操作,保证dump过程中内存数据的不变性,dump结束 后通过resumeAll恢复虚拟机的执行。
2.3 hprof文件的解析
获取到内存映射文件heapDumpFile后,会开启HeapAnalyzerService服务(继承于IntentService),在 子线程中通过 shark 插件 完成heapDumpFile文件的解析,找到内存泄露的引用链。核心代码如下:
...
private fun FindLeakInput.analyzeGraph( metadataExtractor: MetadataExtractor, leakingObjectFinder: LeakingObjectFinder, heapDumpFile: File, analysisStartNanoTime: Long
): HeapAnalysisSuccess {
listener.onAnalysisProgress(EXTRACTING_METADATA)val metadata = metadataExtractor.extractMetadata(graph)
val retainedClearedWeakRefCount = KeyedWeakReferenceFinder.findKeyedWeakReferences(graph).filter { it.isRetained && !it.hasReferent }.count()
// This should rarely happens, as we generally remove all cleared weak refs right before a heap dump.
val metadataWithCount = if (retainedClearedWeakRefCount > 0){ metadata + ("Count of retained yet cleared" to"$retainedClearedWeakRefCount KeyedWeakReference instances") }
else { metadata}
listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS) // 1.根据对象关系图,找到泄露的对象id.(在graph中存储有所有的gcRoots等) val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
// 2.找到是应用的泄露还是三方SDK的泄露
val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds)
return HeapAnalysisSuccess(
heapDumpFile = heapDumpFile,createdAtTimeMillis = System.currentTimeMillis(), analysisDurationMillis = since(analysisStartNanoTime), metadata = metadataWithCount,applicationLeaks = applicationLeaks,libraryLeaks = libraryLeaks,unreachableObjects = unreachableObjects
) }
...
...
//
private fun FindLeakInput.findLeaks(leakingObjectIds: SetLong>): LeaksAndUnreachableObjects {
val pathFinder = PathFinder(graph, listener, referenceMatchers) val pathFindingResults =
pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)
val unreachableObjects = findUnreachableObjects(pathFindingResults, leakingObjectIds)
// 3.查找最短路径val shortestPaths =
deduplicateShortestPaths(pathFindingResults.pathsToLeakingObjects)
val inspectedObjectsByPath = inspectObjects(shortestPaths)
// 4.计算泄露对象占用的内存val retainedSizes =
if (pathFindingResults.dominatorTree != null) {
computeRetainedSizes(inspectedObjectsByPath,pathFindingResults.dominatorTree)
}else {null}
// 5.构建调用栈信息val (applicationLeaks, libraryLeaks) = buildLeakTraces(shortestPaths, inspectedObjectsByPath, retainedSizes )
return LeaksAndUnreachableObjects(applicationLeaks, libraryLeaks, unreachableObjects)
}
...
三、 内存泄露的常⻅场景和解决方案
1.集合类
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局 性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的 删除机制,很可能导致集合所占用的内存只增不减。常⻅解决方法:在合适的时机进行集合对象的移 除。eg:MainActivity的ondestroy中。
2.单例模式
不正确使用单例模式是引起内存泄露的一个常⻅问题,单例对象在被初始化后将在 JVM 的整个生命周期 中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被 JVM 正常 回收,导致内存泄露。常⻅解决方法是:解决方法是Activity的Context,换成了applicationContext。
以及下载类单例持有Activity,⻓周期持有短周期,导致内存泄露。
3.Rxbus内存泄露 在有生命周期的Android组件中,比如Activity、Fragment中,忘记在onDestroy方法中取消订阅。导致的RxJava的内存泄漏。
4. Handler等非静态内部类内存泄露 非静态内部类持有外部类的引用,导致外部类不能及时被回收,造成内存泄漏。常⻅解决办法:静态内部类+弱引用。
5.定时器TimerTask内存泄露
非静态内部类TimerTask会一直持有外部类Activity 的引用,任务没有执行完毕它所引用的老的 Activity 也不会被销毁,因此就出现了内存泄露的问题。
6.依赖Activity的类资源,在onDestory方法里面没有释放的 进行集合的清理、大对象的回收、监听回调的注销等