Leakcanary原理淺析


LeakCanary是Android內存泄漏的框架,作為一個“面試常見問題”,它一定有值得學習的地方,今天我們就講一下它。作為一名開發,我覺得給人講框架或者庫的原理,最好先把大概思路給讀者講一下,這樣讀者后面會按照這個框架往里填內容,理解起來也更容易一些,所以我先把LeakCanary的大致原理放出來:

其思路大致為:監聽Activity生命周期->onDestroy以后延遲5秒判斷Activity有沒有被回收->如果沒有回收,調用GC,再此判斷是否回收,如果還沒回收,則內存泄露了,反之,沒有泄露。整個框架最核心的問題就是在什么時間點如何判斷一個Activity是否被回收了。

下面開始按這個思路分析源碼,直接從入口開始:

public static RefWatcher install(Application application) {
  return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}

builder模式構建了一個RefWatcher對象,listenerServiceClass()方法綁定了一個后台服務DisplayLeakService

這個服務主要用來分析內存泄漏結果並發送通知。你可以繼承並重寫這個類來進行一些自定義操作,比如上傳分析結果等。

我們看最后buildAndInstall()方法:

  public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      LeakCanary.enableDisplayLeakActivity(context);
      ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);
    }
    return refWatcher;
  }

build()方法,這個方法主要是配置一些東西,先大概了解一下,后面用到再說,下面是幾個配置項目。

watchExecutor : 線程控制器,在 onDestroy()之后並且主線程空閑時執行內存泄漏檢測

debuggerControl: 判斷是否處於調試模式,調試模式中不會進行內存泄漏檢測

gcTrigger: 用於GC

watchExecutor首次檢測到可能的內存泄漏,會主動進行GC,GC之后會再檢測一次,仍然泄漏的判定為內存泄漏,進行后續操作

heapDumper: dump內存泄漏處的heap信息,寫入hprof文件

heapDumpListener: 解析完hprof文件並通知DisplayLeakService彈出提醒

excludedRefs: 排除可以忽略的泄漏路徑

LeakCanary.enableDisplayLeakActivity(context)

這行代碼主要是為了開啟LeakCanary的應用,顯示其圖標.

接下來是重點:

ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher)

它會進入:

    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    activityRefWatcher.watchActivities();

接下來:

    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);

第一行代碼是為了確保不會重復綁定,第二行綁定生命周期,之后監聽Activity的生命周期。

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

監聽到Activity銷毀時執行onActivityDestroyed方法,進入看看:

public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

整個LeakCanary最核心的思路就在這兒了。

前面幾行是這樣的,根據Activity生成一個隨機Key,並將Key加入到一個Set中,然后講key,activity傳如一個包裝的弱引用里。

這里引出了第一個知識點,弱引用和引用隊列ReferenceQueue聯合使用時,如果弱引用持有的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。即 KeyedWeakReference持有的Activity對象如果被垃圾回收,該對象就會加入到引用隊列queue,我們看看RefreceQueue的javadoc:

/**
 * Reference queues, to which registered reference objects are appended by the
 * garbage collector after the appropriate reachability changes are detected.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */
public class ReferenceQueue<T>

證實了上面的說法,另外看名字我們就知道,不光弱引用,軟和虛引用也可以這樣做。

重點是最后一句:ensureGoneAsyc,看字面意思,異步確保消失。這里我們先不看代碼,如果要自己設計一套檢測方案的話,怎么想?其實很簡單,就是在Activiy onDestroy以后,我們等一會,檢測一下這個Acitivity有沒有被回收,那么問題來了,什么時候檢測?怎么檢測?這也是本框架的核心和難點。

LeakCanary是這么做的:onDestroy以后,一旦主線程空閑下來,延時5秒執行一個任務:先判斷Activity有沒有被回收?如果已經回收了,說明沒有內存泄漏,如果還沒回收,我們進一步確認,手動觸發一下gc,然后再判斷有沒有回收,如果這次還沒回收,說明Activity確實泄漏了,接下來把泄漏的信息展示給開發者就好了。

思路其實挺清晰的,我們看代碼實現:

  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

這里watchExecutor是AndroidWatchExecutor,看代碼:

  @Override public void execute(Retryable retryable) {
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      waitForIdle(retryable, 0);
    } else {
      postWaitForIdle(retryable, 0);
    }
  }

主線程和子線程其實一樣,都要到主線程中執行,

  void waitForIdle(final Retryable retryable, final int failedAttempts) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        postToBackgroundWithDelay(retryable, failedAttempts);
        return false;
      }
    });
  }

這里有第二個知識點,IdleHandler,這個東西是干嘛的,其實看名字就知道了,就是當主線程空閑的時候,如果設置了這個東西,就會執行它的queueIdle()方法,所以這個方法就是在onDestory以后,一旦主線程空閑了,就會執行,然后我們看它執行了啥:

  private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
    long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
    long delayMillis = initialDelayMillis * exponentialBackoffFactor;
    backgroundHandler.postDelayed(new Runnable() {
      @Override public void run() {
        Retryable.Result result = retryable.run();
        if (result == RETRY) {
          postWaitForIdle(retryable, failedAttempts + 1);
        }
      }
    }, delayMillis);
  }
}

很簡單,延時5秒執行retryable的run(),注意,因為這里是backgroundHandler post出來的,所以是下面的run是在子線程執行的。這里的retryable就是前面傳過來的:

  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

ensureGone(reference,watchStartNanoTime),在看它干了啥之前,我們先理一下思路,前面onDestory以后,AndroidWatchExecutor這個東西執行excute方法,這個方法讓主線程在空閑的時候發送了一個延時任務,該任務會在5秒延時后在一個子線程執行。理清了思路,我們看看這個任務是怎么執行的。

 Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {
      return DONE;
    }
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

前面我們說過思路了,5秒延遲后先看看有沒有回收,如果回收了,直接返回,沒有發生內存泄漏,如果沒有回收,觸發GC,gc完成后,在此判斷有沒有回收,如果還沒回收,說明泄漏了,收集泄漏信息,展示給開發者。而上面的代碼完全按照這個思路來的。其中,removeWeaklyRechableReferences()和gone(reference)這兩個方法配合,用來判斷對象是否被回收了,看代碼:

  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知道:被回收的對象都會放到設置的引用隊列queue中,我們從queue中拿出所有的ref,根據他們的key匹配retainedKeys集合中的元素並刪除。然后在gone()函數里面判斷key是否被移除.

  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

這個方法挺巧妙的,retainedKeys集合了所有destoryed了的但沒有被回收的Activity的key,這個集合可以用來判斷一個Activity有沒有被回收,但是判斷之前需要用removeWeaklyReachableReferences()這個方法更新一下。

一旦一個Activity檢測出泄漏了,就收集泄漏信息然后通過前面配置的DisplayLeakService通知給用戶並展示在DisplayLeakActivity中,后面的東西都是UI展示東西,就不是本文的重點了,有興趣的可以自己查看。

稍微總結一下,我覺得這個框架中用到的一個很重要但冷門技巧就是弱引用的構造方法:傳入一個RefrenceQueue,可以記錄被垃圾回收的對象引用。說個題外話,一個對象都被回收了,他的弱引用咋辦,總不能一直留着吧,(引用本身也是一個強引用對象,不要把引用和引用的對象搞混了,對象可以被回收了,但是它的引用,包括軟,弱,虛引用都可以繼續存在)。完全不用擔心,這個引用在無用之后也會被GC回收的。

以上就是所有內容了,可以看出來LeakCanary其實算是個比較簡單的庫了~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM