Android App性能優(yōu)化--內(nèi)存篇

1 概述

本篇博客是Android App性能優(yōu)化專題的第一篇文章,該專題會在渲染、計算、內(nèi)存、電量方面進(jìn)行講解Android App的性能優(yōu)化。

為了更加高效的優(yōu)化App性能,Android Studio提供了一個Android Monitor工具,它位于Android Studio主窗口的下方,Android Monitor提供了實時記錄和觀察App以下信息的工具:

  • 系統(tǒng)或用戶定義的Log信息
  • Memory,CPU和GPU使用率
  • Network流量(僅限硬件設(shè)備)

2 預(yù)備知識

2.1 Android App的內(nèi)存結(jié)構(gòu)

Random Access Memory(RAM)在任何軟件開發(fā)環(huán)境中都是一個很寶貴的資源。這一點在物理內(nèi)存通常很有限的移動操作系統(tǒng)上,顯得尤為突出。系統(tǒng)會在RAM上為App進(jìn)程分配固定的內(nèi)存空間,然后該App進(jìn)程就會運行在該內(nèi)存空間上。該內(nèi)存空間會被分成Stack內(nèi)存空間和Heap內(nèi)存空間,其中Stack內(nèi)存空間里存放對象的引用,Heap內(nèi)存空間里存放對象數(shù)據(jù)。

在Android的高級系統(tǒng)版本里面針對Heap內(nèi)存空間有一個3級的Generational Heap Memory的模型,它包括Young Generation,Old Generation,Permanent Generation三個區(qū)域。最新分配的對象會存放在Young Generation區(qū)域,當(dāng)這個對象在這個區(qū)域停留的時間超過某個值的時候,會被移動到Old Generation,最后到Permanent Generation區(qū)域。整個結(jié)構(gòu)如下圖所示:



這3個區(qū)域都有固定的大小,隨著新的對象陸續(xù)被分配到此區(qū)域,當(dāng)這些對象總的大小快達(dá)到該區(qū)域的大小時,會觸發(fā)GC的操作,以便騰出空間來存放其他新的對象,如下圖所示:



最近剛分配的對象會放在Young Generation區(qū)域,這個區(qū)域的GC操作速度也是比Old Generation區(qū)域的GC操作速度更快的,如下圖所示:

通常情況下,當(dāng)GC線程運行時,其他線程會暫停工作(包括UI線程),直到GC完成,如下圖所示:



雖然單個的GC操作并不會占用太多時間,但是頻繁的GC操作有可能會影響到幀率,導(dǎo)致卡頓。

2.2 GC root and Dominator tree

Java中有以下幾種GC root:

  • references on the stack
  • Java Native Interface (JNI) native objects and memory
  • static variables and functions
  • threads and objects that can be referenced
  • classes loaded by the bootstrap loader
  • finalizers and unfinalized objects
  • busy monitor objects

如果從GC Root到達(dá)Y的的所有path都經(jīng)過X,那么我們稱X dominates Y,或者X是Y的Dominator tree。當(dāng)優(yōu)化內(nèi)存時,可以通過釋放一個dominator對象來釋放其所有下級對象。 例如,在下圖中,如果要刪除對象B,那么也會釋放其所主導(dǎo)的對象所使用的內(nèi)存,即對象C,D,E和F,實際上,如果對象C,D, E和F被標(biāo)記為刪除,但對象B仍然指向它們,這可能是它們未被釋放的原因。


3 Memory Monitor

Android Monitor提供了Memory Monitor工具,以便更輕松地實時監(jiān)聽App的性能和內(nèi)存使用情況,通過該工具可以:

  • 顯示空閑和已分配的Java內(nèi)存隨時間變化的圖表。
  • 隨著時間的推移顯示垃圾回收(GC)事件。
  • 啟動GC事件。
  • 快速測試UI線程卡頓是否與頻繁GC事件有關(guān)。
    當(dāng)GC線程運行時,其他線程都會暫停(包括UI線程),直到GC完成。頻繁GC操作有可能會影響到幀率,導(dǎo)致卡頓,特別是性能比較差的手機上,尤為明顯。
  • 快速測試app崩潰是否與內(nèi)存不足(內(nèi)存溢出或者內(nèi)存泄漏)有關(guān)。

Memory Monitor的工作流程
為了分析和優(yōu)化內(nèi)存使用,典型的工作流程是運行app并執(zhí)行以下操作:

  1. 使用Memory Monitor來分析是否由于不良GC事件模式導(dǎo)致的app性能問題。
  2. 如果在短時間內(nèi)產(chǎn)生頻繁的GC事件,就通過Dump Java Heap操作來查看當(dāng)前內(nèi)存快照,繼而查找哪些類型的對象有可能發(fā)生了內(nèi)存泄漏或者占用了太大內(nèi)存。
  3. 最后通過Start allocation tracking操作來追蹤對象分配內(nèi)存時對應(yīng)的方法調(diào)用。

在Memory Monitor中執(zhí)行Dump Java Heap操作時,會創(chuàng)建一個Android-specific Heap/CPU Profiling (HPROF)文件,HPROF文件中保存了app該時刻內(nèi)存中的GC root列表,文件創(chuàng)建完成后會自動在HPROF Viewer中打開, HPROF Viewer使用


圖標(biāo)標(biāo)示GC root(深度為零)以及使用


圖標(biāo)標(biāo)示Dominator tree。

4 常見內(nèi)存性能問題模擬及優(yōu)化

4.1 內(nèi)存抖動現(xiàn)象模擬及優(yōu)化

內(nèi)存抖動是因為在短時間內(nèi)大量的對象被創(chuàng)建又馬上被釋放導(dǎo)致的。因此下面的例子中我通過一個for循環(huán)來不斷的創(chuàng)建和釋放對象來模擬內(nèi)存抖動的現(xiàn)象。
舉個例子:

