LeakCanary-隱藏Icon、Toast、Notify

在Android中,要檢測App的內(nèi)存泄漏,眾所周知有個(gè)Square公司開源神器——LeakCanary。
LeakCanary的使用方便簡單,使用只需要3行代碼即可:
1)在build.gradle文件中,添加依賴(版本號(hào)可自行選擇):

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

2)在Application中,執(zhí)行:

RefWatcher mRefWatcher = LeakCanary.install(this);

mRefWatcher可以用于檢測你想檢測的內(nèi)容,比如用于檢測Fragment。
LeakCanary的更多具體使用方法,網(wǎng)上有很多詳細(xì)的內(nèi)容,可以自行搜索查看。
大家應(yīng)該知道使用LeakCanary后,設(shè)備上會(huì)有一個(gè)Leak的Icon,發(fā)現(xiàn)泄漏后,會(huì)出現(xiàn)一個(gè)Toast提示,并在通知欄中會(huì)展示一個(gè)Leak的Notify信息,但是由于某些原因,我們需要隱藏掉這些外露的信息,目標(biāo)是:可以在debug和release包中都能檢測內(nèi)存泄漏,發(fā)現(xiàn)泄漏后,可以做到獲取泄漏信息時(shí),用戶是無感知的。

解決問題一:希望在debug和release包中都能檢測內(nèi)存泄漏

雖然LeakCanary提供了release的版本,但是release版本為了App的性能,會(huì)跳過檢查,所以LeakCanary的內(nèi)存泄漏檢測是在Debug包中才能產(chǎn)生效果。
在build.gradle中,引入的2行代碼,分別代表,在debug版本中,引入leakcanary-android:1.5.4,在release版本中,引入leakcanary-android-no-op:1.5.4,所以要想實(shí)現(xiàn)想要的效果,只需要將兩行代碼縮減并修改成一行:

compile 'com.squareup.leakcanary:leakcanary-android:1.5.4'

也就是不再區(qū)分debug版本和release版本,直接引入LeakCanary用于檢測內(nèi)存泄漏的版本,Over!(注意:有可能導(dǎo)致App的性能變差,需要額外關(guān)注)

解決問題二:希望能夠隱藏Leak的Icon

想要隱藏Leak的Icon,首先要知道Icon是怎么來的。
首先,LeakCanary的使用手冊中,有告訴我們,如果需要更換Leak的Icon,可以替換圖標(biāo)文件:

res/
  drawable-hdpi/
    __leak_canary_icon.png
  drawable-mdpi/
    __leak_canary_icon.png
  drawable-xhdpi/
    __leak_canary_icon.png
  drawable-xxhdpi/
    __leak_canary_icon.png
  drawable-xxxhdpi/
    __leak_canary_icon.png
 
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="__leak_canary_display_activity_label">MyLeaks</string>
</resources>

但是可惜,我們要的不是替換Leak的Icon,而是直接隱藏Leak的Icon。

在網(wǎng)上查閱資料,看到有位大神提供的建議,將DisplayLeakActivity隱藏,鏈接:https://gist.github.com/lennykano/2bb061c9cff85b225590,無法翻墻的小伙伴請看下面這部分代碼:

<activity
 
    android:enabled="false"
 
    android:icon="@drawable/leak_canary_icon"
 
    android:label="@string/__leak_canary_display_activity_label"
 
    android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
 
    android:taskAffinity="com.squareup.leakcanary"
 
    android:theme="@style/__LeakCanary.Base">
 
    <intent-filter tools:node="remove">
 
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
 
    </intent-filter>
 
</activity>

實(shí)踐后,發(fā)現(xiàn)這部分的代碼確實(shí)可以讓App找不到DisplayLeakActivity,所以也確實(shí)可以隱藏Icon,但是正是由于App需要DisplayLeakActivity,卻又找不到它,所以引發(fā)了Crash問題,報(bào)錯(cuò)就是找不到DisplayLeakActivity。所以該方法不可行。
走投無路后,將LeakCanary的代碼down下來,希望能在源碼中,找到隱藏Icon的方法。
首先想到,既然有大神提供了在Mainfest.xml中,隱藏DisplayLeakActivity,那么在源碼的這個(gè)文件下,就一定有對(duì)這個(gè)Activity的某些定義:

