如需转载请评论或简信,并注明出处,未经允许不得转载
目录
前言
在写给程序员的内存泄漏治理手册中我们介绍了android内存泄漏的原理以及治理方案。通过上一节的学习我们可以做到尽可能的避免写出有可能内存泄漏的代码。但是实际开发过程中,由于一个项目往往有多人一起开发,以及有时候项目开发节奏比较快,所以项目开发过程中依然很有可能会出现一些内存泄漏问题,但是内存泄漏问题往往比较隐蔽,不容易发现。所以这里就介绍一款非常好用的内存泄漏检测工具LeakCanary
LeakCanary
官方网站:https://square.github.io/leakcanary/
LeakCanary的使用
添加依赖
LeakCanary
升级到2.0之后,使用起来非常简单,只需要在build.gradle
中添加依赖
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0'
}
上面这个是官网中给出的依赖方式,目的是为了防止大家在release版本中使用,但是有些项目需要设置多个buildtype,那如果以上面这种方式集成就无法正常使用LeakCanary,这种情况可以使用下面这种集成方式
- 在自定义buildtype中设置
debuggable true
,并设置测试版本的签名文件。或直接只用initWith debug
debug2{
debuggable true
...
}
- 增加
debug2Implementation
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0'
debug2Implementation 'com.squareup.leakcanary:leakcanary-android:2.0'
...
}
注意这里不要直接使用implementation
,使用implementation
虽然不会在正式版中弹出内存泄漏的弹窗(因为LeakCanary内部做了限制),但是会增大约1mb的安装包体积
模拟内存泄漏
- 先创建一个单例类
SingleInstance.java
public class SingleInstance {
private Context context;
private SingleInstance(Context context) {
this.context = context;
}
public static class Holder {
private static SingleInstance INSTANCE;
public static SingleInstance newInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = new SingleInstance(context);
}
return INSTANCE;
}
}
}
- 将
SecondActivity
的引用加入到单例中
按back键回到MainActivity
,这时候SecondActivity
的onDestory()
会执行,但是单例类中依然持有SecondActivity
的引用,这时候SecondActivity
就会出现内存泄漏
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
SingleInstance.Holder.newInstance(this);
}
}
- LeakCanary监测到内存泄漏后会发送一个通知
- 点开通知,就可以看到详细的内存泄漏堆栈信息
同样我们也可以通过Logcat查看内存泄漏信息,如下所示
2019-12-30 16:36:40.130 20875-21568/com.geekholt.leakcanarydemo D/LeakCanary: ====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
80988 bytes retained
┬
├─ android.os.HandlerThread
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ GC Root: Local variable in native code
│ ↓ thread HandlerThread.contextClassLoader
├─ dalvik.system.PathClassLoader
│ Leaking: NO (Object[]↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[]
│ Leaking: NO (SingleInstance$Holder↓ is not leaking)
│ ↓ array Object[].[757]
├─ com.geekholt.leakcanarydemo.SingleInstance$Holder
│ Leaking: NO (a class is never leaking)
│ ↓ static SingleInstance$Holder.INSTANCE
│ ~~~~~~~~
├─ com.geekholt.leakcanarydemo.SingleInstance
│ Leaking: UNKNOWN
│ ↓ SingleInstance.context
│ ~~~~~~~
╰→ com.geekholt.leakcanarydemo.SecondActivity
Leaking: YES (Activity#mDestroyed is true and ObjectWatcher was watching this)
key = 2a99b464-42c4-4b8c-a0a8-5edb348f90bb
watchDurationMillis = 10486
retainedDurationMillis = 5485
====================================
0 LIBRARY LEAKS
Leaks coming from the Android Framework or Google libraries.
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 28
Build.MANUFACTURER: HUAWEI
LeakCanary version: 2.0
App process name: com.geekholt.leakcanarydemo
Analysis duration: 11596 ms
Heap dump file path: /data/user/0/com.geekholt.leakcanarydemo/files/leakcanary/2019-12-30_16-36-25_973.hprof
Heap dump timestamp: 1577695000128
====================================
可以看出,LeakCanary
能够实时地帮我们监测出程序中内存泄漏问题,且定位非常准确,可以说是非常的强大!既然这个工具如此强大,所以我们在使用的同时,最好也能理解其中的原理,这样才能真正融会贯通,为己所用
LeakCanary原理
这里再提醒一下,本文的源码都是基于LeakCanary2.0
的哦
用过LeakCanary
1.x的同学一定知道,过去LeakCanary
初始化的时候都是需要在Application
中调用LeakCanary.install()
进行注册的,升级到2.0之后连注册的代码都省了。那LeanCanary2.0是如何生效的呢?
LeakCanary初始化
查看LeakCanary源码,依然发现了install()
相关的代码
AppWatcher.java
/**
* [AppWatcher] is automatically installed on main process start by
* [leakcanary.internal.AppWatcherInstaller] which is registered in the AndroidManifest.xml of
* your app. If you disabled [leakcanary.internal.AppWatcherInstaller] or you need AppWatcher
* or LeakCanary to run outside of the main process then you can call this method to install
* [AppWatcher].
*/
fun manualInstall(application: Application) = InternalAppWatcher.install(application)
从这个方法的注释中我们得出了以下信息:
-
AppWatcher#manualInstall()
会在主进程中自动被AppWatcherInstaller
调用 -
AppWatcherInstaller
会在AndroidManifest.xml
中被注册 - 如果要在非主进程监听内存泄漏,需要手动调用
AppWatcher#manualInstall()
方法
既然这个AppWatcherInstaller
会在AndroidManifest.xml
中被注册,那么它一定是四大组件之一,查看源码发现,其实AppWatcherInstaller
就是ContentProvider
的子类
AppWatcherInstaller.java
internal sealed class AppWatcherInstaller : ContentProvider() {
/**
* [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
*/
internal class MainProcess : AppWatcherInstaller()
/**
* When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
* [LeakCanaryProcess] automatically sets up the LeakCanary code
*/
internal class LeakCanaryProcess : AppWatcherInstaller() {
override fun onCreate(): Boolean {
super.onCreate()
AppWatcher.config = AppWatcher.config.copy(enabled = false)
return true
}
}
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
//执行LeakCanary初始化操作
InternalAppWatcher.install(application)
return true
}
override fun query(
uri: Uri,
strings: Array<String>?,
s: String?,
strings1: Array<String>?,
s1: String?
): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(
uri: Uri,
contentValues: ContentValues?
): Uri? {
return null
}
override fun delete(
uri: Uri,
s: String?,
strings: Array<String>?
): Int {
return 0
}
override fun update(
uri: Uri,
contentValues: ContentValues?,
s: String?,
strings: Array<String>?
): Int {
return 0
}
}
APP构建经过manifest-merge
后会合并多个清单文件,这个ContentProvider
会被合并到唯一的manifest.xml中. 当APP初始化时会加载这个LeakSentryInstaller
,就会自动帮我们执行InternalLeakSentry.install(application)
作为ContentProvider他的其他CRUD实现都是空的,作者只是巧妙利用了ContentProvider无需显式初始化的特性(对比Service、BroadcastReceiver)来实现了自动注册
如何检测Activity内存泄漏
我们再来看看InternalAppWatcher#install()
方法做了什么
InternalAppWatcher.java
这个方法中比较关键的就是ActivityDestroyWatcher#install()
和FragmentDestroyWatcher#install()
,
这两个方法内部实现思路相似,所以就这里只分析ActivityDestroyWatcher#install()
fun install(application: Application) {
SharkLog.logger = DefaultCanaryLog()
SharkLog.d { "Installing AppWatcher" }
checkMainThread()
if (this::application.isInitialized) {
return
}
InternalAppWatcher.application = application
val configProvider = { AppWatcher.config }
ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
onAppWatcherInstalled(application)
}
ActivityDestroyWatcher.java
这个方法实际上就是调用了application#registerActivityLifecycleCallbacks()
对整个应用中的Activity
的生命周期进行监听
internal class ActivityDestroyWatcher private constructor(
private val objectWatcher: ObjectWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
//当activity onDestory的时候做处理
objectWatcher.watch(activity)
}
}
}
companion object {
fun install(
application: Application,
objectWatcher: ObjectWatcher,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(objectWatcher, configProvider)
//生命周期监听
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
ObjectWatcher.java
源码看到目前为止,小结一下其实就是Activity
会在onDestory()
之后,调用下面的ObjectWatcher#watch()
,这个方法比较关键,不仅仅可以检测Activity的内存泄漏,还可以通过这个方法检测任何对象的内存泄漏
@Synchronized fun watch(
watchedObject: Any,
name: String
) {
if (!isEnabled()) {
return
}
//1.移除弱可达引用
//弱可达:一个对象只被弱引用所引用,由于弱引用的特性,这样的对象是不会出现内存泄漏的
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
//2.将activity加入到WeakReference中
val reference =
KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (name.isNotEmpty()) " named $name" else "") +
" with key $key"
}
//3.将reference保存到一个数组中
watchedObjects[key] = reference
checkRetainedExecutor.execute {
//4.五秒后执行后续流程(checkRetainedExecutor配置了watchDurationMillis是5秒,具体可以自己看代码)
moveToRetained(key)
}
}
/**
* 移除弱可达引用
*/
private fun removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
var ref: KeyedWeakReference?
// 已经回收掉的弱引用对象会存放在RefrenceQueue中,循环移除
do {
ref = queue.poll() as KeyedWeakReference?
//如果RefrenceQueue里存在,说明这个弱引用对象被回收了
if (ref != null) {
val removedRef = watchedReferences.remove(ref.key)
//如果watchedReferences中的这个弱引用对象被回收了,retainedReferences也移除掉这个弱引用
if (removedRef == null) {
retainedReferences.remove(ref.key)
}
}
} while (ref != null)
}
思考1:如何判断一个对象是否被回收
其实注释中已经基本解释了
如果一个对象除了弱引用以外,没有被其他对象所引用,当发生GC时,这个弱引用对象就会被回收,并且被回收掉的对象会被存放到ReferenceQueue中,所以当ReferenceQueue中有这个对象就代表这个对象已经被回收,反之就是没有被回收
思考2: 这里为什么要延迟五秒执行任务
我们都知道GC不是即时的, 页面销毁后预留5秒的时间给GC操作, 再后续分析引用泄露, 避免无效的分析
HeapDumpTrigger.java
//仅展示关键代码
private fun checkRetainedObjects(reason: String) {
...
//移除弱可达对象后,统计中还剩下的引用数
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
//手动进行一次GC
gcTrigger.runGc()
//在GC后, 再次统计剩下的引用数
//到这一步剩下的就是没被回收掉的就是可能发生泄露的引用. 需要后续的dump分析
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//判断当前泄露实例个数如果小于5个,仅仅只是给用户一个通知,不会进行heap dump 操作,并在5s后再次发起检测
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
...
//生成内存泄漏堆栈信息
val heapDumpFile = heapDumper.dumpHeap()
.....
}
private fun checkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int // retainedVisibleThreshold默认为 5 个
): Boolean {
val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
lastDisplayedRetainedObjectCount = retainedKeysCount
if (retainedKeysCount == 0) {
SharkLog.d { "No retained objects" }
if (countChanged) {
showNoMoreRetainedObjectNotification()
}
return true
}
if (retainedKeysCount < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
SharkLog.d {
"Found $retainedKeysCount retained objects, which is less than the visible threshold of $retainedVisibleThreshold"
}
// // 通知用户 "App visible, waiting until 5 retained instances"
showRetainedCountBelowThresholdNotification(retainedKeysCount, retainedVisibleThreshold)
// 5s 后再次发起检测
scheduleRetainedObjectCheck(
"Showing retained objects notification", WAIT_FOR_OBJECT_THRESHOLD_MILLIS
)
return true
}
}
return false
}
生成heap dump 文件
AndroidHeapDumper.java
这个过程主要就是两步
1.发送通知
2.使用Debug.dumpHprofData(heapDumpFile.absolutePath)
捕获堆转储
override fun dumpHeap(): File? {
val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null
val waitingForToast = FutureResult<Toast?>()
showToast(waitingForToast)
if (!waitingForToast.wait(5, SECONDS)) {
SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
return null
}
//1.发送通知
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Notifications.canShowNotification) {
val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
val builder = Notification.Builder(context)
.setContentTitle(dumpingHeap)
val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
}
val toast = waitingForToast.get()
return try {
//2.捕获堆转储
Debug.dumpHprofData(heapDumpFile.absolutePath)
if (heapDumpFile.length() == 0L) {
SharkLog.d { "Dumped heap file is 0 byte length" }
null
} else {
heapDumpFile
}
} catch (e: Exception) {
SharkLog.d(e) { "Could not dump heap" }
// Abort heap dump
null
} finally {
cancelToast(toast)
notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
}
}
分析 heap dump 文件
最后启动一个前台服务 HeapAnalyzerService
来分析 heap dump 文件。然后通过解析库找到最短 GC Roots 引用路径,展示给用户。这里不是我们的重点,就不做具体分析了,感兴趣的可以自己看一下源码
总结
- LeakCanary2.0利用了ContentProvider无需显式初始化的特性来实现了自动注册
- 通过
application#registerActivityLifecycleCallbacks()
对Activity的生命周期进行监听 - 当
Activity
销毁时,将Activity
添加到一个WeakReference
中,利用WeakReference
和ReferenceQueue
的特性,如果一个对象除了弱引用以外,没有被其他对象所引用,当发生GC时,这个弱引用对象就会被回收,并且被回收掉的对象会被存放到ReferenceQueue
中,所以当ReferenceQueue
中有这个对象就代表这个对象已经被回收,反之就是没有被回收 - 调用Android原生提供的捕获堆转储的方法
Debug.dumpHprofData(heapDumpFile.absolutePath)
- 使用解析库来分析 heap dump 文件