Android LeakCanary的使用和原理

renxhui發表於2019-08-26

LeakCanary介紹

LeakCanary提供了一種很方便的方式,讓我們在開發階段測試記憶體洩露,我們不需要自己根據記憶體塊來分析記憶體洩露的原因,我們只需要在專案中整合他,然後他就會幫我們檢測記憶體洩露,並給出記憶體洩露的引用鏈

整合

  • 在gradle中新增依賴,這種不區分debug和release
    implementation 'com.squareup.leakcanary:leakcanary-android:1.5.1'
複製程式碼
  • 重寫Application
public class App extends Application {

    private RefWatcher mRefWatcher;

    @Override
    public void onCreate() {
        super.onCreate();

        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }

        mRefWatcher = LeakCanary.install(this);

    }


    public static RefWatcher getRefWatcher(Context context) {
        App application = (App) context.getApplicationContext();
        return application.mRefWatcher;
    }
}
複製程式碼
  • 比如我們在Activity中製造Handler發延遲訊息的記憶體洩露
public class ActivityOne extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_one);

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, 100000);
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}
複製程式碼
  • 然後我們開啟這個activity然後再關閉,桌面就會出現一個leaks的圖示,然後我們開啟它
    在這裡插入圖片描述
    這就是基本使用,其實這種形式只能監視Activity的記憶體洩露,至於為什麼只能監視Activity的記憶體洩露我們下面再分析,如果想要監視其他的記憶體洩露怎麼辦,比如要監視Fragment的記憶體洩露,可以這樣寫,主動監視想要監視的物件
public abstract class BaseFragment extends Fragment {
 
  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = App.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}
複製程式碼

原理概述

通過監聽Activity的onDestory,手動呼叫GC,然後通過ReferenceQueue+WeakReference,來判斷Activity物件是否被回收,然後結合dump Heap的hpof檔案,通過Haha開源庫分析洩露的位置

主要的知識點

註冊Activity的生命週期的監聽器

通過Application.registerActivityLifecycleCallbacks()方法註冊Activity的生命週期的監聽器,每一個Actvity的生命週期都會回撥到這個ActivityLifecycleCallbacks上,如果一個Activity走到了onDestory,那麼就意味著他就不再存在,然後檢測這個Activity是否是真的被銷燬

通過ReferenceQueue+WeakReference,來判斷物件是否被回收

WeakReference建立時,可以傳入一個ReferenceQueue物件,假如WeakReference中引用物件被回收,那麼就會把WeakReference物件新增到ReferenceQueue中,可以通過ReferenceQueue中是否為空來判斷,被引用物件是否被回收

詳細介紹推薦部落格:www.jianshu.com/p/964fbc301…

MessageQueue中加入一個IdleHandler來得到主執行緒空閒回撥

詳細介紹請看Android Handler 原始碼解析

手動呼叫GC後還呼叫了System.runFinalization();,這個是強制呼叫已失去引用物件的finalize方法

在可達性演算法中,不可達物件,也不是非死不可,這時他們處於“緩刑”階段,要宣告一個物件真正死亡需要至少倆個標記階段, 如果發現物件沒有引用鏈,則會進行第一次標記,並進行一次篩選,篩選的條件是此物件是否有必要進行finalize()方法,當物件沒有覆蓋finalize(),或者finalize()已經呼叫過,這倆種都視為“沒有必要執行”

Apolication中可通過processName判斷是否是任務執行程式

