我爱编程

LeakCanary-隐藏Icon、Toast、Notify

2018-04-16  本文已影响0人  viky_lyn

在Android中,要检测App的内存泄漏,众所周知有个Square公司开源神器——LeakCanary。
LeakCanary的使用方便简单,使用只需要3行代码即可:
1)在build.gradle文件中,添加依赖(版本号可自行选择):

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

2)在Application中,执行:

RefWatcher mRefWatcher = LeakCanary.install(this);

mRefWatcher可以用于检测你想检测的内容,比如用于检测Fragment。
LeakCanary的更多具体使用方法,网上有很多详细的内容,可以自行搜索查看。
大家应该知道使用LeakCanary后,设备上会有一个Leak的Icon,发现泄漏后,会出现一个Toast提示,并在通知栏中会展示一个Leak的Notify信息,但是由于某些原因,我们需要隐藏掉这些外露的信息,目标是:可以在debug和release包中都能检测内存泄漏,发现泄漏后,可以做到获取泄漏信息时,用户是无感知的。

解决问题一:希望在debug和release包中都能检测内存泄漏

虽然LeakCanary提供了release的版本,但是release版本为了App的性能,会跳过检查,所以LeakCanary的内存泄漏检测是在Debug包中才能产生效果。
在build.gradle中,引入的2行代码,分别代表,在debug版本中,引入leakcanary-android:1.5.4,在release版本中,引入leakcanary-android-no-op:1.5.4,所以要想实现想要的效果,只需要将两行代码缩减并修改成一行:

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

也就是不再区分debug版本和release版本,直接引入LeakCanary用于检测内存泄漏的版本,Over!(注意:有可能导致App的性能变差,需要额外关注)

解决问题二:希望能够隐藏Leak的Icon

想要隐藏Leak的Icon,首先要知道Icon是怎么来的。
首先,LeakCanary的使用手册中,有告诉我们,如果需要更换Leak的Icon,可以替换图标文件:

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。

在网上查阅资料,看到有位大神提供的建议,将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>

实践后,发现这部分的代码确实可以让App找不到DisplayLeakActivity,所以也确实可以隐藏Icon,但是正是由于App需要DisplayLeakActivity,却又找不到它,所以引发了Crash问题,报错就是找不到DisplayLeakActivity。所以该方法不可行。
走投无路后,将LeakCanary的代码down下来,希望能在源码中,找到隐藏Icon的方法。
首先想到,既然有大神提供了在Mainfest.xml中,隐藏DisplayLeakActivity,那么在源码的这个文件下,就一定有对这个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的由来,同时Activity是在leakcanary进程中(不在我们的App进程中),所以展示不受影响。
既然我们希望可以隐藏Icon,所以最直接的方法,就是通过 tools:node="remove" 方法,移除掉Activity的定义,从而达到隐藏Activity的目的,也就是上面大神提供的那个方法,然而并不可行。
所以只能往它的上一步查找:屌用Activity的地方,可以想象,如果我们将所有屌用Activity的部分注释掉,那么也可以达到我们想要的效果。
查找后,发现整份源码中,只有2个部分屌用到了DisplayLeakActivity,并且它的入口都在同一份java文件中(这是非常幸运的一件事情,感谢Square公司大神们的代码架构非常好):

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

所以自然而然的,冒出来的第一个想法就是:继承LeakCanary,修改这两部分代码。但是可以看到,LeakCanary类是final类型,无法继承,所以只能放弃继承的想法。
但是我们可以重写一个MyLeakCanary,内容和LeakCanary一样,在MyLeakCanary中,修改这两部分的代码,在外部屌用LeakCanary.install(this);的部分,修改成MyLeakCanary.install(this);,似乎也是可以达到我们想要的效果。
所以重新建立一个com.squareup.leakcanary包名,新建一个LeakCanaryWithoutDisplay类,将LeakCanary的内容全部复制过来,按照我们想要的修改,所以修改后变成:

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文件中屌用的。
所以,新建一个MyAndroidRefWatcherBuilder,将AndroidRefWatcherBuilder的内容全部复制过来,修改:

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公司大神们优秀的代码架构,我们将2个文件重新定义一份后,在屌用的地方,将LeakCanary替换成LeakCanaryWithoutDisplay,将AndroidRefWatcherBuilder替换成AndroidRefWatcherBuilderWithoutToast,就可以成功实现Leak Icon的隐藏了。

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

解决问题三:希望发现泄漏后,不再显示Toast和Notify

在解决完问题二后,解决问题的思路就大体形成了:
1、找到展示(Toast、Notify、Activity)的源码部分(方法)
2、查看屌用该方法的类
3、重写一份该类,注释(修改)其中屌用的方法块
有了思路,查看源码后,就可以发现,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);//发现泄漏后,显示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);//发现泄漏后,通知栏展示Notify
        ...
    }
}

所以相应的,就是重写这两份类、重写屌用这两个方法的类、修改LeakCanary安装时屌用的类,具体的就不再一一细说。
最终文件
最终一共重写了4份源码文件:
1、AndroidHeapDumperWithoutToast.java
2、AndroidRefWatcherBuilderWithoutToast.java
3、DisplayLeakServiceWithoutNotification.java
4、LeakCanaryWithoutDisplay.java
以下是修改的具体内容。

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;
        }
        // 下面是处理泄漏数据的代码块
        Log.e("TAG-leakInfo", "leakInfo = " + leakInfo);
        File dumpFile = heapDump.heapDumpFile;
        if (dumpFile.exists()) {
            Log.e("TAG-leakInfo", "dumpFile path = " + dumpFile.getAbsolutePath());
        }
        ...
    }
}
上一篇下一篇

猜你喜欢

热点阅读