Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android內存洩露檢測工具---LeakCanary的前世今生

Android內存洩露檢測工具---LeakCanary的前世今生

編輯:關於Android編程

曾經檢測內存洩露的方式

讓我們來看看在沒有LeakCanary之前,我們怎麼來檢測內存洩露
1. Bug收集
通過Bugly、友盟這樣的統計平台,統計Bug,了解OutOfMemaryError的情況。
2. 重現問題

對Bug進行篩選,歸類,排除干擾項。然後為了重現問題,有時候你必須找到出現問題的機型,因為有些問題只會在特定的設備上才會出現。為了找到特定的機型,可能會想盡一切辦法,去買、去借、去求人(14年的時候,上家公司專門派了一個商務去廣州找了一家租賃手機的公司,借了50台手機回來,600塊錢一天)。然後,為了重現問題,一遍一遍的嘗試,去還原當時OutOfMemaryError出現的原因,用最原始、最粗暴的方式。

3. Dump導出hprof文件

使用Eclipse ADT的DDMS,觀察Heap,然後點擊手動GC按鈕(Cause GC),觀察內存增長情況,導出hprof文件。

主要觀測的兩項數據:

1. Heap Size的大小,當資源增加到堆空余空間不夠的時候,系統會增加堆空間的大小,但是超過可分配的最大值(比如手機給App分配的最大堆空間為128M)就會發生OutOfMemaryError,這個時候進程就會被殺死。這個最大堆空間,不同手機會有不同的值,跟手機內存大小和廠商定制過後的系統存在關聯。

2. Allocated堆中已分配的大小,這是應用程序實際占用的大小,資源回收後,這項數據會變小。

查看操作前後的堆數據,看是否存在內存洩露,比如反復打開、關閉一個頁面,看看堆空間是否會一直增大。

這裡寫圖片描述
4. 然後使用MAT內存分析工具打開,反復查看找到那些原本應該被回收掉的對象。
5. 計算這個對象到GC roots的最短強引用路徑。
6. 確定那個路徑中那個應用不該有,然後修復問題。

很麻煩,不是嗎。現在有一個類庫可以直接解決這個問題<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMSBpZD0="leakcanary">LeakCanary

使用方式

使用AndroidStudio,在Module.appbuild.gradle中引入

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
 }

然後在Application中重寫onCreate()方法

public class ExampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

在Activity中寫一些導致內存洩露的代碼,當發生內存洩露了,會在通知欄彈出消息,點擊跳轉到洩露頁面
這裡寫圖片描述
LeakCanary 可以做到非常簡單方便、低侵入性地捕獲內存洩漏代碼,甚至很多時候你可以捕捉到 Android 系統組件的內存洩漏代碼,最關鍵是不用再進行(捕獲錯誤+Bug歸檔+場景重現+Dump+Mat分析) 這一系列復雜操作,6得不行。

原理分析

如果我們自己實現

首先,設想如果讓我們自己來實現一個LeakCanary,我們怎麼來實現。
按照前面說的曾經檢測內存的方式,我想,大概需要以下幾個步驟:
1. 檢測一個對象,查看他是否被回收了。
2. 如果沒有被回收,使用DDMS的dump導出.hprof文件,確定是否內存洩露,如果洩露了導出最短引用路徑
3. 把最短引用路徑封裝到一個對象中,用Intent發送給Notification,然後點擊跳轉到展示頁,頁面展示

檢測對象,是否被回收

我們來看看,LeakCanary是不是按照這種方式實現的。除了剛才說的只需要在Application中的onCreate方法注冊LeakCanary.install(this);這種方式。 查看源碼,使用官方給的Demo示例代碼中,我們發現有一個RefWatcher對象,也可以用來監測,看看它是如何使用的。
MainActivity.class

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RefWatcher refWatcher = LeakCanary.androidWatcher(getApplicationContext(),
                new ServiceHeapDumpListener(getApplicationContext(), DisplayLeakService.class),
                AndroidExcludedRefs.createAppDefaults().build());
        refWatcher.watch(this);
  }

就是把MainActivity作為一個對象監測起來,查看refWatcher.watch(this)的實現

  public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

  /**
   * Watches the provided references and checks if it can be GCed. This method is non blocking,
   * the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
   *
   * @param referenceName An logical identifier for the watched object.
   */
  public void watch(Object watchedReference, String referenceName) {
    Preconditions.checkNotNull(watchedReference, "watchedReference");
    Preconditions.checkNotNull(referenceName, "referenceName");
    if (debuggerControl.isDebuggerAttached()) {
      return;
    }
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    watchExecutor.execute(new Runnable() {
      @Override public void run() {
        ensureGone(reference, watchStartNanoTime);
      }
    });
  }

可以總結出他的實現步驟如下:
1. 先檢查監測對象是否為空,為空拋出異常
2. 如果是在調試Debugger過程中允許內存洩露出現,不再監測。因為這個時候監測的對象是不准確的,而且會干擾我們調試代碼。
3. 給監測對象生成UUID唯一標識符,存入Set集合,方便查找。
4. 然後定義了一個KeyedWeakReference,查看下KeyedWeakReference是個什麼玩意

public final class KeyedWeakReference extends WeakReference

原來KeyedWeakReference就是對WeakReference進行了一些加工,是一種裝飾設計模式,其實就是弱引用的衍生類。配合前面的Set retainedKeys使用,retainedKeys代表的是沒有被GC回收的對象,referenceQueue中的弱引用代表的是被GC了的對象,通過這兩個結構就可以明確知道一個對象是不是被回收了。( 一個對象在referenceQueue可以找到當時在retainedKeys中找不到,那麼肯定被回收了,沒有內存洩漏一說)
5. 接著看上面的執行過程,然後通過線程池開啟了一個異步任務方法ensureGonewatchExecutor看看這個實體的類實現—AndroidWatchExecutor,查看源碼

public final class AndroidWatchExecutor implements Executor {

  static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
  private static final int DELAY_MILLIS = 5000;

  private final Handler mainHandler;
  private final Handler backgroundHandler;

  public AndroidWatchExecutor() {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
  }

  @Override public void execute(final Runnable command) {
    if (isOnMainThread()) {
      executeDelayedAfterIdleUnsafe(command);
    } else {
      mainHandler.post(new Runnable() {
        @Override public void run() {
          executeDelayedAfterIdleUnsafe(command);
        }
      });
    }
  }

  private boolean isOnMainThread() {
    return Looper.getMainLooper().getThread() == Thread.currentThread();
  }

  private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
        return false;
      }
    });
  }
}

做得事情就是,通過主線程的mainHandler轉發到後台backgroundHandler執行任務,後台線程延遲DELAY_MILLIS這麼多時間執行
6. 具體執行的任務在ensureGone()方法裡面

  void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    //記錄觀測對象的時間
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //清除在queue中的弱引用 保留retainedKeys中剩下的對象
    removeWeaklyReachableReferences();
    //如果剩下的對象中不包含引用對象,說明已被回收,返回||調試中,返回
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
      return;
    }
    //請求執行GC
    gcTrigger.runGc();
    //再次清理一次對象
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      //記錄下GC執行時間
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      //Dump導出hprof文件
      File heapDumpFile = heapDumper.dumpHeap();

      if (heapDumpFile == null) {
        // Could not dump the heap, abort.
        return;
      }
      //記錄下Dump和文件導出用的時間
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      //分析hprof文件
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
  }

  private boolean gone(KeyedWeakReference reference) {
    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;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }

這裡我們思考兩個問題:
1. retainedKeys和queue怎麼關聯起來的?這裡的removeWeaklyReachableReferences方法就實現了我們說的 retainedKeys代表的是沒有被GC回收的對象,queue中的弱引用代表的是被GC了的對象,之間的關聯, 一個對象在queue可以找到當時在retainedKeys中找不到,那麼肯定被回收了。gone()返回true說明對象已被回收,不需要觀測了。
2. 為什麼執行removeWeaklyReachableReferences()兩次?為了保證效率,如果對象被回收,沒必要再通知GC執行,Dump操作等等一系列繁瑣步驟,況且GC是一個線程優先級極低的線程,就算你通知了,她也不一定會執行,基於這一點,我們分析的觀測對象的時機就顯得尤為重要了,在對象被回收的時候召喚觀測。