通過processName,來判斷程式

  public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
      packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
    } catch (Exception e) {
      CanaryLog.d(e, "Could not get package info for %s", context.getPackageName());
      return false;
    }
    String mainProcess = packageInfo.applicationInfo.processName;

    ComponentName component = new ComponentName(context, serviceClass);
    ServiceInfo serviceInfo;
    try {
      serviceInfo = packageManager.getServiceInfo(component, 0);
    } catch (PackageManager.NameNotFoundException ignored) {
      // Service is disabled.
      return false;
    }

    if (serviceInfo.processName.equals(mainProcess)) {
      CanaryLog.d("Did not expect service %s to run in main process %s", serviceClass, mainProcess);
      // Technically we are in the service process, but we're not in the service dedicated process.
      return false;
    }

    int myPid = android.os.Process.myPid();
    ActivityManager activityManager =
        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.RunningAppProcessInfo myProcess = null;
    List<ActivityManager.RunningAppProcessInfo> runningProcesses =
        activityManager.getRunningAppProcesses();
    if (runningProcesses != null) {
      for (ActivityManager.RunningAppProcessInfo process : runningProcesses) {
        if (process.pid == myPid) {
          myProcess = process;
          break;
        }
      }
    }
    if (myProcess == null) {
      CanaryLog.d("Could not find running process for %d", myPid);
      return false;
    }

    return myProcess.processName.equals(serviceInfo.processName);
  }
複製程式碼

原始碼分析

SDK初始化

mRefWatcher = LeakCanary.install(this);
複製程式碼

這個是SDK向外暴露的方法,我們以此為入口進行原始碼的分析

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

 public static AndroidRefWatcherBuilder refWatcher(Context context) {
    return new AndroidRefWatcherBuilder(context);
  }
複製程式碼

install方法首先初始化了一個AndroidRefWatcherBuilder類,然後通過listenerServiceClass方法設定了DisplayLeakService,這個類主要用於分析記憶體洩露的結果資訊,然後傳送通知給使用者

public final class AndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilder> {

  /**
   * Sets a custom {@link AbstractAnalysisResultService} to listen to analysis results. This
   * overrides any call to {@link #heapDumpListener(HeapDump.Listener)}.
   */
  public AndroidRefWatcherBuilder listenerServiceClass(
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
  }
  ...
  }

public class RefWatcherBuilder<T extends RefWatcherBuilder<T>> {
  ...
  /** @see HeapDump.Listener */
  public final T heapDumpListener(HeapDump.Listener heapDumpListener) {
    this.heapDumpListener = heapDumpListener;
    return self();
  }
  ...
  }
複製程式碼

然後呼叫excludedRefs方法新增白名單,在AndroidExcludedRefs列舉類中定義了忽略列表資訊,如果這些列表中的類發生了記憶體洩露,並不會顯示出來,同時HeapAnalyzer計算GCRoot強引用路徑,也會忽略這些類,如果你希望自己專案中某個類洩露的,但是不希望他顯示,就可以把類新增到這個上面

public enum AndroidExcludedRefs {

  // ######## Android SDK Excluded refs ########

  ACTIVITY_CLIENT_RECORD__NEXT_IDLE(SDK_INT >= KITKAT && SDK_INT <= LOLLIPOP) {
    @Override void add(ExcludedRefs.Builder excluded) {
      excluded.instanceField("android.app.ActivityThread$ActivityClientRecord", "nextIdle")
          .reason("Android AOSP sometimes keeps a reference to a destroyed activity as a"
              + " nextIdle client record in the android.app.ActivityThread.mActivities map."
              + " Not sure what's going on there, input welcome.");
    }
  }
  ...
  }
複製程式碼

最後呼叫了buildAndInstall方法,建立了一個RefWatcher物件並返回,這個物件是用於檢測是否有物件未被回收導致的記憶體洩露

  /**
   * Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+).
   */
  public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      LeakCanary.enableDisplayLeakActivity(context);
      ActivityRefWatcher.install((Application) context, refWatcher);
    }
    return refWatcher;
  }
複製程式碼

因為分析洩露是在另一個程式進行的,所以判斷當前啟動的Application是否在分析記憶體洩露的程式中,如果是就直接返回DISABLED,不在進行後續初始化,如果發現是在程式主程式中,就進行初始化

LeakCanary.enableDisplayLeakActivity(context);主要作用是呼叫PackageManagerDisplayLeakActivity設定為可用。

 public static void enableDisplayLeakActivity(Context context) {
    setEnabled(context, DisplayLeakActivity.class, true);
  }

  public static void setEnabled(Context context, final Class<?> componentClass,
      final boolean enabled) {
    final Context appContext = context.getApplicationContext();
    executeOnFileIoThread(new Runnable() {
      @Override public void run() {
        setEnabledBlocking(appContext, componentClass, enabled);
      }
    });
  }

  public static void setEnabledBlocking(Context appContext, Class<?> componentClass,
      boolean enabled) {
    ComponentName component = new ComponentName(appContext, componentClass);
    PackageManager packageManager = appContext.getPackageManager();
    int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
    // Blocks on IPC.
    packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
  }
