Profiler入門系列(一)——記憶體分析
0.前言
Android+Studio/">Android Studio升級到3.0以後DDMS入口不見了,不要著急,取而代之的是 Layout Inspector , File Explorer 以及 Profiler 等新工具。很多人對新工具還不是很瞭解,Profiler是一個分析app效能的強大工具合輯,可以分析記憶體、cpu、啟動時間、網路情況、功耗等各個指標,今天先來看看Profiler如何分析應用的記憶體情況吧。
1.開啟Profiler
如何開啟Profiler呢?點選選單欄上的Run -> Profiler ‘app’可以開始分析app效能。不過我更喜歡用 Ctrl+Shift+a ,執行find action命令,搜尋profiler,可以不用滑鼠更快地開啟Profiler。

find-action-profiler.png
app啟動好後,profiler也會起來,主介面如下:

Profiler主介面.png
點選左上角紅色的按鈕可以停止分析,想要再次分析就點左邊的+號開始新的會話。
可以看到主介面一直在跑的有四個效能指標,分別是CPU、MEMORY、NETWORK、ENERGY,今天我們是來看記憶體分析的,所以點選MEMORY看看。
2.進入Memory分析介面

Memory介面.png
可以看到,這裡以四種顏色分別表示四種記憶體佔用情況,其中我們比較關心的Native和Java堆記憶體在最下面,把滑鼠放上去可以看到具體的數值。

force-gc.png
點選左上的垃圾桶可以強制進行gc垃圾回收,在定位記憶體洩漏的時候很有用。
3.檢視Java堆記憶體和具體Java例項
點選任意一個時間點可以看到當前的java堆記憶體情況,下面來做個實驗,在當前Activity新增幾個自定義物件Person並儲存到一個成員變數集合中:
public class MainActivity extends AppCompatActivity { List<Person> persons = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); persons.add(new Person("zhangsan",23)); persons.add(new Person("lisi",24)); persons.add(new Person("wangwu",25)); } private class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } } }
按道理來說,被成員變數persons集合持有後,這三個例項肯定不會被回收,我們來看下是不是這樣,點選啟動後的任意一個時間點,下方會列出所有的java物件,然後我們點選右側的搜尋,搜尋到Person

檢視搜尋記憶體例項.png
可以看到Person物件的totalsize是3,點選後右側顯示的正好是三個例項。現在我們明白瞭如何去查詢某個時間點記憶體物件的數量了。
當然你的模擬器或測試機可能不支援這種隨時點選任意時間點就能看到Java堆的feature(看官網寫的好像是要8.0以上),沒關係點選垃圾桶右邊的Dump Java Heap按鈕一樣可以列印Java堆的情況。列印下來的檔案可以在左側另存為hprof檔案,可以用MAT等記憶體分析工具進行分析。
4.記憶體洩漏分析
Android記憶體優化最常見的問題就是發現並解決記憶體洩漏了,其中又以Activity洩漏最為常見。下面我們來手寫一個會發生洩漏的Activity(沒用問題製造問題也要上- -),看看怎麼用Profiler定位洩漏問題:
public void openLeakActivity(View view) { startActivity(new Intent(this, LeakActivity.class)); }
建立一個新的LeakActivity,並在MainActivity新增一個啟動LeakActivity的按鈕。然後我們來給LeakActivity製造洩漏。
public class LeakActivity extends AppCompatActivity { private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_leak); mHandler.sendEmptyMessageDelayed(0, 24 * 60 * 60); } }
Activity洩漏的經典案例:Handler持有造成的洩漏。handler持有Activity物件,讓Handler延時傳送訊息這樣Handler在訊息發完之前就不會回收,Activity也跟著無法銷燬。其實這段程式碼寫出來就會被Android Studio瘋狂diss,用大大的黃塊告訴你Handler應當用static修飾,否則可能會發生洩漏。但是現在我們要的就是洩漏。開啟應用,開啟Profiler,不斷反覆開啟、關閉LeakActivity,我這裡開啟關閉了六次,看下記憶體的波動情況:

LeakActivity記憶體洩漏.png
我這裡圖沒截好,不過也能看出來趨勢了,每次開啟LeakActivity記憶體都會上漲一點點。這裡我忘記點force gc了,當然由於handler無法銷燬,gc自然也是沒用的。
現在我們可以確認LeakActivity存在洩漏了,那如何定位洩漏原因呢?我們先Dump一下Java堆,來查詢一下LeakActivity:

分析洩漏原因.png
果不其然,開啟的六個LeakActivity一個也沒有銷燬,右側點選任意一個LeakActivity,我們來看看引用情況。
很明顯可以看到,前面的八個this$0引用都是值得那一個Handler匿名內部類,這樣我們就輕鬆定位到了洩漏原因。原因找到了,再根據記憶體引用的知識去修改就容易了。這裡我們只要根據提示把匿名內部類Handler改為靜態內部類就可以,如過引用了成員變數無法用static修飾,就使用弱引用去獲取物件。
好了,使用Profiler去分析記憶體是不是輕鬆加愉快呢?不要再怕定位記憶體洩漏啦!