Android 記憶體洩漏檢查工具LeakCanary原始碼淺析
使用
監控 Activity 洩露
我們經常把 Activity 當作為 Context 物件使用,在不同場合由各種物件引用 Activity。所以,Activity 洩漏是一個重要的需要檢查的記憶體洩漏之一。
public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@Override public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}
LeakCanary.install() 返回一個配置好了的 RefWatcher 例項。它同時安裝了 ActivityRefWatcher 來監控 Activity 洩漏。即當 Activity.onDestroy() 被呼叫之後,如果這個 Activity 沒有被銷燬,logcat 就會打印出資訊告訴你記憶體洩漏發生了。
leakInfo:In com.example.leakcanary:1.0:1.
* com.example.leakcanary.MainActivity has leaked:
* GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$2.this$0 (anonymous subclass of android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance *
LeakCanary 自動檢測 Activity 洩漏只支援 Android ICS 以上版本。因為 Application.registerActivityLifecycleCallbacks() 是在 API 14 引入的。如果要在 ICS 之前監測 Activity 洩漏,可以過載 Activity.onDestroy() 方法,然後在這個方法裡呼叫 RefWatcher.watch(this) 來實現。
監控 Fragment 洩漏
public abstract class BaseFragment extends Fragment {
@Override
public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}
當 Fragment.onDestroy() 被呼叫之後,如果這個 fragment 例項沒有被銷燬,那麼就會從 logcat 裡看到相應的洩漏資訊。
監控其他洩漏
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(someObjNeedGced);
當 someObjNeedGced 還在記憶體中時,就會在 logcat 裡看到記憶體洩漏的提示。
原理
1. RefWatcher.watch() 建立一個 KeyedWeakReference 到要被監控的物件。
2. 然後在後臺執行緒檢查引用是否被清除,如果沒有,呼叫GC。
3. 如果引用還是未被清除,把 heap 記憶體 dump 到 APP 對應的檔案系統中的一個 .hprof 檔案中。
4. 在另外一個程序中的 HeapAnalyzerService 有一個 HeapAnalyzer 使用 另一個開源庫HAHA 解析這個hprof檔案。(MAT也可以解析hprof,但是是圖形化的形式,HAHA完全是非UI形式解析)
5. 得益於唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位記憶體洩露。
6. HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,並確定是否是洩露。如果是的話,建立導致洩露的引用鏈。
7. 引用鏈傳遞到 APP 程序中的 DisplayLeakService, 並以通知的形式展示出來。
現在就來通過程式碼來講解吧
首先,Application的onCreate方法中呼叫LeakCanary.install(this);
做初始化工作
LeakCanary.java
public static RefWatcher install(Application application) {
return install(application, DisplayLeakService.class,
AndroidExcludedRefs.createAppDefaults().build());
}
public static RefWatcher install(Application application,
Class<? extends AbstractAnalysisResultService> listenerServiceClass,
ExcludedRefs excludedRefs) {
//首先判斷當前應用程式程序是否在HeapAnalyzerService所在的程序,如果是直接return,HeapAnalyzerService所在的程序是HAHA庫專門解析hprof檔案的程序
if (isInAnalyzerProcess(application)) {
return RefWatcher.DISABLED;
}
enableDisplayLeakActivity(application);
HeapDump.Listener heapDumpListener =
new ServiceHeapDumpListener(application, listenerServiceClass); //listenerServiceClass就是上面的DisplayLeakService.class,用來顯示記憶體洩漏資訊用的
RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs); //構造RefWatcher類物件
ActivityRefWatcher.installOnIcsPlus(application, refWatcher); //呼叫ActivityRefWatcher的方法
return refWatcher;
}
//ActivityRefWatcher類,專門針對activity,所以在api14以上的onDestroy中不用監控,自動回撥
@TargetApi(ICE_CREAM_SANDWICH) public final class ActivityRefWatcher {
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
..............
@Override public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity); //當activity退出執行了onDestory方法的時候就會執行該onActivityDestroyed方法
}
};
private final Application application;
private final RefWatcher refWatcher;
void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity); //呼叫watch方法
}
public void watchActivities() {
// Make sure you don't get installed twice.
stopWatchingActivities();
application.registerActivityLifecycleCallbacks(lifecycleCallbacks); //api14才引入的方法,任何activity生命週期的變化都會回撥給lifecycleCallbacks
}
public void stopWatchingActivities() {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
}
}
excludedRefs
/* References that should be ignored when analyzing this heap dump. /
SDK導致的記憶體洩露,隨著時間的推移,很多SDK 和廠商 ROM 中的記憶體洩露問題已經被儘快修復了。但是,當這樣的問題發生時,一般的開發者能做的事情很有限。LeakCanary有一個已知問題的忽略列表,AndroidExcludedRefs.java
中可以檢視到
就是說如果發現AndroidExcludedRefs.java
類中維護的列表類的記憶體洩漏,那麼在DisplayLeakService並不會顯示出來,同時HeapAnalyzer在計算到GC roots的最短強引用路徑,也會忽略這些類
下面具體分析。
此時初始化完畢,如果退出一個activity那麼就會執行refWatcher.watch(activity);
RefWatcher.java
public void watch(Object watchedReference, String referenceName) {
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
if (debuggerControl.isDebuggerAttached()) { //連上資料線就此時就返回true了
return;
}
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString(); //唯一的key
retainedKeys.add(key); //key儲存在retainedKeys集合
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue); //虛引用KeyedWeakReference
watchExecutor.execute(new Runnable() { //watchExecutor就不說了,自定義的一個Executor
@Override
public void run() {
ensureGone(reference, watchStartNanoTime); //關鍵ensureGone方法
}
});
}
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
removeWeaklyReachableReferences();
if (gone(reference) || debuggerControl.isDebuggerAttached()) { //判斷reference是否被回收了已經為null,同時是否插了USB線,除錯模式下,不去檢查記憶體洩漏
return;
}
gcTrigger.runGc(); //觸發GC
removeWeaklyReachableReferences();
if (!gone(reference)) { //哈,此時說明強制發生GC,該引用還在,那麼就沒被回收咯,可能發生記憶體洩漏,所以才能繼續往下走
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = heapDumper.dumpHeap(); //關鍵,生成dropf檔案
if (heapDumpFile == HeapDumper.NO_DUMP) { //NO_DUMP型別,木有生成dropf吧,直接return
// Could not dump the heap, abort.
return;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs)); //heapdumpListener就是上面初始化的ServiceHeapDumpListener
}
}
private boolean gone(KeyedWeakReference reference) {
//當referent被回收了,因為removeWeaklyReachableReferences方法所以該key被移出了,contains返回false,gone返回true
//當referent沒被回收了,因為key還在retainedKeys中,contains返回true,gone返回false
return !retainedKeys.contains(reference.key);
}
private void 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.
KeyedWeakReference ref;
//ReferenceQueue的poll方法,當referent被回收了那麼poll方法返回不為null,沒被GC回收才為null
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
retainedKeys.remove(ref.key); //當referent被回收了,那麼從retainedKeys移出該key
}
}
先稍微看下KeyedWeakReference的定義,繼承自WeakReference
final class KeyedWeakReference extends WeakReference<Object> {
public final String key;
public final String name;
KeyedWeakReference(Object referent, String key, String name,
ReferenceQueue<Object> referenceQueue) {
super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
this.key = checkNotNull(key, "key");
this.name = checkNotNull(name, "name");
}
}
測試一下
public static void main(String[] args) {
// TODO Auto-generated method stub
WeakReference w = new WeakReference(new String("haha"), queue);
System.gc();
if (w.get() == null) {
System.out.println("w get is null");
} else {
System.out.println("w get is not null");
}
WeakReference temp= (WeakReference) queue.poll();
if(temp != null) { // ReferenceQueue的poll方法
System.out.println("queue.poll is not null");
}else{
System.out.println("queue.poll is null");
}
}
列印
w get is null
queue.poll is not null
註釋掉System.gc();
此時列印
w get is not null
queue.poll is null
然後看看heapDumper.dumpHeap()
怎麼生成hprof檔案,然後再看heapdumpListener.analyze流程
AndroidHeapDumper.java
@Override public File dumpHeap() {
if (!leakDirectoryProvider.isLeakStorageWritable()) {
CanaryLog.d("Could not write to leak storage to dump heap.");
leakDirectoryProvider.requestWritePermission();
return NO_DUMP;
}
File heapDumpFile = getHeapDumpFile();
// Atomic way to check for existence & create the file if it doesn't exist.
// Prevents several processes in the same app to attempt a heapdump at the same time.
boolean fileCreated;
try {
fileCreated = heapDumpFile.createNewFile();
} catch (IOException e) {
cleanup();
CanaryLog.d(e, "Could not check if heap dump file exists");
return NO_DUMP;
}
if (!fileCreated) {
CanaryLog.d("Could not dump heap, previous analysis still is in progress.");
// Heap analysis in progress, let's not put too much pressure on the device.
return NO_DUMP;
}
FutureResult<Toast> waitingForToast = new FutureResult<>(); //在生成heap dump檔案的時候會彈出一個toast提示介面
showToast(waitingForToast);
if (!waitingForToast.wait(5, SECONDS)) {
CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
return NO_DUMP;
}
Toast toast = waitingForToast.get();
try {
Debug.dumpHprofData(heapDumpFile.getAbsolutePath()); //Debug.dumpHprofData方法生成hprof並寫入該file,其實DDMS獲取hprof檔案應該也是呼叫該方法的
cancelToast(toast);
return heapDumpFile;
} catch (Exception e) {
cleanup();
CanaryLog.d(e, "Could not perform heap dump");
// Abort heap dump
return NO_DUMP;
}
}
hprof檔案預設放在/sdcard/Download/…/目錄下,所以如果要使用MAT分析的話,可以到該目錄下尋找,該hprof不能直接被MAT檢視,需要android提供的hprof-conv轉換
生成了hprof檔案之後,執行heapdumpListener.analyze流程,主要就是通過另外一個程序中的HeapAnalyzerService有一個HeapAnalyzer(內部實際呼叫開源庫HAHA)解析這個hprof檔案,然後如果需要就顯示在傳遞到APP程序中的DisplayLeakService,並以通知的形式展示出來,程式碼如下
HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
public final class HeapAnalyzerService extends IntentService {
public static void runAnalysis(Context context, HeapDump heapDump,
Class<? extends AbstractAnalysisResultService> listenerServiceClass) { //靜態方法
Intent intent = new Intent(context, HeapAnalyzerService.class); //啟動本服務,此時HeapAnalyzerService執行在leakcanary獨立程序中
intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName()); //HeapAnalyzerService
intent.putExtra(HEAPDUMP_EXTRA, heapDump); //hropf檔案
context.startService(intent);
}
@Override protected void onHandleIntent(Intent intent) {
if (intent == null) {
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
return;
}
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA); //DisplayLeakService
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA); //hropf檔案
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);
//HeapAnalyzer.checkForLeak解析這個hprof檔案完畢,那麼傳送給app程序的HeapAnalyzerService顯示通知
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
}
<service
android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
android:enabled="false"
android:process=":leakcanary" />
分析hprof檔案,設計IO操作,放在leakcanary獨立程序當然更好啦,此時沒有不是單獨開執行緒的方式
HeapAnalyzer.java
public final class HeapAnalyzer {
private final ExcludedRefs excludedRefs;
public HeapAnalyzer(ExcludedRefs excludedRefs) {
this.excludedRefs = excludedRefs;
}
public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
..........
try {
//開始解析hropf,HAHA庫中的類HprofBuffer,HprofParser,Snapshot
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
Instance leakingRef = findLeakingReference(referenceKey, snapshot); //findLeakingReference檢查該物件是否
// False alarm, weak reference was cleared in between key check and heap dump.
if (leakingRef == null) { //為null說明已經被GC回收啦,那麼直接return noLeak型別,DisplayLeakService就不顯示啦
return noLeak(since(analysisStartNanoTime));
}
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef); //走到這裡,就是計算到GC roots的最短強引用路徑,並確定是否是洩露。如果是的話,建立導致洩露的引用鏈
} catch (Throwable e) {
return failure(e, since(analysisStartNanoTime));
}
}
private Instance findLeakingReference(String key, Snapshot snapshot) {
ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName()); //HAHA類庫snapshot的findclass方法
List<String> keysFound = new ArrayList<>();
for (Instance instance : refClass.getInstancesList()) {
List<ClassInstance.FieldValue> values = classInstanceValues(instance);
String keyCandidate = asString(fieldValue(values, "key"));
if (keyCandidate.equals(key)) {
return fieldValue(values, "referent");
}
keysFound.add(keyCandidate);
}
throw new IllegalStateException(
"Could not find weak reference with key " + key + " in " + keysFound);
}
//計算到GC roots的最短強引用路徑,並確定是否是洩露。如果是的話,建立導致洩露的引用鏈
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
Instance leakingRef) {
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs); //到GCroot最短路勁ShortestPathFinder,此時排除了SDK自帶問題的excludedRefs
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
// False alarm, no strong reference path to GC Roots.
if (result.leakingNode == null) {
return noLeak(since(analysisStartNanoTime));
}
LeakTrace leakTrace = buildLeakTrace(result.leakingNode); //buildLeakTrace方法建立導致洩露的引用鏈
String className = leakingRef.getClassObj().getClassName();
// Side effect: computes retained size.
snapshot.computeDominators();
Instance leakingInstance = result.leakingNode.instance;
long retainedSize = leakingInstance.getTotalRetainedSize();
retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
since(analysisStartNanoTime));
}
private LeakTrace buildLeakTrace(LeakNode leakingNode) //建立導致洩露的引用鏈
private LeakTraceElement buildLeakElement(LeakNode node) //建立導致洩露的引用鏈元素
最後來看下app程序中的DisplayLeakService用來顯示記憶體洩漏的引用鏈的IntentService
public class DisplayLeakService extends AbstractAnalysisResultService {
@Override protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
String leakInfo = leakInfo(this, heapDump, result, true);
CanaryLog.d(leakInfo);
boolean resultSaved = false;
boolean shouldSaveResult = result.leakFound || result.failure != null; //leakFound表示木有記憶體洩漏
if (shouldSaveResult) {
heapDump = renameHeapdump(heapDump);
resultSaved = saveResult(heapDump, result);
}
PendingIntent pendingIntent;
String contentTitle;
String contentText;
if (!shouldSaveResult) {
contentTitle = getString(R.string.leak_canary_no_leak_title);
contentText = getString(R.string.leak_canary_no_leak_text);
pendingIntent = null;
} else if (resultSaved) {
pendingIntent = DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);
if (result.failure == null) {
String size = formatShortFileSize(this, result.retainedHeapSize);
String className = classSimpleName(result.className);
if (result.excludedLeak) { //注意這裡的excludedLeak
contentTitle = getString(R.string.leak_canary_leak_excluded, className, size);
} else {
contentTitle = getString(R.string.leak_canary_class_has_leaked, className, size);
}
} else {
contentTitle = getString(R.string.leak_canary_analysis_failed);
}
contentText = getString(R.string.leak_canary_notification_message);
} else {
contentTitle = getString(R.string.leak_canary_could_not_save_title);
contentText = getString(R.string.leak_canary_could_not_save_text);
pendingIntent = null;
}
showNotification(this, contentTitle, contentText, pendingIntent);
afterDefaultHandling(heapDump, result, leakInfo);
}
.....
}
第一行的leakInfo列印結果:
leakInfo:In com.example.leakcanary:1.0:1.
* com.example.leakcanary.MainActivity has leaked:
* GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$2.this$0 (anonymous subclass of android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance
至此分析完畢
GC回收演算法
hprof檔案轉換MAT檢視
Debug.dumpHprofData方法生成hprof不能直接在MAT中檢視,需要使用android提供的hprof-conv轉換
hprof-conv xxxxx.hprof yyyyy.hprof,其中xxxxx.hprof為原始檔案,yyyyy.hprof為轉換過後的檔案
根搜尋演算法
參考:根搜尋演算法
在主流的商用程式語言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜尋演算法(GC Roots Tracing)判定物件是否存活的。這個演算法的基本思路就是通過一系列的名為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的。如圖3-1所示,物件object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的物件。
在Java語言裡,可作為GC Roots的物件包括下面幾種:
1. 虛擬機器棧(棧幀中的本地變量表)中的引用的物件。
2. 方法區中的類靜態屬性引用的物件。
3. 方法區中的常量引用的物件。
4. 本地方法棧中JNI(即一般說的Native方法)的引用的物件。
無論是通過引用計數演算法判斷物件的引用數量,還是通過根搜尋演算法判斷物件的引用鏈是否可達,判定物件是否存活都與“引用”有關。在JDK 1.2之前,Java中的引用的定義很傳統:如果reference型別的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。這種定義很純粹,但是太過狹隘,一個物件在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些“食之無味,棄之可惜”的物件就顯得無能為力。我們希望能描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體之中;如果記憶體在進行垃圾收集後還是非常緊張,則可以拋棄這些物件。很多系統的快取功能都符合這樣的應用場景。
在JDK 1.2之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。
1. 強引用就是指在程式程式碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。
2. 軟引用用來描述一些還有用,但並非必需的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。
3. 弱引用也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK 1.2之後,提供了WeakReference類來實現弱引用。
4. 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是希望能在這個物件被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實現虛引用。
引用計數法
引用計數法是唯一沒有使用根集的垃圾回收的法,該演算法使用引用計數器來區分存活物件和不再使用的物件。一般來說,堆中的每個物件對應一個引用計數器。當每一次建立一個物件並賦給一個變數時,引用計數器置為1。當物件被賦給任意變數時,引用計數器每次加1當物件出了作用域後(該物件丟棄不再使用),引用計數器減1,一旦引用計數器為0,物件就滿足了垃圾收集的條件。
基於引用計數器的垃圾收集器執行較快,不會長時間中斷程式執行,適宜地必須 實時執行的程式。但引用計數器增加了程式執行的開銷,因為每次物件賦給新的變數,計數器加1,而每次現有物件出了作用域生,計數器減1。
ps:用根集的方法(既有向圖的方法)進行記憶體物件管理,可以消除迴圈引用的問題.就是說如果有三個物件相互引用,只要他們和根集是不可達的,gc也是可以回收他們.根集的方法精度很高,但是效率低.計數器法精度低(無法處理迴圈引用),但是執行效率高.