[Android組件解讀] LeakCanary詳解

前言

LeakCanary是Square公司提供的用于Android檢測內(nèi)存的小工具,他能幫助我們快速定位代碼隱藏的BUG,減少OOM的機(jī)會(huì)抛寝。

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

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

OKHttp 一個(gè)開源穩(wěn)定的Http的通信依賴庫蟆肆,感覺比HttpUrlConnection好用,
    okhttp現(xiàn)在已經(jīng)得到Google官方的認(rèn)可了晦款。
    
okie  OKHttp依賴這個(gè)庫

dagger 快速依賴注入框架⊙坠Γ現(xiàn)在已經(jīng)由google公司維護(hù)了,
現(xiàn)在應(yīng)該是dagger2.官網(wǎng)地址:https://google.github.io/dagger/

picasso 一個(gè)圖片緩存庫缓溅,可以實(shí)現(xiàn)圖片的下載和緩存功能

retrofit 是一個(gè)RESTFUL(自行百度)的Http網(wǎng)絡(luò)請求框架的封裝蛇损,基于OKHttp,retrofit在于對接口的封裝,實(shí)質(zhì)是使用OKHttp進(jìn)行網(wǎng)絡(luò)請求

leakcanary 一個(gè)檢測內(nèi)存的小工具淤齐,本文說的就是這個(gè)

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

...

回到正文更啄,現(xiàn)在開始講解LeakCanary的使用

使用LeakCanary

其實(shí)可以參考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中添加內(nèi)存泄漏代碼稚疹,本文就參照sample中的的例子,寫了一個(gè)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. 運(yùn)行祭务,會(huì)發(fā)現(xiàn)出現(xiàn)一個(gè)LeakCanary的圖標(biāo)内狗。后續(xù)會(huì)介紹這個(gè)圖標(biāo)是如何出現(xiàn)的,當(dāng)出現(xiàn)內(nèi)存泄漏的時(shí)候义锥,會(huì)在通知欄顯示一條內(nèi)存泄漏通知柳沙,點(diǎn)擊通知會(huì)進(jìn)入內(nèi)存泄漏的具體問題。


    LeakCanary圖標(biāo)
通知欄顯示內(nèi)存泄漏
內(nèi)存泄漏詳情

根據(jù)圖標(biāo)顯示我們能夠看出內(nèi)存泄漏在AsyncTask中拌倍,根據(jù)AsyncTask中進(jìn)行內(nèi)存修改

講解完如何使用赂鲤,現(xiàn)在開始講解LeakCanary。

LeakCanary詳解

代碼目錄結(jié)構(gòu)

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

對外方法LeakCanary.install(this)

實(shí)際上LeakCanary對外提供的方法只有

LeakCanary.install(this);

從這里開始切入缴允,對應(yīng)源碼