<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="@mipmap/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的定義中,定義了它的Icon,也定義了它的label,這就是Leak Icon的由來,同時(shí)Activity是在leakcanary進(jìn)程中(不在我們的App進(jìn)程中),所以展示不受影響。
既然我們希望可以隱藏Icon,所以最直接的方法,就是通過 tools:node="remove" 方法,移除掉Activity的定義,從而達(dá)到隱藏Activity的目的,也就是上面大神提供的那個(gè)方法,然而并不可行。
所以只能往它的上一步查找:屌用Activity的地方,可以想象,如果我們將所有屌用Activity的部分注釋掉,那么也可以達(dá)到我們想要的效果。
查找后,發(fā)現(xiàn)整份源碼中,只有2個(gè)部分屌用到了DisplayLeakActivity,并且它的入口都在同一份java文件中(這是非常幸運(yùn)的一件事情,感謝Square公司大神們的代碼架構(gòu)非常好):

public final class LeakCanary {
    ...
    public static void enableDisplayLeakActivity(Context context) {
        setEnabled(context, DisplayLeakActivity.class, true);
    }
    ...
    /**
    * If you build a {@link RefWatcher} with a {@link AndroidHeapDumper} that has a custom {@link
    * LeakDirectoryProvider}, then you should also call this method to make sure the activity in
    * charge of displaying leaks can find those on the file system.
    */
    public static void setDisplayLeakActivityDirectoryProvider(LeakDirectoryProvider leakDirectoryProvider) {
        DisplayLeakActivity.setLeakDirectoryProvider(leakDirectoryProvider);
    }
    ...
}

所以自然而然的,冒出來的第一個(gè)想法就是:繼承LeakCanary,修改這兩部分代碼。但是可以看到,LeakCanary類是final類型,無法繼承,所以只能放棄繼承的想法。
但是我們可以重寫一個(gè)MyLeakCanary,內(nèi)容和LeakCanary一樣,在MyLeakCanary中,修改這兩部分的代碼,在外部屌用LeakCanary.install(this);的部分,修改成MyLeakCanary.install(this);,似乎也是可以達(dá)到我們想要的效果。
所以重新建立一個(gè)com.squareup.leakcanary包名,新建一個(gè)LeakCanaryWithoutDisplay類,將LeakCanary的內(nèi)容全部復(fù)制過來,按照我們想要的修改,所以修改后變成:

public final class LeakCanaryWithoutDisplay {
 
    
    public interface LeakCanaryCallBack {
        void onAnalysisResult(String result);
    }

    private static LeakCanaryCallBack sLeakCanaryCallBack;

    public static LeakCanaryCallBack getLeakCanaryCallBack() {
        return sLeakCanaryCallBack;
    }
    /**
     * Builder to create a customized {@link RefWatcher} with appropriate Android defaults.
     */
    public static AndroidRefWatcherBuilderWithoutToast refWatcher(Context context) {
        return new AndroidRefWatcherBuilderWithoutToast(context);
    }

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

    private LeakCanaryWithoutDisplay() {
        throw new AssertionError();
    }
    ...
}

而setDisplayLeakActivityDirectoryProvider方法,是在AndroidRefWatcherBuilder文件中屌用的。
所以,新建一個(gè)MyAndroidRefWatcherBuilder,將AndroidRefWatcherBuilder的內(nèi)容全部復(fù)制過來,修改:

public final class MyAndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilderWithoutToast> {
    ...
        /**
     * Sets the maximum number of heap dumps stored. This overrides any call to {@link
     * #heapDumper(HeapDumper)} as well as any call to
     * {@link LeakCanary#setDisplayLeakActivityDirectoryProvider(LeakDirectoryProvider)})}
     *
     * @throws IllegalArgumentException if maxStoredHeapDumps < 1.
     */
    public AndroidRefWatcherBuilderWithoutToast maxStoredHeapDumps(int maxStoredHeapDumps) {
        LeakDirectoryProvider leakDirectoryProvider =
                new DefaultLeakDirectoryProvider(context, maxStoredHeapDumps);
//        LeakCanary.setDisplayLeakActivityDirectoryProvider(leakDirectoryProvider);//將這行注釋掉,不再屌用即可
        return heapDumper(new AndroidHeapDumperWithoutToast(context, leakDirectoryProvider));
    }
    ...
}

