[Android組件解讀] LeakCanary詳解

前言

LeakCanary是Square公司提供的用于Android檢測內存的小工具,他能幫助我們快速定位代碼隱藏的BUG,減少OOM的機會。

此處為git地址鏈接:https://github.com/square/leakcanary

題外話:Square真的是家良心公司,提供了很多有名的組件。后續會整理目前市面上有名的組件。比如Facebook的開源組件... 現在先介紹下Square有哪些開源組件

OKHttp 一個開源穩定的Http的通信依賴庫,感覺比HttpUrlConnection好用,
    okhttp現在已經得到Google官方的認可了。
    
okie  OKHttp依賴這個庫

dagger 快速依賴注入框架。現在已經由google公司維護了,
現在應該是dagger2.官網地址:https://google.github.io/dagger/

picasso 一個圖片緩存庫,可以實現圖片的下載和緩存功能

retrofit 是一個RESTFUL(自行百度)的Http網絡請求框架的封裝,基于OKHttp,retrofit在于對接口的封裝,實質是使用OKHttp進行網絡請求

leakcanary 一個檢測內存的小工具,本文說的就是這個

otto Android事件總線,降低代碼的耦合性,可以跟EventBus做比較

...

回到正文,現在開始講解LeakCanary的使用

使用LeakCanary

其實可以參考leakcanary的Sample介紹

  1. 首先在build.gradle中引用
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.2.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha9'
    testCompile 'junit:junit:4.12'

    // LeakCanary
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
  1. 在Application的onCreate添加方法
    public class ExampleApp extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        // LeakCanary初始化
        LeakCanary.install(this);
    }
}
  1. 在App中添加內存泄漏代碼,本文就參照sample中的的例子,寫了一個SystemClock.sleep(20000);
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        Button asynTaskBtn = (Button) this.findViewById(R.id.async_task);
        asynTaskBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startAsyncTask();
            }
        });
    }

    private void startAsyncTask() {
        // This async task is an anonymous class and therefore has a hidden reference to the outer
        // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
        // the activity instance will leak.
        new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
                // Do some slow work in background
                SystemClock.sleep(20000);
                return null;

            }
        }.execute();
    }
}
  1. 運行,會發現出現一個LeakCanary的圖標。后續會介紹這個圖標是如何出現的,當出現內存泄漏的時候,會在通知欄顯示一條內存泄漏通知,點擊通知會進入內存泄漏的具體問題。


    LeakCanary圖標
通知欄顯示內存泄漏
內存泄漏詳情

根據圖標顯示我們能夠看出內存泄漏在AsyncTask中,根據AsyncTask中進行內存修改

講解完如何使用,現在開始講解LeakCanary。

LeakCanary詳解

代碼目錄結構

.
├── AbstractAnalysisResultService.java 
├── ActivityRefWatcher.java -- Activity監控者,監控其生命周期
├── AndroidDebuggerControl.java --Android Debug控制開關,就是判斷Debug.isDebuggerConnected()
├── AndroidExcludedRefs.java -- 內存泄漏基類
├── AndroidHeapDumper.java --生成.hrpof的類
├── AndroidWatchExecutor.java -- Android監控線程,延遲5s執行
├── DisplayLeakService.java -- 顯示通知欄的內存泄漏,實現了AbstractAnalysisResultService.java
├── LeakCanary.java --對外類,提供install(this)方法
├── ServiceHeapDumpListener.java 
└── internal --這個文件夾用于顯示內存泄漏的情況(界面相關)
    ├── DisplayLeakActivity.java --內存泄漏展示的Activity
    ├── DisplayLeakAdapter.java 
    ├── DisplayLeakConnectorView.java 
    ├── FutureResult.java
    ├── HeapAnalyzerService.java 在另一個進程啟動的Service,用于接收數據并發送數據到界面
    ├── LeakCanaryInternals.java
    ├── LeakCanaryUi.java
    └── MoreDetailsView.java

對外方法LeakCanary.install(this)

實際上LeakCanary對外提供的方法只有

LeakCanary.install(this);

從這里開始切入,對應源碼

/**
 * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
 * references (on ICS+).
 */
public static RefWatcher install(Application application) {
    return install(application, DisplayLeakService.class,
            AndroidExcludedRefs.createAppDefaults().build());
}

/**
 * Creates a {@link RefWatcher} that reports results to the provided service, and starts watching
 * activity references (on ICS+).
 */
public static RefWatcher install(Application application,
                                 Class<? extends AbstractAnalysisResultService> listenerServiceClass,
                                 ExcludedRefs excludedRefs) {
    // 判斷是否在Analyzer進程
    if (isInAnalyzerProcess(application)) {
        return RefWatcher.DISABLED;
    }
    // 允許顯示內存泄漏情況Activity
    enableDisplayLeakActivity(application);

    HeapDump.Listener heapDumpListener =
            new ServiceHeapDumpListener(application, listenerServiceClass);

    RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
    ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
    return refWatcher;
}

為什么LeakCanary要求在4.0以上

<mark>通過注釋能看出這個LeakCanary是用于4.0以上的方法

references (on ICS+).

為什么要使用在4.0以上呢?

ActivityRefWatcher.installOnIcsPlus(application, refWatcher);

這句方法告訴我們這個是用在Ics+(即4.0版本以上),那這個類ActivityRefWatcher具體使用來干什么的呢

