LeakCanary-隐藏Icon、Toast、Notify
在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());
}
...
}
}