至此,得益于Square公司大神們優(yōu)秀的代碼架構(gòu),我們將2個(gè)文件重新定義一份后,在屌用的地方,將LeakCanary替換成LeakCanaryWithoutDisplay,將AndroidRefWatcherBuilder替換成AndroidRefWatcherBuilderWithoutToast,就可以成功實(shí)現(xiàn)Leak Icon的隱藏了。

// 安裝LeakCanary
AndroidRefWatcherBuilderWithoutToast refBuilder = LeakCanaryWithoutDisplay.refWatcher(ContextUtil.getApplication());
refBuilder.buildAndInstall();
LeakCanaryWithoutDisplay.enableDisplayLeakActivity(ContextUtil.getContext());

解決問題三:希望發(fā)現(xiàn)泄漏后,不再顯示Toast和Notify

在解決完問題二后,解決問題的思路就大體形成了:
1、找到展示(Toast、Notify、Activity)的源碼部分(方法)
2、查看屌用該方法的類
3、重寫一份該類,注釋(修改)其中屌用的方法塊
有了思路,查看源碼后,就可以發(fā)現(xiàn),Toast展示的方法是在AndroidHeapDumper.java:

public final class AndroidHeapDumper implements HeapDumper {
    ...
    @SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
    @Override
    public File dumpHeap() {
        ...
        FutureResult<Toast> waitingForToast = new FutureResult<>();
        showToast(waitingForToast);//發(fā)現(xiàn)泄漏后,顯示Toast
        ...
    }
    ...
}

而Notify的展示,是定義在:DisplayLeakService.java

public class DisplayLeakService extends AbstractAnalysisResultService {
 
    @Override
    protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
        ...
        // New notification id every second.
        int notificationId = (int) (SystemClock.uptimeMillis() / 1000);
        showNotification(this, contentTitle, contentText, pendingIntent, notificationId);//發(fā)現(xiàn)泄漏后,通知欄展示Notify
        ...
    }
}

所以相應(yīng)的,就是重寫這兩份類、重寫屌用這兩個(gè)方法的類、修改LeakCanary安裝時(shí)屌用的類,具體的就不再一一細(xì)說。
最終文件
最終一共重寫了4份源碼文件:
1、AndroidHeapDumperWithoutToast.java
2、AndroidRefWatcherBuilderWithoutToast.java
3、DisplayLeakServiceWithoutNotification.java
4、LeakCanaryWithoutDisplay.java
以下是修改的具體內(nèi)容。

public final class AndroidHeapDumperWithoutToast implements HeapDumper {
 
    final Context context;
    private final LeakDirectoryProvider leakDirectoryProvider;
    private final Handler mainHandler;
 
    public AndroidHeapDumperWithoutToast(Context context, LeakDirectoryProvider leakDirectoryProvider) {
        this.leakDirectoryProvider = leakDirectoryProvider;
        this.context = context.getApplicationContext();
        mainHandler = new Handler(Looper.getMainLooper());
    }
 
 
    @SuppressWarnings("ReferenceEquality") // Explicitly checkinnamed null.
    @Override
    public File dumpHeap() {
        Log.e("TAG-AndroidHeapDumper", "AndroidHeapDumperWithoutToast-dumpHeap");
        File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
 
        if (heapDumpFile == RETRY_LATER) {
            return RETRY_LATER;
        }
 
        FutureResult<Toast> waitingForToast = new FutureResult<>();
        showToast(waitingForToast);
 
        if (!waitingForToast.wait(5, SECONDS)) {
            CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
            return RETRY_LATER;
        }
 
        Toast toast = waitingForToast.get();
        try {
            Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
            cancelToast(toast);
            return heapDumpFile;
        } catch (Exception e) {
            CanaryLog.d(e, "Could not dump heap");
            // Abort heap dump
            return RETRY_LATER;
        }
    }
 