public class TestLeakActivity1 extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Button click = new Button(this);
        click.setOnClickListener(this);
        click.setText("模擬內(nèi)存抖動");
        setContentView(click);
    }

    @Override
    public void onClick(View v) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    Bitmap result1;
                    result1 = BitmapFactory.decodeResource(getResources(), R.drawable.noah_silliman);
                }
            }
        }).start();
    }
}

上面代碼很簡單,當(dāng)點擊模擬內(nèi)存抖動按鈕時,通過Memory Monitor工具可以看到出現(xiàn)了非常明顯的內(nèi)存抖動情況,如下圖所示:


當(dāng)內(nèi)存抖動的峰值快達(dá)到Y(jié)oung Generation區(qū)域的容量時就會觸發(fā)GC操作,因此為了觸發(fā)GC操作,就在代碼中加載來一張非常大圖片(5184*3456),對應(yīng)的GC log如下:

08-22 10:53:51.579 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 39% free, 17MB/29MB, paused 492us total 52.970ms
08-22 10:53:51.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 370us total 36.902ms
08-22 10:53:52.329 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 365us total 36.754ms
08-22 10:53:52.664 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.072ms
08-22 10:53:52.952 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 8.791ms for cause Alloc
08-22 10:53:52.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.178ms
08-22 10:53:53.396 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 9.036ms for cause Alloc
08-22 10:53:53.444 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 493us total 43.976ms
08-22 10:53:53.809 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 11.791ms for cause Alloc
08-22 10:53:53.853 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 373us total 38.598ms
08-22 10:53:54.181 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 311us total 32.794ms
08-22 10:53:54.617 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 18(736B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 481us total 46.280ms

通過上面的log中的時間點證明了發(fā)生了頻繁的GC, 由于導(dǎo)致GC的原因是Alloc(可以參考調(diào)查 RAM 使用情況來理解GC Log),因此可能會在不久的將來會發(fā)生OOM異常;當(dāng)我點擊兩次按鈕時,確實引發(fā)OOM異常;頻繁GC操作有可能會影響到幀率,導(dǎo)致卡頓。

Allocation Tracker功能(可以參考Allocation Tracker)對于識別和優(yōu)化內(nèi)存抖動是非常有效的,接下來就通過這個功能來定位上面發(fā)生內(nèi)存抖動的位置:


點擊右下角紅色框中的按鈕,即開始執(zhí)行Allocation Tracker,等一段時間,再點擊一下右下角紅色框中的按鈕就會停止Allocation Tracker,此時下面的波形圖中矩形區(qū)域就是Allocation Tracker執(zhí)行的周期,并且會生成和打開一個alloc格式的文件,通過上圖可知分配內(nèi)存最多的是Thread 22線程,打開Thread 22線程的調(diào)用stack,定位到TestLeakActivity1的34行就是分配內(nèi)存的位置,接下去的問題修復(fù)也就顯得相對簡單了,盡量避免在for循環(huán)里面分配對象,嘗試把對象的創(chuàng)建移到循環(huán)體之外,對于那些無法避免需要創(chuàng)建對象的情況,我們可以考慮對象池模型,通過對象池來解決頻繁創(chuàng)建與銷毀的問題,注意在對象池沒用時需要手動釋放對象池中的對象。

4.2 內(nèi)存泄漏現(xiàn)象模擬及優(yōu)化

內(nèi)存泄漏是指不再用到的對象由于被錯誤引用而無法被GC回收,這樣就導(dǎo)致這個對象一直留在內(nèi)存當(dāng)中,占用了寶貴的內(nèi)存空間。顯然會導(dǎo)致每級Generation的內(nèi)存區(qū)域可用空間變小,GC就會更容易被觸發(fā),從而引起性能問題。

舉個例子:

public class TestLeakActivity2 extends AppCompatActivity implements View.OnClickListener {

    private Button testLeakBtn = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_leak2);
        testLeakBtn = (Button) findViewById(R.id.button_test_leak);
        testLeakBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(this, TestLeakActivity3.class);
        startActivity(intent);
    }
}

public class TestLeakActivity3 extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView imageView = new ImageView(this);
        imageView.setImageResource(R.drawable.noah_silliman);
        setContentView(imageView);
        handler.sendEmptyMessageDelayed(0, 60000);
    }

    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}

上面的代碼很簡單,運行App,多次并且快速執(zhí)行操作(從TestLeakActivity2跳轉(zhuǎn)到TestLeakActivity3,然后再回到TestLeakActivity2),接著利用Memory Monitor工具的Dump Java Heap功能(可以參考HPROF Viewer and Analyzer)列舉此時Heap中各種類型對象的多少和大?。?br>


點擊右上角的箭頭,可以解析出當(dāng)前泄漏的activity,然后選中instance窗口中的第一個泄漏的實例,下面的reference tree窗口就會立即顯示該實例對應(yīng)的reference tree,可以看出:
1> TestLeakActivity3$1@316570880是TestLeakActivity3@315071952的Dominator tree。
2> TestLeakActivity3$1@316570880的類型是Message中target的類型,即Handler類型。
3> 由于TestLeakActivity3$1@316570880通過this$0引用TestLeakActivity3@315071952,因此TestLeakActivity3$1是TestLeakActivity3的內(nèi)部類。
4> target和handler是同一個實例(TestLeakActivity3$1@316570880),并且handler是TestLeakActivity3@315071952的一個屬性。
由上面的4條信息可以得出只要TestLeakActivity3中handler的生命周期在TestLeakActivity3生命周期之內(nèi),就可以避免TestLeakActivity3實例的泄漏,接下去的問題修復(fù)也就顯得相對簡單了,就不在贅敘了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容