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í)行以下操作:
- 使用Memory Monitor來分析是否由于不良GC事件模式導(dǎo)致的app性能問題。
- 如果在短時間內(nèi)產(chǎn)生頻繁的GC事件,就通過Dump Java Heap操作來查看當(dāng)前內(nèi)存快照,繼而查找哪些類型的對象有可能發(fā)生了內(nèi)存泄漏或者占用了太大內(nèi)存。
- 最后通過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(深度為零)以及使用

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ù)也就顯得相對簡單了,就不在贅敘了。