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