美文网首页我爱编程
LeakCanary-隐藏Icon、Toast、Notify

LeakCanary-隐藏Icon、Toast、Notify

作者: viky_lyn | 来源:发表于2018-04-16 16:28 被阅读0次

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

    相关文章

      网友评论

        本文标题:LeakCanary-隐藏Icon、Toast、Notify

        本文链接:https://www.haomeiwen.com/subject/jgyekftx.html