    private void showToast(final FutureResult<Toast> waitingForToast) {
        mainHandler.post(new Runnable() {
            @Override
            public void run() {
                Log.e("TAG-AndroidHeapDumper", "AndroidHeapDumperWithoutToast-showToast");
                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();
            }
        });
    }
}
public final class AndroidRefWatcherBuilderWithoutToast extends RefWatcherBuilder<AndroidRefWatcherBuilderWithoutToast> {
 
    private static final long DEFAULT_WATCH_DELAY_MILLIS = SECONDS.toMillis(5);
 
    private final Context context;
 
    AndroidRefWatcherBuilderWithoutToast(Context context) {
        this.context = context.getApplicationContext();
    }
 
    /**
     * Sets a custom {@link AbstractAnalysisResultService} to listen to analysis results. This
     * overrides any call to {@link #heapDumpListener(HeapDump.Listener)}.
     */
    public AndroidRefWatcherBuilderWithoutToast listenerServiceClass(
            Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
        return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
    }
 
    /**
     * Sets a custom delay for how long the {@link RefWatcher} should wait until it checks if a
     * tracked object has been garbage collected. This overrides any call to {@link
     * #watchExecutor(WatchExecutor)}.
     */
    public AndroidRefWatcherBuilderWithoutToast watchDelay(long delay, TimeUnit unit) {
        return watchExecutor(new AndroidWatchExecutor(unit.toMillis(delay)));
    }
 
    /**
     * Sets the maximum number of heap dumps stored. This overrides any call to {@link
     * #heapDumper(HeapDumper)} as well as any call to
     * {@link LeakCanary#setDisplayLeakActivityDirectoryProvider(LeakDirectoryProvider)})}
     *
     * @throws IllegalArgumentException if maxStoredHeapDumps < 1.
     */
    public AndroidRefWatcherBuilderWithoutToast maxStoredHeapDumps(int maxStoredHeapDumps) {
        LeakDirectoryProvider leakDirectoryProvider =
                new DefaultLeakDirectoryProvider(context, maxStoredHeapDumps);
//        LeakCanary.setDisplayLeakActivityDirectoryProvider(leakDirectoryProvider);
        return heapDumper(new AndroidHeapDumperWithoutToast(context, leakDirectoryProvider));
    }
 
    /**
     * 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;
    }
 
    @Override
    protected boolean isDisabled() {
        return LeakCanary.isInAnalyzerProcess(context);
    }
 
    @Override
    protected HeapDumper defaultHeapDumper() {
        LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
        return new AndroidHeapDumperWithoutToast(context, leakDirectoryProvider);
    }
 
    @Override
    protected DebuggerControl defaultDebuggerControl() {
        return new AndroidDebuggerControl();
    }
 
    @Override
    protected HeapDump.Listener defaultHeapDumpListener() {
        return new ServiceHeapDumpListener(context, DisplayLeakServiceWithoutNotification.class);
    }
 
    @Override
    protected ExcludedRefs defaultExcludedRefs() {
        return AndroidExcludedRefs.createAppDefaults().build();
    }
 
    @Override
    protected WatchExecutor defaultWatchExecutor() {
        return new AndroidWatchExecutor(DEFAULT_WATCH_DELAY_MILLIS);
    }
}
public class DisplayLeakServiceWithoutNotification extends AbstractAnalysisResultService {
 
    @Override
    protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
        Log.e("TAG-viky", "DisplayLeakServiceWithoutNotification-onHeapAnalyzed");
        String leakInfo = leakInfo(this, heapDump, result, true);
        CanaryLog.d("%s", leakInfo);
        if (LeakCanaryWithoutDisplay.getLeakCanaryCallBack() != null) {
            LeakCanaryWithoutDisplay.getLeakCanaryCallBack().onAnalysisResult(leakInfo);
        }
 
        boolean shouldSaveResult = result.leakFound || result.failure != null;
        if (shouldSaveResult) {
            heapDump = renameHeapdump(heapDump);
        }
 
        // New notification id every second.
        afterDefaultHandling(heapDump, result, leakInfo);
    }
 
    private HeapDump renameHeapdump(HeapDump heapDump) {
        String fileName =
                new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(new Date());
 
        File newFile = new File(heapDump.heapDumpFile.getParent(), fileName);
        boolean renamed = heapDump.heapDumpFile.renameTo(newFile);
        if (!renamed) {
            CanaryLog.d("Could not rename heap dump file %s to %s", heapDump.heapDumpFile.getPath(),
                    newFile.getPath());
        }
        return new HeapDump(newFile, heapDump.referenceKey, heapDump.referenceName,
                heapDump.excludedRefs, heapDump.watchDurationMs, heapDump.gcDurationMs,
                heapDump.heapDumpDurationMs);
    }
 
    /**
     * You can override this method and do a blocking call to a server to upload the leak trace and
     * the heap dump. Don't forget to check {@link AnalysisResult#leakFound} and {@link
     * AnalysisResult#excludedLeak} first.
     */
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
    }
}
public final class LeakCanaryWithoutDisplay {
 
