編輯:關於Android編程
讓我們來看看在沒有LeakCanary之前,我們怎麼來檢測內存洩露
1. Bug收集
通過Bugly、友盟這樣的統計平台,統計Bug,了解OutOfMemaryError
的情況。
2. 重現問題
對Bug進行篩選,歸類,排除干擾項。然後為了重現問題,有時候你必須找到出現問題的機型,因為有些問題只會在特定的設備上才會出現。為了找到特定的機型,可能會想盡一切辦法,去買、去借、去求人(14年的時候,上家公司專門派了一個商務去廣州找了一家租賃手機的公司,借了50台手機回來,600塊錢一天)。然後,為了重現問題,一遍一遍的嘗試,去還原當時OutOfMemaryError出現的原因,用最原始、最粗暴的方式。
3. Dump導出hprof文件
使用Eclipse ADT的DDMS,觀察Heap,然後點擊手動GC按鈕(Cause GC),觀察內存增長情況,導出hprof文件。
主要觀測的兩項數據:
1. Heap Size的大小,當資源增加到堆空余空間不夠的時候,系統會增加堆空間的大小,但是超過可分配的最大值(比如手機給App分配的最大堆空間為128M)就會發生OutOfMemaryError,這個時候進程就會被殺死。這個最大堆空間,不同手機會有不同的值,跟手機內存大小和廠商定制過後的系統存在關聯。
2. Allocated堆中已分配的大小,這是應用程序實際占用的大小,資源回收後,這項數據會變小。
查看操作前後的堆數據,看是否存在內存洩露,比如反復打開、關閉一個頁面,看看堆空間是否會一直增大。
4. 然後使用MAT內存分析工具打開,反復查看找到那些原本應該被回收掉的對象。
5. 計算這個對象到GC roots的最短強引用路徑。
6. 確定那個路徑中那個應用不該有,然後修復問題。
很麻煩,不是嗎。現在有一個類庫可以直接解決這個問題<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMSBpZD0="leakcanary">LeakCanary
使用AndroidStudio,在Module.app
的build.gradle
中引入
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
然後在Application
中重寫onCreate()
方法
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
在Activity中寫一些導致內存洩露的代碼,當發生內存洩露了,會在通知欄彈出消息,點擊跳轉到洩露頁面
LeakCanary 可以做到非常簡單方便、低侵入性地捕獲內存洩漏代碼,甚至很多時候你可以捕捉到 Android 系統組件的內存洩漏代碼,最關鍵是不用再進行(捕獲錯誤+Bug歸檔+場景重現+Dump+Mat分析) 這一系列復雜操作,6得不行。
首先,設想如果讓我們自己來實現一個LeakCanary
,我們怎麼來實現。
按照前面說的曾經檢測內存的方式,我想,大概需要以下幾個步驟:
1. 檢測一個對象,查看他是否被回收了。
2. 如果沒有被回收,使用DDMS的dump導出.hprof文件,確定是否內存洩露,如果洩露了導出最短引用路徑
3. 把最短引用路徑封裝到一個對象中,用Intent發送給Notification,然後點擊跳轉到展示頁,頁面展示
我們來看看,LeakCanary
是不是按照這種方式實現的。除了剛才說的只需要在Application
中的onCreate
方法注冊LeakCanary.install(this);
這種方式。 查看源碼,使用官方給的Demo示例代碼中,我們發現有一個RefWatcher
對象,也可以用來監測,看看它是如何使用的。
MainActivity.class
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RefWatcher refWatcher = LeakCanary.androidWatcher(getApplicationContext(),
new ServiceHeapDumpListener(getApplicationContext(), DisplayLeakService.class),
AndroidExcludedRefs.createAppDefaults().build());
refWatcher.watch(this);
}
就是把MainActivity作為一個對象監測起來,查看refWatcher.watch(this)
的實現
public void watch(Object watchedReference) {
watch(watchedReference, "");
}
/**
* Watches the provided references and checks if it can be GCed. This method is non blocking,
* the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
*
* @param referenceName An logical identifier for the watched object.
*/
public void watch(Object watchedReference, String referenceName) {
Preconditions.checkNotNull(watchedReference, "watchedReference");
Preconditions.checkNotNull(referenceName, "referenceName");
if (debuggerControl.isDebuggerAttached()) {
return;
}
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
watchExecutor.execute(new Runnable() {
@Override public void run() {
ensureGone(reference, watchStartNanoTime);
}
});
}
可以總結出他的實現步驟如下:
1. 先檢查監測對象是否為空,為空拋出異常
2. 如果是在調試Debugger過程中允許內存洩露出現,不再監測。因為這個時候監測的對象是不准確的,而且會干擾我們調試代碼。
3. 給監測對象生成UUID唯一標識符,存入Set集合,方便查找。
4. 然後定義了一個KeyedWeakReference
,查看下KeyedWeakReference
是個什麼玩意
public final class KeyedWeakReference extends WeakReference
原來KeyedWeakReference
就是對WeakReference
進行了一些加工,是一種裝飾設計模式,其實就是弱引用的衍生類。配合前面的Set
retainedKeys
使用,retainedKeys
代表的是沒有被GC回收的對象,referenceQueue
中的弱引用代表的是被GC了的對象,通過這兩個結構就可以明確知道一個對象是不是被回收了。( 一個對象在referenceQueue
可以找到當時在retainedKeys
中找不到,那麼肯定被回收了,沒有內存洩漏一說)
5. 接著看上面的執行過程,然後通過線程池開啟了一個異步任務方法ensureGone
。watchExecutor
看看這個實體的類實現—AndroidWatchExecutor
,查看源碼
public final class AndroidWatchExecutor implements Executor {
static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
private static final int DELAY_MILLIS = 5000;
private final Handler mainHandler;
private final Handler backgroundHandler;
public AndroidWatchExecutor() {
mainHandler = new Handler(Looper.getMainLooper());
HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
handlerThread.start();
backgroundHandler = new Handler(handlerThread.getLooper());
}
@Override public void execute(final Runnable command) {
if (isOnMainThread()) {
executeDelayedAfterIdleUnsafe(command);
} else {
mainHandler.post(new Runnable() {
@Override public void run() {
executeDelayedAfterIdleUnsafe(command);
}
});
}
}
private boolean isOnMainThread() {
return Looper.getMainLooper().getThread() == Thread.currentThread();
}
private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
// This needs to be called from the main thread.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
return false;
}
});
}
}
做得事情就是,通過主線程的mainHandler
轉發到後台backgroundHandler
執行任務,後台線程延遲DELAY_MILLIS
這麼多時間執行
6. 具體執行的任務在ensureGone()
方法裡面
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
//記錄觀測對象的時間
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//清除在queue中的弱引用 保留retainedKeys中剩下的對象
removeWeaklyReachableReferences();
//如果剩下的對象中不包含引用對象,說明已被回收,返回||調試中,返回
if (gone(reference) || debuggerControl.isDebuggerAttached()) {
return;
}
//請求執行GC
gcTrigger.runGc();
//再次清理一次對象
removeWeaklyReachableReferences();
if (!gone(reference)) {
long startDumpHeap = System.nanoTime();
//記錄下GC執行時間
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
//Dump導出hprof文件
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == null) {
// Could not dump the heap, abort.
return;
}
//記錄下Dump和文件導出用的時間
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
//分析hprof文件
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
}
private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}
private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
retainedKeys.remove(ref.key);
}
}
這裡我們思考兩個問題:
1. retainedKeys和queue怎麼關聯起來的?這裡的removeWeaklyReachableReferences
方法就實現了我們說的 retainedKeys
代表的是沒有被GC回收的對象,queue
中的弱引用代表的是被GC了的對象,之間的關聯, 一個對象在queue
可以找到當時在retainedKeys
中找不到,那麼肯定被回收了。gone()
返回true說明對象已被回收,不需要觀測了。
2. 為什麼執行removeWeaklyReachableReferences()
兩次?為了保證效率,如果對象被回收,沒必要再通知GC執行,Dump操作等等一系列繁瑣步驟,況且GC是一個線程優先級極低的線程,就算你通知了,她也不一定會執行,基於這一點,我們分析的觀測對象的時機就顯得尤為重要了,在對象被回收的時候召喚觀測。
我們觀測的是一個Activity,Activity這樣的組件都存在生命周期,在他生命周期結束的時,觀測他如果還存活的話 就肯定就存在內存洩露了,進一步推論,Activity的生命周期結束就關聯到它的onDestory()
方法,也就是只要重寫這個方法就可以了。
@Override
protected void onDestroy() {
super.onDestroy();
refWatcher.watch(this);
}
在MainActivity中加上這行代碼就好了,但是我們顯然不想每個Activity都這樣干,都是同樣的代碼為啥要重復著寫,當然解決辦法呼之欲出:
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override public void onActivityStarted(Activity activity) {
}
@Override public void onActivityResumed(Activity activity) {
}
@Override public void onActivityPaused(Activity activity) {
}
@Override public void onActivityStopped(Activity activity) {
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
};
void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
LeakCanary源碼是這樣做的,通過ActivityLifecycleCallbacks
轉發,然後在install()
中使用這個接口,這就實現了我們只需要調用LeakCanary.install(this);
這句代碼在Application中就可以實現監測
public final class LeakCanary {
public static RefWatcher install(Application application) {
return install(application, DisplayLeakService.class, AndroidExcludedRefs.createAppDefaults().build());
}
public static RefWatcher install(Application application, Class listenerServiceClass, ExcludedRefs excludedRefs) {
if(isInAnalyzerProcess(application)) {
return RefWatcher.DISABLED;
} else {
enableDisplayLeakActivity(application);
ServiceHeapDumpListener heapDumpListener = new ServiceHeapDumpListener(application, listenerServiceClass);
RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
return refWatcher;
}
}
不需要在每個Activity方法的結束再多寫幾行onDestroy()
代碼,但是這個方法有個缺點,看注釋
// If you need to support Android < ICS, override onDestroy() in your base activity.
//ICS
October 2011: Android 4.0.
public static final int ICE_CREAM_SANDWICH = 14;
如果是SDK 14 Android 4.0以下的系統,不具備這個接口,也就是還是的通過剛才那種方式重寫onDestory()
方法。而且只實現了ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
對Activity進行監測,如果是服務或者廣播還需要我們自己實現
接著分析,查看文件解析類發現他是個轉發工具類
public final class ServiceHeapDumpListener implements HeapDump.Listener {
...
@Override public void analyze(HeapDump heapDump) {
Preconditions.checkNotNull(heapDump, "heapDump");
//轉發給HeapAnalyzerService
HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}
}
通過IntentService
運行在另一個進程中執行分析任務
public final class HeapAnalyzerService extends IntentService {
private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";
private static final String HEAPDUMP_EXTRA = "heapdump_extra";
public static void runAnalysis(Context context, HeapDump heapDump,
Class listenerServiceClass) {
Intent intent = new Intent(context, HeapAnalyzerService.class);
intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
intent.putExtra(HEAPDUMP_EXTRA, heapDump);
context.startService(intent);
}
@Override protected void onHandleIntent(Intent intent) {
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
ExcludedRefs androidExcludedDefault = createAndroidDefaults().build();
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(androidExcludedDefault, heapDump.excludedRefs);
//獲取分析結果
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
}
查看heapAnalyzer.checkForLeak
代碼
public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
long analysisStartNanoTime = System.nanoTime();
if (!heapDumpFile.exists()) {
Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
return AnalysisResult.failure(exception, since(analysisStartNanoTime));
}
ISnapshot snapshot = null;
try {
// 加載hprof文件
snapshot = openSnapshot(heapDumpFile);
//找到洩露對象
IObject leakingRef = findLeakingReference(referenceKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
if (leakingRef == null) {
return AnalysisResult.noLeak(since(analysisStartNanoTime));
}
String className = leakingRef.getClazz().getName();
// 最短引用路徑
AnalysisResult result =
findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
//如果沒找到 嘗試排除系統進程干擾的情況下找出最短引用路徑
if (!result.leakFound) {
result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
}
return result;
} catch (SnapshotException e) {
return AnalysisResult.failure(e, since(analysisStartNanoTime));
} finally {
cleanup(heapDumpFile, snapshot);
}
}
到這裡,我們就找到了洩露對象的最短引用路徑,剩下的工作就是發送消息給通知,然後點擊通知欄跳轉到我們另一個App打開繪制出路徑即可。
但是我們在找出最短引用路徑的時候,有這樣一段代碼,他是干什麼的呢
// 最短引用路徑
AnalysisResult result =
findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
//如果沒找到 嘗試排除系統進程干擾的情況下找出最短引用路徑
if (!result.leakFound) {
result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
查看findLeakTrace()
private AnalysisResult findLeakTrace(long analysisStartNanoTime, ISnapshot snapshot,
IObject leakingRef, String className, boolean excludingKnownLeaks) throws SnapshotException {
ExcludedRefs excludedRefs = excludingKnownLeaks ? this.excludedRefs : baseExcludedRefs;
PathsFromGCRootsTree gcRootsTree = shortestPathToGcRoots(snapshot, leakingRef, excludedRefs);
// False alarm, no strong reference path to GC Roots.
if (gcRootsTree == null) {
return AnalysisResult.noLeak(since(analysisStartNanoTime));
}
LeakTrace leakTrace = buildLeakTrace(snapshot, gcRootsTree, excludedRefs);
return AnalysisResult.leakDetected(!excludingKnownLeaks, className, leakTrace, since(analysisStartNanoTime));
}
唯一的不同是excludingKnownLeaks
從字面意思也很好理解,是否排除已知內存洩露
其實是這樣的,在我們系統中本身就存在一些內存洩露的情況,這是上層App工程師無能為力的。但是如果是廠商或者做Android Framework
層的工程師可能需要關心這個,於是做成一個參數配置的方式,讓我們靈活選擇豈不妙哉。當然,默認是會排除系統自帶洩露情況的,不然打開App,彈出一堆莫名其妙的內存洩露,我們還無能為力,著實讓人惶恐,而且我們還可以自己配置。
通過ExcludedRefs
這個類
public final class ExcludedRefs implements Serializable {
public final Map> excludeFieldMap;
public final Map> excludeStaticFieldMap;
public final Set excludedThreads;
private ExcludedRefs(Map> excludeFieldMap,
Map> excludeStaticFieldMap, Set excludedThreads) {
// Copy + unmodifiable.
this.excludeFieldMap = unmodifiableMap(new LinkedHashMap>(excludeFieldMap));
this.excludeStaticFieldMap = unmodifiableMap(new LinkedHashMap>(excludeStaticFieldMap));
this.excludedThreads = unmodifiableSet(new LinkedHashSet(excludedThreads));
}
public static final class Builder {
private final Map> excludeFieldMap = new LinkedHashMap>();
private final Map> excludeStaticFieldMap = new LinkedHashMap>();
private final Set excludedThreads = new LinkedHashSet();
public Builder instanceField(String className, String fieldName) {
Preconditions.checkNotNull(className, "className");
Preconditions.checkNotNull(fieldName, "fieldName");
Set excludedFields = excludeFieldMap.get(className);
if (excludedFields == null) {
excludedFields = new LinkedHashSet();
excludeFieldMap.put(className, excludedFields);
}
excludedFields.add(fieldName);
return this;
}
public Builder staticField(String className, String fieldName) {
Preconditions.checkNotNull(className, "className");
Preconditions.checkNotNull(fieldName, "fieldName");
Set excludedFields = excludeStaticFieldMap.get(className);
if (excludedFields == null) {
excludedFields = new LinkedHashSet();
excludeStaticFieldMap.put(className, excludedFields);
}
excludedFields.add(fieldName);
return this;
}
public Builder thread(String threadName) {
Preconditions.checkNotNull(threadName, "threadName");
excludedThreads.add(threadName);
return this;
}
public ExcludedRefs build() {
return new ExcludedRefs(excludeFieldMap, excludeStaticFieldMap, excludedThreads);
}
}
}
當內容超過了TextView的顯示范圍,這個時候就需要TextView裡面的內容滾動起來。 首先看下布局文件: 即便布局文件這樣設置了相關屬性,但是在
當今的android應用設計中,一種主流的設計方式就是會擁有一個側滑菜單,以圖為證: 實現這樣的側滑效果,在5.0以前我們用的最多的就是SlidingMenu這
重點內容我們在做項目中經常會遇到要使用第三方SDK,這時,我們通常先將第三方jar包導入,然後再導入相應的資源文件,最後在自己的項目中調用,很多人只會用卻不知道怎麼讓自己
Android:使用AppCompatAutoCompleteTextView:我們先看看實現的效果吧,也就是我們俗話說的自動提示功能。這裡我實現了點擊AppCompat