@TargetApi(ICE_CREAM_SANDWICH) public final class ActivityRefWatcher {

  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    if (SDK_INT < ICE_CREAM_SANDWICH) {
      // If you need to support Android < ICS, override onDestroy() in your base activity.
      return;
    }
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    activityRefWatcher.watchActivities();
  }

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

  private final Application application;
  private final RefWatcher refWatcher;

  /**
   * Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking
   * after they have been destroyed.
   */
  public ActivityRefWatcher(Application application, final RefWatcher refWatcher) {
    this.application = checkNotNull(application, "application");
    this.refWatcher = checkNotNull(refWatcher, "refWatcher");
  }

  void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity);
  }

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

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

application.registerActivityLifecycleCallbacks(lifecycleCallbacks);這個方法使用在Android4.0上的,用于觀察Activity的生命周期。從上面代碼看出,LeakCanary監聽Activity的銷毀操作

ActivityRefWatcher.this.onActivityDestroyed(activity);

LeakCanary如何出現LeakCanry的圖標

public static void setEnabled(Context context, final Class<?> componentClass,
                                  final boolean enabled) {
        final Context appContext = context.getApplicationContext();
        // 耗時操作
        executeOnFileIoThread(new Runnable() {
            @Override
            public void run() {
                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);
            }
        });
    }

在install的方法執行的時候調用了

 // 允許顯示內存泄漏情況Activity
enableDisplayLeakActivity(application);

這個方法執行了上面顯示的方法setEnable.最核心的方法是packageManager.setComponentEnabledSetting。
這個方法可以用來隱藏/顯示應用圖標
具體可以參照android 禁用或開啟四大組件setComponentEnabledSetting

[重點]LeakCanary如何捕獲內存泄漏

通過Debug.dumpHprofData()方法生成.hprof文件,然后利用開源庫HAHA(開源地址:https://github.com/square/haha)解析.hprof文件,并發送給DisplayLeakActivity進行展示

public final class AndroidHeapDumper implements HeapDumper {

  private static final String TAG = "AndroidHeapDumper";

  private final Context context;
  private final Handler mainHandler;

  public AndroidHeapDumper(Context context) {
    this.context = context.getApplicationContext();
    mainHandler = new Handler(Looper.getMainLooper());
  }

  @Override public File dumpHeap() {
    if (!isExternalStorageWritable()) {
      Log.d(TAG, "Could not dump heap, external storage not mounted.");
    }
    File heapDumpFile = getHeapDumpFile();
    if (heapDumpFile.exists()) {
      Log.d(TAG, "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<>();
    showToast(waitingForToast);

    if (!waitingForToast.wait(5, SECONDS)) {
      Log.d(TAG, "Did not dump heap, too much time waiting for Toast.");
      return NO_DUMP;
    }

    Toast toast = waitingForToast.get();
    try {
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      cancelToast(toast);
      return heapDumpFile;
    } catch (IOException e) {
      cleanup();
      Log.e(TAG, "Could not perform heap dump", e);
      // Abort heap dump
      return NO_DUMP;
    }
  }

  /**
   * Call this on app startup to clean up all heap dump files that had not been handled yet when
   * the app process was killed.
   */
  public void cleanup() {
    LeakCanaryInternals.executeOnFileIoThread(new Runnable() {
      @Override public void run() {
        if (isExternalStorageWritable()) {
          Log.d(TAG, "Could not attempt cleanup, external storage not mounted.");
        }
        File heapDumpFile = getHeapDumpFile();
        if (heapDumpFile.exists()) {
          Log.d(TAG, "Previous analysis did not complete correctly, cleaning: " + heapDumpFile);
          heapDumpFile.delete();
        }
      }
    });
  }

  private File getHeapDumpFile() {
    return new File(storageDirectory(), "suspected_leak_heapdump.hprof");
  }

  private void showToast(final FutureResult<Toast> waitingForToast) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        final Toast toast = new Toast(context);
        toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
        toast.setDuration(Toast.LENGTH_LONG);
        LayoutInflater inflater = LayoutInflater.from(context);
        toast.setView(inflater.inflate(R.layout.leak_canary_heap_dump_toast, null));
        toast.show();
        // Waiting for Idle to make sure Toast gets rendered.
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
          @Override public boolean queueIdle() {
            waitingForToast.set(toast);
            return false;
          }
        });
      }
    });
  }

  private void cancelToast(final Toast toast) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        toast.cancel();
      }
    });
  }
}

檢測時機

在Activity銷毀的時候會執行RefWatch.watch方法,然后就去執行內存檢測

這里又看到一個比較少的用法,IdleHandler,IdleHandler的原理就是在messageQueue因為空閑等待消息時給使用者一個hook。那AndroidWatchExecutor會在主線程空閑的時候,派發一個后臺任務,這個后臺任務會在DELAY_MILLIS時間之后執行。LeakCanary設置的是5秒。

public void watch(Object watchedReference, String referenceName) {
    checkNotNull(watchedReference, "watchedReference");
    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);
      }
    });
  }
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;
      }
    });
  }
}

Fragment如何使用LeakCanary

如果我們想檢測Fragment的內存的話,可以在Application中將返回的RefWatcher存下來,可以在Fragment的onDestroy中watch它。

public abstract class BaseFragment extends Fragment {
 
  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}

參考LeakCanary開源項目

其他參考資料

LeakCanary 內存泄露監測原理研究

Android 內存泄漏檢查工具LeakCanary源碼淺析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容