標籤:sim 建立對象 關閉 ext ash 選擇 oid 綁定 基礎資料型別 (Elementary Data Type)
起因
寫部落格就像講故事。得有起因,經過,結果,人物。地點和時間。今天就容我給大家講一個故事。
人物呢。肯定是我了。
故事則發生在近期的這兩天,地點在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的資料類型分為兩類:基礎資料型別 (Elementary Data Type)、引用資料類型。
基礎資料型別 (Elementary Data Type)的值儲存在棧記憶體中,而引用資料類型須要開闢兩Block Storage空間。一塊在堆記憶體中。用於儲存該類型的對象;還有一塊在棧記憶體中。用於儲存堆記憶體中該對象的引用。
當中參考型別變數分為四類:
強引用
最經常使用的引用形式。把一個對象賦給一個參考型別變數,則為強引用。
僅僅要一個引用是強引用,則記憶體回收行程永遠都無法回收這個對象的記憶體空間,除非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:存在無效的引用!
記憶體泄露能夠引發非常多的問題。常見的記憶體泄露導致問題例如以下:
記憶體流失分析工具
看到這些問題。突然發現好像離真相越來越近了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