Android性能優化之被忽視的Memory Leaks
起因
寫博客就像講故事。得有起因,經過,結果,人物。地點和時間。今天就容我給大家講一個故事。
人物呢。肯定是我了。
故事則發生在近期的這兩天,地點在coder君上班的公司。那天無意中我發現了一個奇怪的現象,隨著我點開我們App的頁面,Memory Monitor中顯示占用的內存越來越多(前面的頁面已經finish掉了)。
咦?什麽鬼?
經過
有了問題就解決嘛,俗話說的好。有bug要上。沒有bug寫個bug也要上。那究竟是是什麽問題會引起這個現象呢?
Android中內存相關的問題無非就是這麽幾點:
- Memory Leaks 內存泄漏
- Memory Churn 內存抖動
- OutOfMemory 內存溢出
阿西吧。細致想想怎麽這麽像內存泄漏呢。那究竟是不是呢?那我們就一點一點分析一下唄。
內存相關數據
關於內存我們可能想了解的數據大概有三點:
總內存
private String getTotalMemory() { String str1 = "/proc/meminfo";// 系統內存信息文件 String str2; String[] arrayOfString; long initial_memory = 0; try { FileReader localFileReader = new FileReader(str1); BufferedReader localBufferedReader = new BufferedReader( localFileReader, 8192); str2 = localBufferedReader.readLine();// 讀取meminfo第一行,系統總內存大小 arrayOfString = str2.split("\\s+"); for (String num : arrayOfString) { Log.i(str2, num + "\t"); } initial_memory = Integer.valueOf(arrayOfString[1]).intValue() * 1024;// 獲得系統總內存,單位是KB,乘以1024轉換為Byte localBufferedReader.close(); } catch (IOException e) { } return Formatter.formatFileSize(getBaseContext(), initial_memory);// Byte轉換為KB或者MB,內存大小規格化 }
系統當前可用內存
private String getAvailMemory() { // 獲取android當前可用內存大小 ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); am.getMemoryInfo(mi); //mi.availMem; 當前系統的可用內存 return Formatter.formatFileSize(getBaseContext(), mi.availMem);// 將獲取的內存大小規格化 }
我們能夠使用的內存
每個Android設備都會有不同的RAM總大小與可用空間。因此不同設備為app提供了不同大小的heap限制。你能夠通過調用getMemoryClass())來獲取你的app的可用heap大小。假設你的app嘗試申請很多其它的內存,會出現OutOfMemory的錯誤。
在一些特殊的情景下,你能夠通過在manifest的application標簽下加入largeHeap=true的屬性來聲明一個更大的heap空間。假設你這樣做,你能夠通過getLargeMemoryClass())來獲取到一個更大的heap size。
然而。能夠獲取更大heap的設計本意是為了一小部分會消耗大量RAM的應用(比如一個大圖片的編輯應用)。不要輕易的由於你須要使用大量的內存而去請求一個大的heap size。
僅僅有當你清楚的知道哪裏會使用大量的內存而且為什麽這些內存必須被保留時才去使用large heap. 因此請盡量少使用large heap。使用額外的內存會影響系統總體的用戶體驗,而且會使得GC的每次執行時間更長。
在任務切換時,系統的性能會變得大打折扣。
另外, large heap並不一定能夠獲取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。因此即使你申請了large heap。你還是應該通過執行getMemoryClass()來檢查實際獲取到的heap大小。
private String getAllocationMemory() { // 獲取系統分配的內存大小 ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); //開啟了android:largeHeap="true",米4系統能分配的內存為512M,不開啟為128M //return am.getLargeMemoryClass()+""; //return am.getMemoryClass()+""; }
Java中的四種引用
開始分析之前。有必要先了解下Java的內存分配與回收。
Java的數據類型分為兩類:基本數據類型、引用數據類型。
基本數據類型的值存儲在棧內存中,而引用數據類型須要開辟兩塊存儲空間。一塊在堆內存中。用於存儲該類型的對象;還有一塊在棧內存中。用於存儲堆內存中該對象的引用。
當中引用類型變量分為四類:
強引用
最經常使用的引用形式。把一個對象賦給一個引用類型變量,則為強引用。
僅僅要一個引用是強引用,則垃圾回收器永遠都無法回收這個對象的內存空間,除非JVM終止。
軟引用
當內存資源充足的時候,垃圾回收器不會回收軟引用相應的對象的內存空間;但當內存資源緊張時,軟引用所相應的對象就會被垃圾回收器回收。
//創建一個Student類型的軟引用 SoftReference<Student> sr = new SoftReference<Student>(new Student());
弱引用
無論JVM內存資源是否緊張,僅僅要垃圾回收器執行,弱引用所相應的對象就會被釋放。
虛引用
虛引用等於沒有引用,無法通過虛引用訪問其相應的對象。
軟引用和弱引用在其對象被回收之後。這些引用會被加入到引用隊列中去;而虛引用在其對象被回收之前,虛引用就被加入到引用隊列中去了。因此虛引用能夠在其對象被釋放之前進行一些操作。
虛引用和引用隊列綁定的方法:
//創建引用隊列 ReferenceQueue<String> queue = new ReferenceQueue<String>(); //創建虛引用,並綁定引用隊列 PhantomReference<String> str = new PhantomReference<String>("啦啦啦",queue);
Garbage Collection Android中的垃圾回收
Android系統會在適當的時機觸發GC操作,一旦進行GC操作,就會將一些不再使用的對象進行回收。
執行GC操作的時候,全部線程的不論什麽操作都會須要暫停。等待GC操作完畢之後,其它操作才幹夠繼續執行。
通常來說,單個的GC並不會占用太多時間,可是大量不停的GC操作則會顯著占用幀間隔時間(16ms)。假設在幀間隔時間裏面做了過多的GC操作,那麽自然其它相似計算。渲染等操作的可用時間就變得少了
Memory Leaks內存泄漏
內存泄漏表示的是不再用到的對象由於被錯誤引用而無法進行回收。發生內存泄漏會導致Memory Generation中的剩余可用Heap Size越來越小,這樣會導致頻繁觸發GC,更進一步引起性能問題。
總結起來事實上非常easy:存在無效的引用!
內存泄露能夠引發非常多的問題。常見的內存泄露導致問題例如以下:
應用卡頓。響應速度慢(內存占用高時JVM虛擬機會頻繁觸發GC);
應用被從後臺進程幹為空進程;
應用莫名的崩潰(也就是超過了HeepSize閾值引起OOM)。
內存泄漏分析工具
看到這些問題。突然發現好像離真相越來越近了0.0。
想要更加清楚地實時知曉當前應用程序的內存使用情況,我們須要通過一些工具來實現。比較好用的工具有兩種:
- Memory Analyzer Tool
- LeakCanary
以下我們分開介紹。
Memory Analyzer Tool
Memory Analysis Tools(點我下載)是一個專門分析Java堆數據內存引用的工具,我們能夠使用它方便的定位內存泄露原因,核心任務就是找到GC ROOT位置。
接下來說下使用步驟。
抓取內存信息
AndriodStudio中抓取內存信息還是非常方便的,有兩種方法:
使用Android Device Monitor
點擊Android Studio工具欄上的Tool–>Android Device Monitor
在Android Device Monitor界面中選在你要分析的應用程序的包名,點擊Update Heap來更新統計信息,然後點擊Cause GC就可以查看當前堆的使用情況,點擊Dump HPROF file,將該應用當前的內存信息保存成hprof文件,放在桌面就可以。操作例如以下圖
直接獲取
Android Studio的最新版本號能夠直接獲取hprof文件,可是註意在使用之前一定要手動點擊 Initiate GCbutton手動觸發GC。這樣抓到的內存使用情況就是不包含Unreachable對象的。
稍等片刻,生成的文件會出如今captures中。然後選擇文件,點擊右鍵轉換成標準的hprof文件。就能夠在MAT中打開了。
使用MAT工具查看分析
這裏我寫了個簡單的demo來測試,這個demo一共同擁有兩個頁面,在跳轉到第二個頁面之後,新開一個現成去打印activity信息。
/**
* 打印ActivityName
*/
public void printActivityName() {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true)
try {
Thread.sleep(1000 * 30);
Log.e(ActivityHelper.class.getSimpleName(), ((Activity) mContext).getClass().getSimpleName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
多次進入SecondActivity之後會發現內存一直在增長,並沒有減少。
而且log裏會不停的輸出log,打印當前activity的name。
在MAT中打開抓取到的文件後如圖
MAT中提供了非常多的功能,這裏我們僅僅要學習幾個最經常使用的就能夠了。上圖最中央的那個餅狀圖展示了最大的幾個對象所占內存的比例,這張圖中提供的內容並不多。我們能夠忽略它。紅色框中有兩個非常實用的工具是我們經常使用的。
Histogram能夠列出內存中每個對象的名字、數量以及大小。
Dominator Tree會將全部內存中的對象按大小進行排序,而且我們能夠分析對象之間的引用結構。
我們先來看Histogram
我們應該怎樣去分析內存泄漏呢?
即分析大內存的對象。
可是假如我們有目標對象的話,左上角值支持正則表達式的,我們輸入SecondActivity。這裏我們看到。我們有5個SecondActivity的實例。由於我們引用SecondActivity的現成沒有銷毀。導致會有非常多實例。
接下來對著SecondActivity右鍵 -> List objects -> with incoming references查看詳細SecondActivity實例。例如以下圖所看到的:
假設想要查看內存泄漏的詳細原因,能夠對著隨意一個MainActivity的實例右鍵 -> Path to GC Roots -> exclude weak references,結果例如以下圖所看到的:
能夠看到紅色框中,由於我們的線程持有SecondActivity的實例。全部導致內存泄漏。
此外。我們能夠選擇以我們項目的包結構的形式來查看
接下來我們看下Dominator Tree。
關於Dominator Tree我們須要註意三點:
- 首先Retained Heap表示這個對象以及它所持有的其它引用(包含直接和間接)所占的總內存,因此從上圖中看,前兩行的Retained Heap是最大的。我們分析內存泄漏時,內存最大的對象也是最應該去懷疑的。
- 帶有黃點的對象就表示是能夠被GC Roots訪問到的,依據上面的解說,能夠被GC Root訪問到的對象都是無法被回收的。
- 並非全部帶黃點的對象都是泄漏的對象,有些對象系統須要一直使用。本來就不應該被回收。我們能夠註意到。有些帶黃點的對象最右邊會寫一個System Class,說明這是一個由系統管理的對象,並非由我們自己創建並導致內存泄漏的對象。
如今我們能夠對著我們想查看的內容點擊右鍵 -> Path to GC Roots -> exclude weak references,為什麽選擇exclude weak references呢?由於弱引用是不會阻止對象被垃圾回收器回收的。所以我們這裏直接把它排除掉。然後一步一步分析。
LeakCanary
leakcanary是一個開源項目,一個內存泄露自己主動檢測工具,是著名的GitHub開源組織Square貢獻的,它的主要優勢就在於自己主動化過早的發覺內存泄露、配置簡單、抓取貼心,缺點在於還存在一些bug,只是正常使用百分之九十情況是OK的,其核心原理與MAT工具相似。
由於配置十分簡單。這裏就不多說了,官方文檔。
我們看下分析結果
簡單直白!
常見內存泄漏情況
構造Adapter時。沒有使用緩存的 convertView
Bitmap對象不在使用時調用recycle()釋放內存
Context使用不當造成內存泄露:不要對一個Activity Context保持長生命周期的引用。
盡量在一切能夠使用應用ApplicationContext取代Context的地方進行替換。
非靜態內部類的靜態實例easy造成內存泄漏:即一個類中假設你不能夠控制它當中內部類的生命周期(譬如Activity中的一些特殊Handler等)。則盡量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。
警惕線程未終止造成的內存泄露。譬如在Activity中關聯了一個生命周期超過Activity的Thread,在退出Activity時切記結束線程。一個典型的樣例就是HandlerThread的run方法是一個死循環,它不會自己結束,線程的生命周期超過了Activity生命周期。我們必須手動在Activity的銷毀方法中中調運thread.getLooper().quit();才不會泄露。
對象的註冊與反註冊沒有成對出現造成的內存泄露。譬如註冊廣播接收器、註冊觀察者(典型的譬如數據庫的監聽)等。
創建與關閉沒有成對出現造成的泄露;譬如Cursor資源必須手動關閉,WebView必須手動銷毀。流等對象必須手動關閉等。
不要在執行頻率非常高的方法或者循環中創建對象(比方onmeasure),能夠使用HashTable等創建一組對象容器從容器中取那些對象。而不用每次new與釋放。
避免代碼設計模式的錯誤造成內存泄露;譬如循環引用,A持有B。B持有C。C持有A,這種設計誰都得不到釋放。
結果
真相僅僅有一個,那就是確實是由於內存泄漏才出現我遇到的情況。程序猿嘛,誰還不踩個坑,跳出來,拍拍身上的灰塵。總結一下,過兩天又是一條棒棒的coder。源代碼
Android性能優化之被忽視的Memory Leaks