複製程式碼

從配置檔案看LeakCanary這幾個檔案都是執行在新程式的,DisplayLeakActivity預設enable=false,這樣就可以一開始隱藏啟動圖示

<application>
    <service
        android:name=".internal.HeapAnalyzerService"
        android:process=":leakcanary"
        android:enabled="false"/>
    <service
        android:name=".DisplayLeakService"
        android:process=":leakcanary"
        android:enabled="false"/>
    <activity
        android:theme="@style/leak_canary_LeakCanary.Base"
        android:name=".internal.DisplayLeakActivity"
        android:process=":leakcanary"
        android:enabled="false"
        android:label="@string/leak_canary_display_activity_label"
        android:icon="@drawable/leak_canary_icon"
        android:taskAffinity="com.squareup.leakcanary.${applicationId}">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity
        android:theme="@style/leak_canary_Theme.Transparent"
        android:name=".internal.RequestStoragePermissionActivity"
        android:process=":leakcanary"
        android:taskAffinity="com.squareup.leakcanary.${applicationId}"
        android:enabled="false"
        android:excludeFromRecents="true"
        android:icon="@drawable/leak_canary_icon"
        android:label="@string/leak_canary_storage_permission_activity_label"/>
</application>
複製程式碼

接著 ActivityRefWatcher.install((Application) context, refWatcher);這裡把refWatcher當做引數傳入,同時對Activity的生命週期進行了監聽

  public static void install(Application application, RefWatcher refWatcher) {
    new ActivityRefWatcher(application, refWatcher).watchActivities();
  }


  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }
  

 public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }
  

 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);
  }

複製程式碼

首先就是註冊Activity的生命週期的監聽器lifecycleCallbacks,這個監聽器可以監聽專案中所有Activity的的生命週期,然後在Activity銷燬呼叫onActivityDestroyed的時候,LeakCanary就會獲取這個Activity,然後對其進行分析,看是否有記憶體洩露

分析記憶體洩露

這裡分析物件是否記憶體洩露的是RefWatcher類,下面簡單介紹一下這個類的成員變數

  • WatchExecutor watchExecutor:確保任務在主執行緒進行,同時預設延遲5s執行任務,留時間給系統GC
  • DebuggerControl debuggerControl:控制中心
  • GcTrigger gcTrigger:內部呼叫Runtime.getRuntime().gc(),手動觸發GC
  • HeapDumper heapDumper:用於建立.hprof檔案,用於儲存head堆快照,可以知道哪些程式在大部分使用記憶體
  • HeapDump.Listener heapdumpListener:分析結果完成後會告訴這個監聽器
  • ExcludedRefs excludedRefs:分析記憶體洩露的白名單

從上面可以看出,每當Activity銷燬,就會呼叫RefWatcherwatch方法,去分析是否是記憶體洩露

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

  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);
  }
複製程式碼

上面程式碼主要作用是,先生成一個隨機數key放在retainedKeys容器裡,用來區分物件是否被回收,建立了一個弱引用,然後把要分析的Activity物件存入,然後呼叫了ensureGoneAsync方法

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

然後用watchExecutor去排程分析任務,這個主要是保證,在主執行緒進行,延遲5s,讓系統有時間GC

  @SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
  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;
  }



  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);
    }
  }

  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }
複製程式碼

首先通過removeWeaklyReachableReferences()方法,嘗試從弱引用佇列獲取待分析物件,如果不為空說明被系統回收了,就把retainedKeys中的key值移除,如果被系統回收直接返回DONE,如果沒有被系統回收,就手動呼叫 gcTrigger.runGc();手動觸發系統gc,然後再次呼叫removeWeaklyReachableReferences()方法,如過還是為空,則判斷為記憶體洩露

 GcTrigger DEFAULT = new GcTrigger() {
    @Override public void runGc() {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every time. Runtime.gc() is
      // more likely to perfom a gc.
      Runtime.getRuntime().gc();
      enqueueReferences();
      System.runFinalization();
    }

    private void enqueueReferences() {
      // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
      // references to the appropriate queues.
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        throw new AssertionError();
      }
    }
  };