    public interface LeakCanaryCallBack {
        void onAnalysisResult(String result);
    }
 
    private static LeakCanaryCallBack sLeakCanaryCallBack;
 
    public static LeakCanaryCallBack getLeakCanaryCallBack() {
        return sLeakCanaryCallBack;
    }
    /**
     * Builder to create a customized {@link RefWatcher} with appropriate Android defaults.
     */
    public static AndroidRefWatcherBuilderWithoutToast refWatcher(Context context) {
        return new AndroidRefWatcherBuilderWithoutToast(context);
    }
 
    public static void enableDisplayLeakActivity(Context context) {
        setEnabled(context, DisplayLeakActivity.class, false);
    }
 
    private LeakCanaryWithoutDisplay() {
        throw new AssertionError();
    }
}

屌用這些文件的地方,也需要修改:

// 安裝LeakCanary
AndroidRefWatcherBuilderWithoutToast refBuilder = LeakCanaryWithoutDisplay.refWatcher(ContextUtil.getApplication());
refBuilder.listenerServiceClass(LeakUploadService.class);
refBuilder.maxStoredHeapDumps(20);
refBuilder.buildAndInstall();
LeakCanaryWithoutDisplay.enableDisplayLeakActivity(ContextUtil.getContext());
public class LeakUploadService extends DisplayLeakServiceWithoutNotification {
 
    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        if (!result.leakFound || result.excludedLeak){
            return;
        }
        // 下面是處理泄漏數(shù)據(jù)的代碼塊
        Log.e("TAG-leakInfo", "leakInfo = " + leakInfo);
        File dumpFile = heapDump.heapDumpFile;
        if (dumpFile.exists()) {
            Log.e("TAG-leakInfo", "dumpFile path = " + dumpFile.getAbsolutePath());
        }
        ...
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,025評(píng)論 25 708
  • 請?jiān)试S我借鑒前輩們的東西~~~~ 感激不盡~~~~~ 以下為Android 框架排行榜 么么噠~ Android...
    嗯_(tái)新閱讀 2,110評(píng)論 3 32
  • 一.榜單介紹 排行榜包括四大類: 單一框架:僅提供路由、網(wǎng)絡(luò)層、UI層、通信層或其他單一功能的框架 混合開發(fā)框架:...
    偉子男閱讀 5,257評(píng)論 0 161
  • 附上原文作者連接:作者:金誠 一.榜單介紹 排行榜包括四大類: 單一框架:僅提供路由、網(wǎng)絡(luò)層、UI層、通信層或其他...
    這個(gè)美嘉不姓陳閱讀 2,277評(píng)論 1 35
  • 從小學(xué)到大學(xué)我通常都是最普通的那一波人,顏值一般,家庭背景一般。校花從來都是屬于我高攀不起的存在,感覺像是生活在同...
    好久不來居然被注冊閱讀 333評(píng)論 0 0