1 概述
本篇博客是Android App性能優(yōu)化專(zhuān)題的第一篇文章,該專(zhuān)題會(huì)在渲染辆飘、計(jì)算啦辐、內(nèi)存谓传、電量方面進(jìn)行講解Android App的性能優(yōu)化。
為了更加高效的優(yōu)化App性能芹关,Android Studio提供了一個(gè)Android Monitor工具续挟,它位于Android Studio主窗口的下方,Android Monitor提供了實(shí)時(shí)記錄和觀察App以下信息的工具:
- 系統(tǒng)或用戶定義的Log信息
- Memory侥衬,CPU和GPU使用率
- Network流量(僅限硬件設(shè)備)
2 預(yù)備知識(shí)
2.1 Android App的內(nèi)存結(jié)構(gòu)
Random Access Memory(RAM)在任何軟件開(kāi)發(fā)環(huán)境中都是一個(gè)很寶貴的資源诗祸。這一點(diǎn)在物理內(nèi)存通常很有限的移動(dòng)操作系統(tǒng)上,顯得尤為突出轴总。系統(tǒng)會(huì)在RAM上為App進(jìn)程分配固定的內(nèi)存空間直颅,然后該App進(jìn)程就會(huì)運(yùn)行在該內(nèi)存空間上。該內(nèi)存空間會(huì)被分成Stack內(nèi)存空間和Heap內(nèi)存空間怀樟,其中Stack內(nèi)存空間里存放對(duì)象的引用功偿,Heap內(nèi)存空間里存放對(duì)象數(shù)據(jù)。
在Android的高級(jí)系統(tǒng)版本里面針對(duì)Heap內(nèi)存空間有一個(gè)3級(jí)的Generational Heap Memory的模型往堡,它包括Young Generation械荷,Old Generation,Permanent Generation三個(gè)區(qū)域虑灰。最新分配的對(duì)象會(huì)存放在Young Generation區(qū)域吨瞎,當(dāng)這個(gè)對(duì)象在這個(gè)區(qū)域停留的時(shí)間超過(guò)某個(gè)值的時(shí)候,會(huì)被移動(dòng)到Old Generation瘩缆,最后到Permanent Generation區(qū)域关拒。整個(gè)結(jié)構(gòu)如下圖所示:
這3個(gè)區(qū)域都有固定的大小,隨著新的對(duì)象陸續(xù)被分配到此區(qū)域庸娱,當(dāng)這些對(duì)象總的大小快達(dá)到該區(qū)域的大小時(shí),會(huì)觸發(fā)GC的操作谐算,以便騰出空間來(lái)存放其他新的對(duì)象熟尉,如下圖所示:
最近剛分配的對(duì)象會(huì)放在Young Generation區(qū)域,這個(gè)區(qū)域的GC操作速度也是比Old Generation區(qū)域的GC操作速度更快的洲脂,如下圖所示:
通常情況下斤儿,當(dāng)GC線程運(yùn)行時(shí),其他線程會(huì)暫停工作(包括UI線程)恐锦,直到GC完成往果,如下圖所示:
雖然單個(gè)的GC操作并不會(huì)占用太多時(shí)間,但是頻繁的GC操作有可能會(huì)影響到幀率一铅,導(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)過(guò)X,那么我們稱(chēng)X dominates Y潘飘,或者X是Y的Dominator tree肮之。當(dāng)優(yōu)化內(nèi)存時(shí)掉缺,可以通過(guò)釋放一個(gè)dominator對(duì)象來(lái)釋放其所有下級(jí)對(duì)象。 例如戈擒,在下圖中眶明,如果要?jiǎng)h除對(duì)象B,那么也會(huì)釋放其所主導(dǎo)的對(duì)象所使用的內(nèi)存筐高,即對(duì)象C搜囱,D,E和F柑土,實(shí)際上犬辰,如果對(duì)象C,D冰单, E和F被標(biāo)記為刪除幌缝,但對(duì)象B仍然指向它們,這可能是它們未被釋放的原因诫欠。
3 Memory Monitor
Android Monitor提供了Memory Monitor工具涵卵,以便更輕松地實(shí)時(shí)監(jiān)聽(tīng)App的性能和內(nèi)存使用情況,通過(guò)該工具可以:
- 顯示空閑和已分配的Java內(nèi)存隨時(shí)間變化的圖表荒叼。
- 隨著時(shí)間的推移顯示垃圾回收(GC)事件轿偎。
- 啟動(dòng)GC事件。
- 快速測(cè)試UI線程卡頓是否與頻繁GC事件有關(guān)被廓。
當(dāng)GC線程運(yùn)行時(shí)坏晦,其他線程都會(huì)暫停(包括UI線程),直到GC完成嫁乘。頻繁GC操作有可能會(huì)影響到幀率昆婿,導(dǎo)致卡頓,特別是性能比較差的手機(jī)上蜓斧,尤為明顯仓蛆。 - 快速測(cè)試app崩潰是否與內(nèi)存不足(內(nèi)存溢出或者內(nèi)存泄漏)有關(guān)。
Memory Monitor的工作流程
為了分析和優(yōu)化內(nèi)存使用挎春,典型的工作流程是運(yùn)行app并執(zhí)行以下操作:
- 使用Memory Monitor來(lái)分析是否由于不良GC事件模式導(dǎo)致的app性能問(wèn)題看疙。
- 如果在短時(shí)間內(nèi)產(chǎn)生頻繁的GC事件,就通過(guò)Dump Java Heap操作來(lái)查看當(dāng)前內(nèi)存快照直奋,繼而查找哪些類(lèi)型的對(duì)象有可能發(fā)生了內(nèi)存泄漏或者占用了太大內(nèi)存能庆。
- 最后通過(guò)Start allocation tracking操作來(lái)追蹤對(duì)象分配內(nèi)存時(shí)對(duì)應(yīng)的方法調(diào)用。
在Memory Monitor中執(zhí)行Dump Java Heap操作時(shí)脚线,會(huì)創(chuàng)建一個(gè)Android-specific Heap/CPU Profiling (HPROF)文件搁胆,HPROF文件中保存了app該時(shí)刻內(nèi)存中的GC root列表,文件創(chuàng)建完成后會(huì)自動(dòng)在HPROF Viewer中打開(kāi), HPROF Viewer使用
圖標(biāo)標(biāo)示GC root(深度為零)以及使用
4 常見(jiàn)內(nèi)存性能問(wèn)題模擬及優(yōu)化
4.1 內(nèi)存抖動(dòng)現(xiàn)象模擬及優(yōu)化
內(nèi)存抖動(dòng)是因?yàn)樵诙虝r(shí)間內(nèi)大量的對(duì)象被創(chuàng)建又馬上被釋放導(dǎo)致的拓巧。因此下面的例子中我通過(guò)一個(gè)for循環(huán)來(lái)不斷的創(chuàng)建和釋放對(duì)象來(lái)模擬內(nèi)存抖動(dòng)的現(xiàn)象。
舉個(gè)例子:
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)存抖動(dòng)");
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();
}
}
上面代碼很簡(jiǎn)單一死,當(dāng)點(diǎn)擊模擬內(nèi)存抖動(dòng)按鈕時(shí)肛度,通過(guò)Memory Monitor工具可以看到出現(xiàn)了非常明顯的內(nèi)存抖動(dòng)情況,如下圖所示:
當(dāng)內(nèi)存抖動(dòng)的峰值快達(dá)到Y(jié)oung Generation區(qū)域的容量時(shí)就會(huì)觸發(fā)GC操作投慈,因此為了觸發(fā)GC操作承耿,就在代碼中加載來(lái)一張非常大圖片(5184*3456),對(duì)應(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
通過(guò)上面的log中的時(shí)間點(diǎn)證明了發(fā)生了頻繁的GC, 由于導(dǎo)致GC的原因是Alloc(可以參考調(diào)查 RAM 使用情況來(lái)理解GC Log)伪煤,因此可能會(huì)在不久的將來(lái)會(huì)發(fā)生OOM異常加袋;當(dāng)我點(diǎn)擊兩次按鈕時(shí),確實(shí)引發(fā)OOM異常抱既;頻繁GC操作有可能會(huì)影響到幀率职烧,導(dǎo)致卡頓。
Allocation Tracker功能(可以參考Allocation Tracker)對(duì)于識(shí)別和優(yōu)化內(nèi)存抖動(dòng)是非常有效的防泵,接下來(lái)就通過(guò)這個(gè)功能來(lái)定位上面發(fā)生內(nèi)存抖動(dòng)的位置:
點(diǎn)擊右下角紅色框中的按鈕蚀之,即開(kāi)始執(zhí)行Allocation Tracker,等一段時(shí)間捷泞,再點(diǎn)擊一下右下角紅色框中的按鈕就會(huì)停止Allocation Tracker足删,此時(shí)下面的波形圖中矩形區(qū)域就是Allocation Tracker執(zhí)行的周期,并且會(huì)生成和打開(kāi)一個(gè)alloc格式的文件锁右,通過(guò)上圖可知分配內(nèi)存最多的是Thread 22線程失受,打開(kāi)Thread 22線程的調(diào)用stack,定位到TestLeakActivity1的34行就是分配內(nèi)存的位置咏瑟,接下去的問(wèn)題修復(fù)也就顯得相對(duì)簡(jiǎn)單了拂到,盡量避免在for循環(huán)里面分配對(duì)象,嘗試把對(duì)象的創(chuàng)建移到循環(huán)體之外响蕴,對(duì)于那些無(wú)法避免需要?jiǎng)?chuàng)建對(duì)象的情況谆焊,我們可以考慮對(duì)象池模型,通過(guò)對(duì)象池來(lái)解決頻繁創(chuàng)建與銷(xiāo)毀的問(wèn)題浦夷,注意在對(duì)象池沒(méi)用時(shí)需要手動(dòng)釋放對(duì)象池中的對(duì)象。
4.2 內(nèi)存泄漏現(xiàn)象模擬及優(yōu)化
內(nèi)存泄漏是指不再用到的對(duì)象由于被錯(cuò)誤引用而無(wú)法被GC回收辜王,這樣就導(dǎo)致這個(gè)對(duì)象一直留在內(nèi)存當(dāng)中劈狐,占用了寶貴的內(nèi)存空間。顯然會(huì)導(dǎo)致每級(jí)Generation的內(nèi)存區(qū)域可用空間變小呐馆,GC就會(huì)更容易被觸發(fā)肥缔,從而引起性能問(wèn)題。
舉個(gè)例子:
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);
}
};
}
上面的代碼很簡(jiǎn)單汹来,運(yùn)行App续膳,多次并且快速執(zhí)行操作(從TestLeakActivity2跳轉(zhuǎn)到TestLeakActivity3改艇,然后再回到TestLeakActivity2),接著利用Memory Monitor工具的Dump Java Heap功能(可以參考HPROF Viewer and Analyzer)列舉此時(shí)Heap中各種類(lèi)型對(duì)象的多少和大蟹夭怼:
點(diǎn)擊右上角的箭頭谒兄,可以解析出當(dāng)前泄漏的activity,然后選中instance窗口中的第一個(gè)泄漏的實(shí)例社付,下面的reference tree窗口就會(huì)立即顯示該實(shí)例對(duì)應(yīng)的reference tree承疲,可以看出:
1> TestLeakActivity3$1@316570880是TestLeakActivity3@315071952的Dominator tree。
2> TestLeakActivity3$1@316570880的類(lèi)型是Message中target的類(lèi)型鸥咖,即Handler類(lèi)型燕鸽。
3> 由于TestLeakActivity3$1@316570880通過(guò)this$0引用TestLeakActivity3@315071952,因此TestLeakActivity3$1是TestLeakActivity3的內(nèi)部類(lèi)啼辣。
4> target和handler是同一個(gè)實(shí)例(TestLeakActivity3$1@316570880)啊研,并且handler是TestLeakActivity3@315071952的一個(gè)屬性。
由上面的4條信息可以得出只要TestLeakActivity3中handler的生命周期在TestLeakActivity3生命周期之內(nèi)鸥拧,就可以避免TestLeakActivity3實(shí)例的泄漏党远,接下去的問(wèn)題修復(fù)也就顯得相對(duì)簡(jiǎn)單了,就不在贅敘了住涉。