何時執行觀測對象

我們觀測的是一個Activity,Activity這樣的組件都存在生命周期,在他生命周期結束的時,觀測他如果還存活的話 就肯定就存在內存洩露了,進一步推論,Activity的生命周期結束就關聯到它的onDestory()方法,也就是只要重寫這個方法就可以了。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        refWatcher.watch(this);
    }

在MainActivity中加上這行代碼就好了,但是我們顯然不想每個Activity都這樣干,都是同樣的代碼為啥要重復著寫,當然解決辦法呼之欲出:

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        @Override public void onActivityStarted(Activity activity) {
        }

        @Override public void onActivityResumed(Activity activity) {
        }

        @Override public void onActivityPaused(Activity activity) {
        }

        @Override public void onActivityStopped(Activity activity) {
        }

        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };
    void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
    }

LeakCanary源碼是這樣做的,通過ActivityLifecycleCallbacks轉發,然後在install()中使用這個接口,這就實現了我們只需要調用LeakCanary.install(this);這句代碼在Application中就可以實現監測

public final class LeakCanary {
    public static RefWatcher install(Application application) {
        return install(application, DisplayLeakService.class, AndroidExcludedRefs.createAppDefaults().build());
    }

    public static RefWatcher install(Application application, Class listenerServiceClass, ExcludedRefs excludedRefs) {
        if(isInAnalyzerProcess(application)) {
            return RefWatcher.DISABLED;
        } else {
            enableDisplayLeakActivity(application);
            ServiceHeapDumpListener heapDumpListener = new ServiceHeapDumpListener(application, listenerServiceClass);
            RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
            ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
            return refWatcher;
        }
    }

不需要在每個Activity方法的結束再多寫幾行onDestroy()代碼,但是這個方法有個缺點,看注釋

  // If you need to support Android < ICS, override onDestroy() in your base activity.
        //ICS
        October 2011: Android 4.0.
        public static final int ICE_CREAM_SANDWICH = 14;

如果是SDK 14 Android 4.0以下的系統,不具備這個接口,也就是還是的通過剛才那種方式重寫onDestory()方法。而且只實現了ActivityRefWatcher.installOnIcsPlus(application, refWatcher);對Activity進行監測,如果是服務或者廣播還需要我們自己實現

分析hprof文件

接著分析,查看文件解析類發現他是個轉發工具類

public final class ServiceHeapDumpListener implements HeapDump.Listener {
  ...
  @Override public void analyze(HeapDump heapDump) {
      Preconditions.checkNotNull(heapDump, "heapDump");
     //轉發給HeapAnalyzerService
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }
}

通過IntentService運行在另一個進程中執行分析任務

public final class HeapAnalyzerService extends IntentService {

  private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";
  private static final String HEAPDUMP_EXTRA = "heapdump_extra";

  public static void runAnalysis(Context context, HeapDump heapDump,
      Class listenerServiceClass) {
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    context.startService(intent);
  }

  @Override protected void onHandleIntent(Intent intent) {
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    ExcludedRefs androidExcludedDefault = createAndroidDefaults().build();
    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(androidExcludedDefault, heapDump.excludedRefs);
    //獲取分析結果
    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }
}

查看heapAnalyzer.checkForLeak代碼

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return AnalysisResult.failure(exception, since(analysisStartNanoTime));
    }

    ISnapshot snapshot = null;
    try {
      // 加載hprof文件
      snapshot = openSnapshot(heapDumpFile);
      //找到洩露對象
      IObject leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return AnalysisResult.noLeak(since(analysisStartNanoTime));
      }

      String className = leakingRef.getClazz().getName();
      // 最短引用路徑
      AnalysisResult result =
          findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
      //如果沒找到  嘗試排除系統進程干擾的情況下找出最短引用路徑
      if (!result.leakFound) {
        result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
      }

      return result;
    } catch (SnapshotException e) {
      return AnalysisResult.failure(e, since(analysisStartNanoTime));
    } finally {
      cleanup(heapDumpFile, snapshot);
    }
  }

