Android內存與性能

Android內存與性能。

GC 的工作機制
當 GC 工作時,虛擬機停止其他工作。頻繁地觸發 GC 進行內存回收,會導致系統性能嚴重下降。

內存抖動
在極短的時間內,分配大量的內存,然後又釋放它,這種現象就會造成內存抖動。典型地,在 View 控件的 onDraw 方法裡分配大量內存,又釋放大量內存,這種做法極易引起內存抖動,從而導致性能下降。因為 onDraw 裡的大量內存分配和釋放會給系統堆空間造成壓力,觸發 GC 工作去釋放更多可用內存,而 GC 工作起來時,又會吃掉寶貴的幀時間 (幀時間是 16ms) ,最終導致性能問題。

內存泄漏
Java 語言的內存泄漏概念和 C/C++ 不太一樣,在 Java 裡是指不正確地引用導致某個對象無法被 GC 釋放,從而導致可用內存越來越少。比如,一個圖片查看程序,使用一個靜態 Map 實例來緩存解碼出來的 Bitmap 實例來加快加載進度。這個時候就可能存在內存泄漏。

內存泄漏會導致可用內存越來越少,從而導致頻繁觸發 GC 回收內存,進而導致性能下降。

調試工具

Memory Monitor Tool: 可以查閱 GC 被觸發起來的時間序列,以便觀察 GC 是否影響性能。
Allocation Tracker Tool: 從 Android Studio 的這個工具裡查看一個函數調用棧裡,是否有大量的相同類型的 Object 被分配和釋放。如果有,則其可能引起性能問題。
MAT: 這是 Eclipse 的一個插件,也有stand alone的工具可以下載使用。

幾個原則

別在循環裡分配內存 (創建新對象)
盡量別在 View 的 onDraw 函數裡分配內存
實在無法避免在這些場景裡分配內存時,考慮使用對象池 (Object Pool)

 

兩個簡單的實例

內存抖動

通過一個非常簡單的例子來演示內存抖動。這個例子裡,在自定義 View 的 onDraw 方法裡大量分配內存來演示內存抖動和性能之間的關系。

版本一:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        String msg = "";
        for (int i = 0; i < 500; i++) {
            if (i != 0) {
                msg += ", ";
            }
            msg += Integer.toString(i + 1);
        }
        Log.d("DEBUG", msg);
    }

版本二:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 500; i ++) {
            if (i != 0) {
                sb.append(", ");
            }
            sb.append(i + 1);
        }
        Log.d("DEBUG", sb.toString());
    }

內存抖動的特征:

從 Memory Monitor 來看,有毛刺出現。即短時間內分配大量的內存並觸發 GC。
memory_churn

從 Allocation Tracker 裡看,一次操作會有大量的內存分配產生。
memory_tracker

內存泄漏

這個例子裡,我們簡單地讓點擊 Settings 菜單,就產生一個 100KB 的內存泄漏。

    private void addSomeCache() {
        // add 100KB cache
        int key = new Random().nextInt(100);
        Log.d("sfox", "add cache for key " + key);
        sCache.put(key, new byte[102400]);
    }

內存泄漏的特征:

從 Memory Monitor 來看,內存占用越來越大
memory_tracker

利用MAT工具進行專業分析。這是個很大的話題。幾乎可以獨立成幾個章節來講。可以參閱 MAT 本身自帶的 Tutorials 來學習。另外,這篇文章裡的分析方法是個不錯的開始。

示例代碼使用 Android Studio 開發環境,可以從這裡下載。

利用 MAT 分析內存問題

內存泄漏

一個典型的問題是 Android 系統越用越慢。這種典型地是由內存泄漏引起的。一個很有用的解決這種問題的辦法是:比較前後兩個階段的內存的使用情況。一般流程如下:

利用 ddms 工具 dump HPROF file
利用 hprof-conv 把 dalvik 格式的轉換為普通 jvm 格式
重復步驟 1 和 2 抓出兩份 LOG。
利用 MAT 對兩份 HRPOF 文件進行分析,結合代碼找出可能存在的內存泄漏

比如針對撥號盤越來越慢的問題,我們可以開機後啟動撥號盤,打進打出10個電話。然後抓個 HPROF 文件。接著,再打進打出10個電話,再抓一個 HPROF 文件。接著拿這兩個文件對比分析,看是不是會造成電話打進打出越多,內存占用越多的情況發生。

HPROF文件

HPROF 簡單地理解,就是從 jvm 裡 dump 出來的內存和 CPU 使用情況的一個二進制文件。它的英文全名叫 A Heap/CPU Profiling Tool。這裡有它完整的官方文檔和它的歷史介紹。

打開 MAT 後,會有一個 Tutorials 來教大傢怎麼用。這裡列出幾個操作步驟及其註意事項。

在 DDMS 裡導出 HPROF 文件前,最好手動執行一下 GC。目的是讓導出的內存全部是被引用的。否則在做內存占用對比時,會有很多不必要的內存占用被標識出來,幹擾我們進行分析。
進行對比時,最好是選擇操作較多的和操作較少的對比,這樣得出的 delta 是正數
通過對比,發現內存泄漏時,可以用 OQL 來查詢,並通過 Root to GC 功能來找到發生泄漏的源代碼

在我們的示例程序裡面,每次點擊 Settings 菜單,都會導致一次100KB的內存泄漏。下面是我們利用上面介紹的流程來查找內存泄漏問題。我們先點擊 5 次 Settings 菜單,然後手動觸發一次 GC,再導出 HPROF 文件。接著,我們再點擊 6 次 Settings 菜單,然後手動觸發一次 GC,再導出第二份 HPROF 文件。我們拿這兩份 HPROF 就可以做一些對比。

mat_diff.png

通過上圖可以看到,兩次操作確實導致瞭某些類的實例增加瞭。圖中可以清楚地看到 byte[] 和 java.util.HashMap$HashMapEntry 兩個類增加得比較明顯。這樣,我們隨便選擇一個,通過 OQL 來查詢系統中的這個內存。

mat_qql.png

從上圖可以找到,本次 dump 出來的內存裡,確實有很多個這個類的實例。在圖上右擊任何一個實例,右擊,選擇Paths to GC roots,可以找到這個實例是被誰引用的。

mat_gc_root.png

從上圖可以看出來,這個內存是被 MainActivity 裡的 sCache 引用的。通過閱讀代碼,我們就可以找到這個漏洞瞭。即每次都往 sCache 裡保存一個引用。

總結

Google 視頻介紹的內容是硬知識,瞭解這些知識可以幫助我們寫出高質量,高性能的代碼。而 MAT, HPROF, Memory Monitor, Allocation Tracker 提供瞭一個“破案”的工具給我們。我們利用這些工具,倒回來去發現代碼裡的問題。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *