如需轉載請評論或簡信,并注明出處,未經(jīng)允許不得轉載
目錄
前言
在寫給程序員的內存泄漏治理手冊中我們介紹了android內存泄漏的原理以及治理方案。通過上一節(jié)的學習我們可以做到盡可能的避免寫出有可能內存泄漏的代碼。但是實際開發(fā)過程中,由于一個項目往往有多人一起開發(fā),以及有時候項目開發(fā)節(jié)奏比較快,所以項目開發(fā)過程中依然很有可能會出現(xiàn)一些內存泄漏問題,但是內存泄漏問題往往比較隱蔽,不容易發(fā)現(xiàn)。所以這里就介紹一款非常好用的內存泄漏檢測工具LeakCanary
LeakCanary
官方網(wǎng)站: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'
}
上面這個是官網(wǎng)中給出的依賴方式,目的是為了防止大家在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的安裝包體積
模擬內存泄漏
- 先創(chuàng)建一個單例類
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()
會執(zhí)行,但是單例類中依然持有SecondActivity
的引用,這時候SecondActivity
就會出現(xiàn)內存泄漏
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
SingleInstance.Holder.newInstance(this);
}
}
- LeakCanary監(jiān)測到內存泄漏后會發(fā)送一個通知
- 點開通知,就可以看到詳細的內存泄漏堆棧信息
同樣我們也可以通過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
能夠實時地幫我們監(jiān)測出程序中內存泄漏問題,且定位非常準確,可以說是非常的強大!既然這個工具如此強大,所以我們在使用的同時,最好也能理解其中的原理,這樣才能真正融會貫通,為己所用
LeakCanary原理
這里再提醒一下,本文的源碼都是基于LeakCanary2.0
的哦
用過LeakCanary
1.x的同學一定知道,過去LeakCanary
初始化的時候都是需要在Application
中調用LeakCanary.install()
進行注冊的,升級到2.0之后連注冊的代碼都省了。那LeanCanary2.0是如何生效的呢?
LeakCanary初始化
查看LeakCanary源碼,依然發(fā)現(xiàn)了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
中被注冊 - 如果要在非主進程監(jiān)聽內存泄漏,需要手動調用
AppWatcher#manualInstall()
方法
既然這個AppWatcherInstaller
會在AndroidManifest.xml
中被注冊,那么它一定是四大組件之一,查看源碼發(fā)現(xiàn),其實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
//執(zhí)行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構建經(jīng)過manifest-merge
后會合并多個清單文件,這個ContentProvider
會被合并到唯一的manifest.xml中. 當APP初始化時會加載這個LeakSentryInstaller
,就會自動幫我們執(zhí)行InternalLeakSentry.install(application)
作為ContentProvider他的其他CRUD實現(xiàn)都是空的,作者只是巧妙利用了ContentProvider無需顯式初始化的特性(對比Service、BroadcastReceiver)來實現(xiàn)了自動注冊
如何檢測Activity內存泄漏
我們再來看看InternalAppWatcher#install()
方法做了什么
InternalAppWatcher.java
這個方法中比較關鍵的就是ActivityDestroyWatcher#install()
和FragmentDestroyWatcher#install()
,
這兩個方法內部實現(xiàn)思路相似,所以就這里只分析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
的生命周期進行監(jiān)聽
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)
//生命周期監(jiān)聽
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
ObjectWatcher.java
源碼看到目前為止,小結一下其實就是Activity
會在onDestory()
之后,調用下面的ObjectWatcher#watch()
,這個方法比較關鍵,不僅僅可以檢測Activity的內存泄漏,還可以通過這個方法檢測任何對象的內存泄漏
@Synchronized fun watch(
watchedObject: Any,
name: String
) {
if (!isEnabled()) {
return
}
//1.移除弱可達引用
//弱可達:一個對象只被弱引用所引用,由于弱引用的特性,這樣的對象是不會出現(xiàn)內存泄漏的
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保存到一個數(shù)組中
watchedObjects[key] = reference
checkRetainedExecutor.execute {
//4.五秒后執(zhí)行后續(xù)流程(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?
// 已經(jīng)回收掉的弱引用對象會存放在RefrenceQueue中,循環(huán)移除
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:如何判斷一個對象是否被回收
其實注釋中已經(jīng)基本解釋了
如果一個對象除了弱引用以外,沒有被其他對象所引用,當發(fā)生GC時,這個弱引用對象就會被回收,并且被回收掉的對象會被存放到ReferenceQueue中,所以當ReferenceQueue中有這個對象就代表這個對象已經(jīng)被回收,反之就是沒有被回收
思考2: 這里為什么要延遲五秒執(zhí)行任務
我們都知道GC不是即時的, 頁面銷毀后預留5秒的時間給GC操作, 再后續(xù)分析引用泄露, 避免無效的分析
HeapDumpTrigger.java
//僅展示關鍵代碼
private fun checkRetainedObjects(reason: String) {
...
//移除弱可達對象后,統(tǒng)計中還剩下的引用數(shù)
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
//手動進行一次GC
gcTrigger.runGc()
//在GC后, 再次統(tǒng)計剩下的引用數(shù)
//到這一步剩下的就是沒被回收掉的就是可能發(fā)生泄露的引用. 需要后續(xù)的dump分析
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//判斷當前泄露實例個數(shù)如果小于5個,僅僅只是給用戶一個通知,不會進行heap dump 操作,并在5s后再次發(fā)起檢測
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 后再次發(fā)起檢測
scheduleRetainedObjectCheck(
"Showing retained objects notification", WAIT_FOR_OBJECT_THRESHOLD_MILLIS
)
return true
}
}
return false
}
生成heap dump 文件
AndroidHeapDumper.java
這個過程主要就是兩步
1.發(fā)送通知
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.發(fā)送通知
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無需顯式初始化的特性來實現(xiàn)了自動注冊
- 通過
application#registerActivityLifecycleCallbacks()
對Activity的生命周期進行監(jiān)聽 - 當
Activity
銷毀時,將Activity
添加到一個WeakReference
中,利用WeakReference
和ReferenceQueue
的特性,如果一個對象除了弱引用以外,沒有被其他對象所引用,當發(fā)生GC時,這個弱引用對象就會被回收,并且被回收掉的對象會被存放到ReferenceQueue
中,所以當ReferenceQueue
中有這個對象就代表這個對象已經(jīng)被回收,反之就是沒有被回收 - 調用Android原生提供的捕獲堆轉儲的方法
Debug.dumpHprofData(heapDumpFile.absolutePath)
- 使用解析庫來分析 heap dump 文件