什么是秒開
Android App秒開,狹義的講是指你的App的Activity從啟動(dòng)到顯示所花費(fèi)的時(shí)間在1秒以內(nèi)获枝,廣義的講是指這個(gè)過程所花費(fèi)的時(shí)間越少越好蠢正。這個(gè)時(shí)間越短,你的App給用戶的感覺就是響應(yīng)越快省店,使用越流暢嚣崭,用戶體驗(yàn)更好。秒開是Android App的一個(gè)很重要的性能指標(biāo)懦傍。需要我們持續(xù)的給予關(guān)注和優(yōu)化雹舀。
如何優(yōu)化秒開
Google提供了很多性能優(yōu)化的建議和官方的工具,網(wǎng)上也有非常多的關(guān)于Android App性能優(yōu)化的文章和工具粗俱,可以幫助你解決大部分卡頓的問題葱跋。但是現(xiàn)實(shí)卻可能是即使你付出了很多精力去做優(yōu)化,你的App還是在啟動(dòng)新Activity的時(shí)候花費(fèi)過多的時(shí)間源梭。特別是隨著需求的不斷增長(zhǎng),你的App會(huì)變得復(fù)雜而龐大稍味,要做優(yōu)化首先要定位需要優(yōu)化的點(diǎn)废麻,而這會(huì)變得愈發(fā)困難。同時(shí)大型App在啟動(dòng)新Activity的時(shí)間花費(fèi)過多情況出現(xiàn)的可能性反而會(huì)越來越大模庐。
在眾多的優(yōu)化建議中烛愧,有一條比較基本的原則是盡量避免在主線程(或者說UI線程)中進(jìn)行耗時(shí)操作。例如文件讀寫操作掂碱、網(wǎng)絡(luò)請(qǐng)求怜姿、大量計(jì)算、循環(huán)等等疼燥。直觀的理解是因?yàn)閱?dòng)新Activity需要在主線程執(zhí)行很多代碼沧卢,例如onCreate()等生命周期的回調(diào)。如果此時(shí)有耗時(shí)操作的代碼在主線程被執(zhí)行醉者,到新Activity展示出來所需要的時(shí)間就會(huì)延長(zhǎng)但狭。要優(yōu)化秒開,首先要能監(jiān)測(cè)主線程的運(yùn)行狀態(tài)撬即,那么問題來了立磁,主線程到底是怎樣在運(yùn)行呢?你的代碼又是什么時(shí)候剥槐,如何在主線程被執(zhí)行的呢唱歧?
深入主線程
要了解主線程的工作過程,首先要了解Android的消息機(jī)制粒竖。
消息機(jī)制
先看一下現(xiàn)實(shí)生活中的一個(gè)例子颅崩,雖然現(xiàn)在都是移動(dòng)支付了几于,但相信大家都去銀行取過錢。當(dāng)你到達(dá)銀行的時(shí)候挨摸,如果你是第一個(gè)孩革,那恭喜你,你可以馬上到柜員那里辦理你的業(yè)務(wù)得运;如果你前面還有人膝蜈,那就比較慘了,你需要排隊(duì)熔掺,得等到你前面的人都辦完業(yè)務(wù)才會(huì)輪到你饱搏;更可怕的是如果你前面有幾位需要辦理的業(yè)務(wù)花費(fèi)的時(shí)間比較長(zhǎng),那你需要等更長(zhǎng)的時(shí)間置逻;后面來的人則會(huì)按順序排在你身后推沸,和你一樣不耐煩的琢磨什么時(shí)候才能輪到自己。
抽象一下券坞,消息機(jī)制其實(shí)和這個(gè)例子十分類似鬓催。每個(gè)人都看做是個(gè)消息,什么時(shí)候到的銀行是不確定的恨锚。柜員可以看做一個(gè)消息處理器宇驾,他幫你辦業(yè)務(wù)就相當(dāng)于在處理你的消息;而人們按照先后順序排起來的隊(duì)伍可以看做是個(gè)消息隊(duì)列猴伶。所以這個(gè)過程可以抽象為有個(gè)消息處理器课舍,他有個(gè)消息隊(duì)列,隨機(jī)來到的消息按照一定順序排列在這個(gè)隊(duì)列里他挎,消息處理器不停的從隊(duì)列頭部獲取消息然后處理之筝尾,周而復(fù)始的循環(huán)重復(fù)這個(gè)過程。如下圖所示:
那么Android是怎樣怎樣實(shí)現(xiàn)這個(gè)消息機(jī)制的呢办桨?
Android的消息機(jī)制
消息機(jī)制首先得有消息筹淫,在Android中就是Message。怎樣能確定一個(gè)消息呢崔挖?消息要有來源或者目標(biāo)贸街,也就是target;消息要表明自己要做什么狸相,也就是what或者callback薛匪;消息要表明自己希望在什么時(shí)候執(zhí)行,也就是when脓鹃。有了這幾個(gè)要素逸尖,基本上這個(gè)消息就是個(gè)完備的消息了,可以被加入到消息隊(duì)列中了。Android中的消息隊(duì)列是MessageQueue娇跟。消息處理循環(huán)是Looper岩齿。Looper是個(gè)死循環(huán),不停的從MessageQueue中獲取消息然后處理之苞俘,具體的執(zhí)行是在Handler里面進(jìn)行的盹沈。另外消息加入消息隊(duì)列也需要Handler來操作。Message吃谣,MessageQueue乞封,Looper,Handler組合在一起岗憋,就構(gòu)成了整個(gè)Android的消息機(jī)制肃晚。
Android的主線程就運(yùn)行著這樣一個(gè)消息機(jī)制。
Android的主線程
主線程是在ActivityThread中創(chuàng)建的仔戈,可以看到在main函數(shù)中
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
Looper.loop();
}
主線程實(shí)現(xiàn)了一個(gè)消息機(jī)制关串。所以Android的主線程就是個(gè)消息處理的循環(huán)。它所做的工作就是在不停的從消息隊(duì)列獲取消息监徘,處理消息晋修,周而復(fù)始。你的App所有的在UI上的操作凰盔,例如點(diǎn)擊事件的處理飞蚓、頁面動(dòng)畫、顯示更新頁面廊蜒、View繪制、啟動(dòng)新Activity等操作都是在給主線程發(fā)消息溅漾,主線程然后挨個(gè)處理這些消息山叮。
主線程如何影響秒開
我們了解了主線程的工作機(jī)制后,就要看看主線程中的消息處理是如何影響Activity秒開的添履。
當(dāng)我們要啟動(dòng)一個(gè)新的Activity的時(shí)候屁倔,從調(diào)用startActivity開始到新Activity顯示出來,Android系統(tǒng)會(huì)發(fā)送一系列的消息給主線程暮胧。這一系列的消息處理所花費(fèi)的總時(shí)間會(huì)影響頁面的秒開锐借,如果執(zhí)行時(shí)間過長(zhǎng),用戶就會(huì)有響應(yīng)非常慢的感覺往衷。此外钞翔,除了Android系統(tǒng)會(huì)給主線程發(fā)消息,App自身也會(huì)給主線程發(fā)消息席舍,如果在啟動(dòng)新Activity的過程中布轿,這些App自己的消息正好插入這一系列的Android系統(tǒng)消息中,那也會(huì)導(dǎo)致總的處理時(shí)間延長(zhǎng),造成不能秒開汰扭。
上圖代表了啟動(dòng)新Activity的主線程的三種情況稠肘,每個(gè)矩形代表主線程處理一個(gè)消息所花的時(shí)間,越寬代表處理的時(shí)間越長(zhǎng)萝毛。綠色填充的代表這是一個(gè)Android系統(tǒng)發(fā)過來的消息项阴;藍(lán)色填充的代表這是一個(gè)App自己發(fā)過來的消息。最下方的向右箭頭代表時(shí)間笆包,起點(diǎn)是startActivity被調(diào)用的時(shí)刻环揽。
- 第一種狀況代表正常的情形,主線程中只有和startActivity相關(guān)的系統(tǒng)消息被處理色查,而且處理每個(gè)消息所花費(fèi)的時(shí)間都在合理范圍內(nèi)薯演。所以這個(gè)頁面可以滿足秒開。
- 第二種情況代表一個(gè)異常的情形秧了,雖然主線程處理的消息都是系統(tǒng)消息跨扮,但是某一個(gè)或某幾個(gè)消息的處理時(shí)間超出了合理值,導(dǎo)致頁面不能秒開验毡。
- 第三種情況代表另一種異常的情形衡创,在系統(tǒng)消息中混入了App自己的消息,主線程不僅要處理系統(tǒng)消息晶通,還要處理App自己的消息璃氢,結(jié)果就是總的啟動(dòng)時(shí)間要額外加上App消息的處理時(shí)間,導(dǎo)致頁面不能秒開狮辽。
實(shí)際情況中還有可能會(huì)出現(xiàn)既有系統(tǒng)消息處理時(shí)間過長(zhǎng)同時(shí)也混有App自己的消息的情形一也。
秒開優(yōu)化
了解了影響秒開的因素之后,我們只要有辦法能監(jiān)測(cè)主線程中每個(gè)消息處理時(shí)間喉脖,我們就能定位到造成頁面卡慢的原因椰苟,然后再做優(yōu)化。
幸好Android工程師為我們?cè)贚ooper中預(yù)留了打log的位置树叽。
public static void loop() {
final Looper me = myLooper();
...
final MessageQueue queue = me.mQueue;
...
for (;;) {
Message msg = queue.next(); // might block
...
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
msg.recycleUnchecked();
}
}
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
可見在消息被處理的開始和處理結(jié)束之后都會(huì)打印log舆蝴。
你只需要在代碼中調(diào)用Looper.setMessageLogging()設(shè)置一下就好。
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String s) {
Log.v("debug", s);
}
});
編譯運(yùn)行你的程序题诵,你會(huì)在logcat輸出看到類似這樣的log:
每行 “>>>>> Dispatching to”開頭的log代表一個(gè)消息即將開始被處理洁仗;緊接著下一行“<<<<< Finished to”開頭的log代表這一消息處理完畢。通過這些log你可以知道所有被主線程處理的消息性锭,并可以根據(jù)開始結(jié)束的時(shí)間差知道每個(gè)消息消耗的時(shí)間赠潦。有了這些信息你可以找到導(dǎo)致你的app卡慢的消息,然后進(jìn)一步去debug問題草冈。
在你啟動(dòng)一個(gè)新的Activity的時(shí)候你可以觀測(cè)這樣的log輸出祭椰,看看里面有沒有處理時(shí)間比較長(zhǎng)的消息臭家,或者看看里面有沒有App自己的消息被處理,如果有的話方淤,這些都是需要優(yōu)化的點(diǎn)钉赁。
然而直接看log的缺點(diǎn)是這樣的log會(huì)比較多,而且并不容易定位啟動(dòng)Activity的開始和結(jié)束時(shí)間點(diǎn)携茂,另外每個(gè)消息處理的時(shí)間也要自己計(jì)算你踩,并不是十分直觀。
StallBuster
為了方便的進(jìn)行秒開優(yōu)化讳苦,我做了個(gè)工具叫StallBuster來協(xié)助定位Activity秒開失敗的原因带膜。
集成StallBuster非常簡(jiǎn)單,只需要兩步就可以了
- 添加對(duì)StallBuster的依賴
dependencies {
compile 'com.github.zhangjianli:stallbuster:1.1'
}
- 在你的App的Application中添加以下代碼
public class YourApplication extends Application {
@Override
public void onCreate() {
StallBuster.getInstance().init(this);
super.onCreate();
}
}
這樣就可以了鸳谜,編譯運(yùn)行你的App膝藕。在你的App中打開新的Activity,StallBuster會(huì)發(fā)出一個(gè)Notification咐扭。告訴你剛啟動(dòng)這個(gè)Activity花了多少毫秒
點(diǎn)擊這個(gè)Notification就會(huì)打開StallBuster的歷史記錄頁面芭挽。
這個(gè)頁面按照時(shí)間順序列出了你的App啟動(dòng)每個(gè)Activity的歷史記錄。每條記錄最左邊是啟動(dòng)所花費(fèi)的時(shí)間蝗肪。綠色代表所費(fèi)時(shí)間符合秒開要求袜爪;紅色代表時(shí)間太長(zhǎng)。需要關(guān)注薛闪。右邊是這條記錄對(duì)應(yīng)的Activity名稱辛馆。點(diǎn)擊某條記錄就會(huì)進(jìn)入詳情頁属铁。
在詳情頁里你可以看到啟動(dòng)這個(gè)Activity的過程中主線程處理過的消息摹恰。上方的復(fù)選框可以過濾執(zhí)行時(shí)間比較短的消息,方便定位問題携御。
對(duì)于每條記錄诱咏,首先顯示的是這條消息開始被處理的時(shí)間戳瓢对。然后是cost字段,表示處理這條消息花了多長(zhǎng)時(shí)間胰苏。正常情況下是字體是黑色的;如果處理時(shí)間過長(zhǎng)醇疼,則顯示為紅色硕并。表明這里可能是我們需要優(yōu)化的地方。
接下來是target字段秧荆,對(duì)應(yīng)的是這個(gè)消息是被哪個(gè)Handler處理的倔毙。Android系統(tǒng)的Handler會(huì)顯示為黑色;App自己的Handler會(huì)顯示為紅色乙濒,表明這個(gè)消息不應(yīng)該在啟動(dòng)Activity的時(shí)候出現(xiàn)陕赃,這里也可能是需要優(yōu)化的地方卵蛉。
例如上圖中第一條記錄,.MainActivity$StallHandler處理這個(gè)消息花費(fèi)了142ms么库。這會(huì)使啟動(dòng)SubActivity的時(shí)間至少延長(zhǎng)了142ms傻丝。而這個(gè)Handler是App自己的Handler。我們需要調(diào)試代碼使得在啟動(dòng)這個(gè)Activity的時(shí)候確保不會(huì)有來自這個(gè)Handler的消息诉儒,142ms的時(shí)間就會(huì)節(jié)省下來葡缰。
最后一個(gè)字段是message或者callback。對(duì)應(yīng)的是Message中的what或callback忱反。有了這些信息我們就能很方便的定位主線程中影響秒開的消息泛释,進(jìn)而優(yōu)化我們的App。
StallBuster就給大家介紹到這里温算,希望StallBuster能幫到你怜校。如果大家有任何建議或者問題請(qǐng)給我留言。
總結(jié)
App秒開是是一項(xiàng)非常重要的性能指標(biāo)注竿。秒開的優(yōu)化是個(gè)復(fù)雜的工作茄茁,有很多因素會(huì)影響App秒開。其中比較重要的一個(gè)因素是啟動(dòng)Activity的時(shí)候主線程的消息處理情況蔓搞。在啟動(dòng)Activity過程中需要避免消息處理時(shí)間過長(zhǎng)胰丁,也要避免在此期間有App自己的消息需要處理。優(yōu)化的關(guān)鍵點(diǎn)是要定位到主線程中的耗時(shí)操作喂分,我們可以通過打印分析主線程的消息處理log來定位锦庸,但這種方式并不是很直觀方便。這時(shí)可以使用StallBuster幫助你快速定位秒開問題點(diǎn)蒲祈,讓秒開優(yōu)化變的更加簡(jiǎn)單甘萧。