/**
 * 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進(jìn)程
    if (isInAnalyzerProcess(application)) {
        return RefWatcher.DISABLED;
    }
    // 允許顯示內(nèi)存泄漏情況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>通過注釋能看出這個(gè)LeakCanary是用于4.0以上的方法

references (on ICS+).

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

ActivityRefWatcher.installOnIcsPlus(application, refWatcher);

這句方法告訴我們這個(gè)是用在Ics+(即4.0版本以上),那這個(gè)類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);這個(gè)方法使用在Android4.0上的练般,用于觀察Activity的生命周期矗漾。從上面代碼看出,LeakCanary監(jiān)聽Activity的銷毀操作

ActivityRefWatcher.this.onActivityDestroyed(activity);

LeakCanary如何出現(xiàn)LeakCanry的圖標(biāo)

public static void setEnabled(Context context, final Class<?> componentClass,
                                  final boolean enabled) {
        final Context appContext = context.getApplicationContext();
        // 耗時(shí)操作
        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的方法執(zhí)行的時(shí)候調(diào)用了

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

這個(gè)方法執(zhí)行了上面顯示的方法setEnable.最核心的方法是packageManager.setComponentEnabledSetting薄料。
這個(gè)方法可以用來隱藏/顯示應(yīng)用圖標(biāo)
具體可以參照android 禁用或開啟四大組件setComponentEnabledSetting

[重點(diǎn)]LeakCanary如何捕獲內(nèi)存泄漏

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

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

檢測時(shí)機(jī)

在Activity銷毀的時(shí)候會(huì)執(zhí)行RefWatch.watch方法,然后就去執(zhí)行內(nèi)存檢測

這里又看到一個(gè)比較少的用法摄职,IdleHandler誊役,IdleHandler的原理就是在messageQueue因?yàn)榭臻e等待消息時(shí)給使用者一個(gè)hook。那AndroidWatchExecutor會(huì)在主線程空閑的時(shí)候谷市,派發(fā)一個(gè)后臺(tái)任務(wù)蛔垢,這個(gè)后臺(tái)任務(wù)會(huì)在DELAY_MILLIS時(shí)間之后執(zhí)行。LeakCanary設(shè)置的是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的內(nèi)存的話鹏漆,可以在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開源項(xiàng)目

其他參考資料

LeakCanary 內(nèi)存泄露監(jiān)測原理研究

Android 內(nèi)存泄漏檢查工具LeakCanary源碼淺析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末艺玲,一起剝皮案震驚了整個(gè)濱河市括蝠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饭聚,老刑警劉巖忌警,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異若治,居然都是意外死亡慨蓝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門端幼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來礼烈,“玉大人,你說我怎么就攤上這事婆跑〈税荆” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵滑进,是天一觀的道長犀忱。 經(jīng)常有香客問我,道長扶关,這世上最難降的妖魔是什么阴汇? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮节槐,結(jié)果婚禮上搀庶,老公的妹妹穿的比我還像新娘。我一直安慰自己铜异,他們只是感情好哥倔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揍庄,像睡著了一般咆蒿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蚂子,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天沃测,我揣著相機(jī)與錄音,去河邊找鬼食茎。 笑死芽突,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的董瞻。 我是一名探鬼主播寞蚌,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼田巴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了挟秤?” 一聲冷哼從身側(cè)響起壹哺,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎艘刚,沒想到半個(gè)月后管宵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攀甚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年箩朴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秋度。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炸庞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出荚斯,到底是詐尸還是另有隱情埠居,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布事期,位于F島的核電站滥壕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏兽泣。R本人自食惡果不足惜绎橘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唠倦。 院中可真熱鬧金踪,春花似錦、人聲如沸牵敷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枷餐。三九已至,卻和暖如春苫亦,著一層夾襖步出監(jiān)牢的瞬間毛肋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國打工屋剑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留润匙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓唉匾,卻偏偏與公主長得像孕讳,于是被迫代替她去往敵國和親谷羞。 傳聞我的和親對象是個(gè)殘疾皇子亚茬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,128評(píng)論 25 707
  • 框架:提供一定能力的小段程序 http://www.cnblogs.com/jincheng-yangchaofa...
    姑娘請別為難小僧閱讀 7,231評(píng)論 0 132
  • 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡單粗俗的講冕广,...
    宇宙只有巴掌大閱讀 2,363評(píng)論 0 12
  • 一.榜單介紹 排行榜包括四大類: 單一框架:僅提供路由、網(wǎng)絡(luò)層胖笛、UI層苍柏、通信層或其他單一功能的框架 混合開發(fā)框架:...
    偉子男閱讀 5,243評(píng)論 0 161
  • 愛情和毒品都會(huì)讓人上癮,不同的是荚恶,你明知道毒品會(huì)帶來傷害卻還是忍不住撩穿,而愛情往往等你上了癮之后才發(fā)現(xiàn)自己已經(jīng)遍體鱗...
    暖心奶茶閱讀 156評(píng)論 0 0