今天來簡單的介紹下怎麼分析android應用的內存泄漏問題。
首先我們要明白爲什麼會有內存泄漏,主要有2種情況:
a.全局進程(process-global)的static變量。這個無視應用的狀態,持有Activity的強引用的怪物。
b.活在Activity生命週期之外的線程。沒有清空對Activity的強引用。
1、瞭解下4種級別對象的引用:
從JDK 1.2版本開始,把對象的引用分爲4種級別,從而使程序能更加靈活地控制對象的生命週期。這4種級別由高到低依次爲:強引用、軟引用、弱引用和虛引用。
強引用(Strong reference):
常見形式如:A a = new A();等
軟引用(Soft Reference):
A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);
軟引用所指示的對象進行垃圾回收需要滿足如下兩個條件:
1.當其指示的對象沒有任何強引用對象指向它;
2.當虛擬機內存不足時。
弱引用(Weak Reference):
A a = new A();
WeakReference<A> wrA = new WeakReference<A>(a);
WeakReference不改變原有強引用對象的垃圾回收時機,一旦其指示對象沒有任何強引用對象時,此對象即進入正常的垃圾回收流程。
虛引用(Phantom Reference):
2、瞭解下內存的知識:
JAVA是在JVM所虛擬出的內存環境中運行的,JVM的內存可分爲三個區:堆(heap)、棧(stack)和方法區(method)。
棧(stack):棧最顯著的特徵是:LIFO(Last In, First Out, 後進先出),棧中只存放基本類型和對象的引用(不是對象)。
堆(heap):堆內存用於存放由new創建的對象和數組。
在堆中分配的內存,由java虛擬機自動垃圾回收器來管理。JVM只有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身。
方法區(method):又叫靜態區,跟堆一樣,被所有的線程共享。方法區包含所有的class和static變量。
查看設備內存的方法:
a.查看meminfo文件
cat /proc/meminfo
cat /proc/meminfo |grep Mem
b.更詳細的信息可以使用dumpsys命令
dumpsys meminfo 查看總的內存使用
dumpsys meminfo com.example.testleakmemory 查看單個應用的內存使用情況
dumpsys meminfo 27606 通過pid查看單個應用的內存使用情況
3、工具準備:
工欲善其事,必先利器。我們要使用的是DDMS+MAT(Memory Analysis Tool)來分析。
DDMS是ADT自帶的調試工具。
MAT的下載網址:http://www.eclipse.org/mat/downloads.php
我自己在線安裝一直都不成功,各種錯誤。後面直接在下載(Stand-alone Eclipse RCP Applications)版本。
開始的時候下載了64位的版本,發現運行exe文件的時候老是報 “failed to load the jni shared jvm.dll” 這個錯誤。
後面又下載32位了發現可以運行。查看本地的java版本,發現是32位的(我電腦是64位的),這就可以解釋通。
D:\android-sdk-windows-4.3\platform-tools>java -version
java version “1.7.0_51”
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) Client VM (build 24.51-b03, mixed mode, sharing)
4、構造一個有內存泄漏的代碼:
MainActivity.java:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void eventClick(View v) {//點擊跳轉到第二個頁面
Intent intent = new Intent(this, Second.class);
startActivity(intent);
}
}
Second.java:
public class Second extends Activity {
private List<String> list = new ArrayList<String>();//全局的list
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
// 模擬Activity一些其他的對象
for (int i = 0; i < 200000; i++) {
list.add(new String("hello world!"));
}
new MyThread().start();// 開啓線程
}
public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {// 模擬耗時操作,線程開啓10分鐘
Thread.sleep(1000*60*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5、獲取hprof文件:
mat分析需要導出hprof文件。可以先導出泄漏之前的hprof文件,在操作app,導出泄漏之後的hprof。
操作之後的內存圖:
dump hprof文件
6、轉化hprof文件:
cmd到當前的目錄下,把你需要轉化的hprof文件也可以拷貝到當前的目錄
hprof-conv 1.hprof before.hprof
7、使用mat打開文件開始分析-直方圖:
這個時候我們獲取了操作之前的before.hprof文件 和 操作之後的 after.hprof文件。
打開mat工具,導入(Open Heap Dump ...)hprof開始分析。
主界面
我們分析最常用到的就是Histogram(直方圖)和 Dorminator Tree(支配樹)
a.打開Histogram(直方圖)他列舉了每個對象的統計。它可以列出任意一個類的實例數。
比如你需要查看MainActivity,可以使用正則表達式 .*Main.* (注意大小寫)
可以列出與MainActivity相關的類
b.選中com.example.testleakmemory.Second,右擊,選擇“Merge Shortest Paths to GC Roots”,
再選擇選擇“exclude all phantom/weak/soft etc.references”
(排查虛引用/弱引用/軟引用等)因爲被虛引用/弱引用/軟引用的對象可以直接被GC給回收.
在JAVA中是通過可達性(Reachability Analysis)來判斷對象是否存活,這個算法的基本思想是通過一系列的稱謂"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索所走得路徑稱爲引用鏈
c.如果存在GC Roots鏈,即存在內存泄露問題。
除了使用Merge Shortest Paths to GC Roots 我們還可以使用
List object - With outgoing References 顯示選中對象持有那些對象
List object - With incoming References 顯示選中對象被那些外部對象所持有
Show object by class - With outgoing References 顯示選中對象持有哪些對象, 這些對象按類合併在一起排序
Show object by class - With incoming References 顯示選中對象被哪些外部對象持有, 這些對象按類合併在一起排序
8、使用mat打開文件開始分析-Dorminator Tree支配樹):
a.通過 Dorminator Tree(支配樹),可以直觀地反映一個對象的retained heap,根據retained heap進行排序.
shallow heap:指的是某一個對象所佔內存大小。
retained heap:指的是一個對象的retained set所包含對象所佔內存的總大小。
它主要可以用於診斷一個對象所佔內存爲什麼會不斷膨脹,一個對象膨脹,就說明它對應到支配樹中的子樹就越來越龐大。
b.通過dominator_tree查看單一對象所持有的對象
右擊對象--Java Basics -- Open In Dominator Tree
很實用
注意:MAT工具上顯示的size的大小單位是: Bytes 。例如 21414072 = 20.4M
在MAT中可以查看到有類似如下的顯示
com.example.main.MainActivity$1
com.example.main.MainActivity$2
這個 $1 表示第一個匿名類的大小。
$2、$3這樣排下去。
9、使用的小技巧1:
a.操作前後兩個hprof的對比:
dump出前後2個hprof文件
使用mat工具打開,在Navigation History這個視圖中,右擊histogram--Add to Compare Basket
Window -- Compare Basket,裏面會有2個histogram。點擊對比就可以了。
上面這個對比結果不利於查找差異,還可以調整對比選項
Difference from base Table
10、使用的小技巧2:
我們使用對比發現了操作後的內存中多了很多 char[] (我們構造的20萬個String未釋放)。
在after.hprof的直方圖視圖中
a.可以右擊 -- Immediate dominators(查看引用者) -- 可以發現Second這個activity持有20萬個,基本可以判斷Second這個頁面有內存泄露。
我們再查看那個變量持有了這麼多。
b.右擊這個Second -- 選擇 Dominated Objects(注意一定是這個不要選擇Objects裏面的) -- Merge Shortest Paths to GC Roots --
exclude all phantom/weak/soft etc.references
c.一步步看誰持有了這個Second不釋放
備註:把這個問題解決了之後,再分析發現還有個輸入法問題。
android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper
.
.
list
可以確定是輸入法持有了activity的引用,導致Second activity裏面list這個變量未正常釋放。
(輸入法是單例的,只會持有一個Second的,所以不管你打開多少次這個頁面,泄露的大小是固定的。網上查閱這個是系統的一個bug,
如果頁面佔用內存不是太多,主要是全局變量,那麼泄露的就可以接受)
11、實際中遇到的內存泄漏問題:
在實際的應用中遇到了以下一些的內存泄露問題:
a.將一些View定義爲static,例如窗口顯示:我在其它的類裏面直接調用靜態的方法來設置靜態的View上面的文本。
因爲static的生命週期很長,導致這個View持有activity的引用不釋放,導致activity銷燬了,資源還得不到釋放(如果有圖片資源泄露的更多)。
解決的方法:
將View不要定義爲static,Handler類定義爲靜態的,使用弱引用來持有activity的引用。
b.在工具類Information類中,定義了一個靜態的WiFiManager變量wm
在使用的時候,直接將context傳到這個工具類中,wm使用到了這個context獲取服務,activity銷燬了,wm不會銷燬,導致activity資源不釋放,內存泄露。
解決的方法:
在工具類中不用傳遞進來的context,直接獲取全局的Context
或者context.getApplicationContext();來獲取
c.將context傳進了測試類中,類裏面又將context定義爲static,導致activity銷燬內存不釋放。(和上面的情況類似)
解決方式:
測試完成,將靜態的context設置爲null,斷掉GC Root鏈路
d.使用第三方的jar包,顯示gif圖像導致的內存泄露:
我們啓動了gif,但是jar提供的停止的方法中不能停止它裏面的子線程。
使用mat分析到時jar包裏面的子線程引用了activity,這樣子線程不結束,activity就釋放不了
解決方法:
我們沒有jar的源碼,通過查看class顯示的私有屬性有個isRunning,但是沒有提供方法設置。我通過反射去設置了這個變量的屬性。問題解決,可以正常釋放了。
if(null != gv_toolbox_distribute_gif){
try {
Class<? extends GifView> clazz = gv_toolbox_distribute_gif.getClass();
Field f = clazz.getDeclaredField("isRun");
f.setAccessible(true);
f.set(gv_toolbox_distribute_gif, false);
} catch (Exception e) {
LogUtils.d(TAG, "失敗:" + e.getMessage());
}
}
e.自定義一個WaveCircle動畫水波紋,裏面使用了postDelay方法。導致內存泄露(網上有比較多的這個分析案例)
通過mat工具分析發現,有個ThreadLocal老是持有activity,繼續往下看,在getRunQueue裏面有2個Runnable未執行。
後面解決方法:
1.在調用stop動畫的時候睡500ms,測試有效。
2.我使用發消息的方法stop動畫,測試也有效(暫未研究源碼,也許是消息執行需要等待一段時間)
f.有個等待的狀態Dialog沒有正常關閉
通過mat分析到時有個WindowManger持有了activity的引用不釋放。後面查代碼發現dialog根本就沒有關閉。
解決方法:
在dialog顯示完成結束的時候,需要手動dismiss掉這個dialog,實際測試OK
g.使用了幀動畫,沒有正常結束
通過mat分析到有很大的Bitmap未釋放,把原圖導出來看下,發現是幀動畫的原圖,檢查代碼發現,原來開啓了動畫,當時沒有結束動畫。
解決方法:
在動畫顯示完成的時候stop掉就OK了。
**總結:
以上只是分析了一個簡單的例子,基本的流程就是這樣,以後遇到了複雜的問題,按照這個思路來分析也能逐步的找到問題的所在。
平時我們寫代碼的時候要多注意這些性能的問題。能避免的儘量避免,不然遇到了泄漏的問題後面來分析花費的代價會更大。
好的習慣很重要!!!**