複製程式碼

手動觸發GC後,呼叫enqueueReferences方法沉睡100ms,給系統GC時間, System.runFinalization();,這個是強制呼叫已失去引用物件的finalize方法

確定有記憶體洩漏後,呼叫heapDumper.dumpHeap();生成.hprof檔案,然後回撥到heapdumpListener監聽,這個監聽實現是ServiceHeapDumpListener類,會調analyze()方法


public final class ServiceHeapDumpListener implements HeapDump.Listener {
...
  @Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }
}
複製程式碼

HeapDump是一個modle類,裡面用於儲存一些分析類強引用的需要資訊 HeapAnalyzerService.runAnalysis方法是傳送了一個intent,啟動了HeapAnalyzerService服務,這是一個intentService

 public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    context.startService(intent);
  }
複製程式碼

啟動服務後,會在onHandleIntent方法啟動分析,找到記憶體洩露的引用關係

  @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);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }
複製程式碼

找到引用關係

  • 啟動分析記憶體洩露的服務後,會例項化一個HeapAnalyzer物件,然後呼叫checkForLeak方法來分析最終得到的結果,
  • checkForLeak這裡用到了Square的另一個庫haha,哈哈哈哈哈,名字真的就是叫這個,開源地址:github.com/square/haha…
  • 得到結果後呼叫AbstractAnalysisResultService.sendResultToListener()方法,這個方法啟動了另一個服務

  public static void sendResultToListener(Context context, String listenerServiceClassName,
      HeapDump heapDump, AnalysisResult result) {
    Class<?> listenerServiceClass;
    try {
      listenerServiceClass = Class.forName(listenerServiceClassName);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
    Intent intent = new Intent(context, listenerServiceClass);
    intent.putExtra(HEAP_DUMP_EXTRA, heapDump);
    intent.putExtra(RESULT_EXTRA, result);
    context.startService(intent);
  }
複製程式碼

listenerServiceClassName就是開始LeakCanary.install方法傳入的DisplayLeakService,它本身也是一個intentService

@Override 
protected final void onHandleIntent(Intent intent) {
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAP_DUMP_EXTRA);
    AnalysisResult result = (AnalysisResult) intent.getSerializableExtra(RESULT_EXTRA);
    try {
      onHeapAnalyzed(heapDump, result);
    } finally {
      //noinspection ResultOfMethodCallIgnored
      heapDump.heapDumpFile.delete();
    }
  }
複製程式碼

然後呼叫自身的onHeapAnalyzed方法

protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
    String leakInfo = LeakCanary.leakInfo(this, heapDump, result, true);
    CanaryLog.d("%s", new Object[]{leakInfo});
    boolean resultSaved = false;
    boolean shouldSaveResult = result.leakFound || result.failure != null;
    if(shouldSaveResult) {
        heapDump = this.renameHeapdump(heapDump);
        resultSaved = this.saveResult(heapDump, result);
    }

    PendingIntent pendingIntent;
    String contentTitle;
    String contentText;

    // 設定訊息通知的 pendingIntent/contentTitle/contentText
    ...

    int notificationId1 = (int)(SystemClock.uptimeMillis() / 1000L);
    LeakCanaryInternals.showNotification(this, contentTitle, contentText, pendingIntent, notificationId1);
    this.afterDefaultHandling(heapDump, result, leakInfo);
}
複製程式碼

這個方法首先判斷是否需要把資訊存到本地,如果需要就存到本地,然後設定訊息通知的基本資訊,最後通過LeakCanaryInternals.showNotification方法呼叫系統的系統通知欄,告訴使用者有記憶體洩露

至此LeakCanary的檢測記憶體洩露原始碼,已經分析完了

參考:blog.csdn.net/xiaohanluo/…

juejin.im/post/5ab8d3…

相關文章