到這裡,我們就找到了洩露對象的最短引用路徑,剩下的工作就是發送消息給通知,然後點擊通知欄跳轉到我們另一個App打開繪制出路徑即可。

補充—排除干擾項

但是我們在找出最短引用路徑的時候,有這樣一段代碼,他是干什麼的呢

 // 最短引用路徑
      AnalysisResult result =
          findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
      //如果沒找到  嘗試排除系統進程干擾的情況下找出最短引用路徑
      if (!result.leakFound) {
        result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);

查看findLeakTrace()

  private AnalysisResult findLeakTrace(long analysisStartNanoTime, ISnapshot snapshot,
      IObject leakingRef, String className, boolean excludingKnownLeaks) throws SnapshotException {

    ExcludedRefs excludedRefs = excludingKnownLeaks ? this.excludedRefs : baseExcludedRefs;

    PathsFromGCRootsTree gcRootsTree = shortestPathToGcRoots(snapshot, leakingRef, excludedRefs);

    // False alarm, no strong reference path to GC Roots.
    if (gcRootsTree == null) {
      return AnalysisResult.noLeak(since(analysisStartNanoTime));
    }

    LeakTrace leakTrace = buildLeakTrace(snapshot, gcRootsTree, excludedRefs);

    return AnalysisResult.leakDetected(!excludingKnownLeaks, className, leakTrace, since(analysisStartNanoTime));
  }

唯一的不同是excludingKnownLeaks 從字面意思也很好理解,是否排除已知內存洩露

其實是這樣的,在我們系統中本身就存在一些內存洩露的情況,這是上層App工程師無能為力的。但是如果是廠商或者做Android Framework層的工程師可能需要關心這個,於是做成一個參數配置的方式,讓我們靈活選擇豈不妙哉。當然,默認是會排除系統自帶洩露情況的,不然打開App,彈出一堆莫名其妙的內存洩露,我們還無能為力,著實讓人惶恐,而且我們還可以自己配置。
通過ExcludedRefs這個類

public final class ExcludedRefs implements Serializable {

  public final Map> excludeFieldMap;
  public final Map> excludeStaticFieldMap;
  public final Set excludedThreads;

  private ExcludedRefs(Map> excludeFieldMap,
      Map> excludeStaticFieldMap, Set excludedThreads) {
    // Copy + unmodifiable.
    this.excludeFieldMap = unmodifiableMap(new LinkedHashMap>(excludeFieldMap));
    this.excludeStaticFieldMap = unmodifiableMap(new LinkedHashMap>(excludeStaticFieldMap));
    this.excludedThreads = unmodifiableSet(new LinkedHashSet(excludedThreads));
  }

  public static final class Builder {
    private final Map> excludeFieldMap = new LinkedHashMap>();
    private final Map> excludeStaticFieldMap = new LinkedHashMap>();
    private final Set excludedThreads = new LinkedHashSet();

    public Builder instanceField(String className, String fieldName) {
        Preconditions.checkNotNull(className, "className");
      Preconditions.checkNotNull(fieldName, "fieldName");
      Set excludedFields = excludeFieldMap.get(className);
      if (excludedFields == null) {
        excludedFields = new LinkedHashSet();
        excludeFieldMap.put(className, excludedFields);
      }
      excludedFields.add(fieldName);
      return this;
    }

    public Builder staticField(String className, String fieldName) {
        Preconditions.checkNotNull(className, "className");
        Preconditions.checkNotNull(fieldName, "fieldName");
      Set excludedFields = excludeStaticFieldMap.get(className);
      if (excludedFields == null) {
        excludedFields = new LinkedHashSet();
        excludeStaticFieldMap.put(className, excludedFields);
      }
      excludedFields.add(fieldName);
      return this;
    }

    public Builder thread(String threadName) {
        Preconditions.checkNotNull(threadName, "threadName");
      excludedThreads.add(threadName);
      return this;
    }

    public ExcludedRefs build() {
      return new ExcludedRefs(excludeFieldMap, excludeStaticFieldMap, excludedThreads);
    }
  }
}

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved