Android性能优化之较精确的获取图像显示到屏幕上的时间
轉(zhuǎn)載自:http://blog.desmondyao.com/android-show-time/
這兩天我的包工頭歪龍木·靈魂架構(gòu)師·王半仙·Yrom給我派了一個(gè)活:統(tǒng)計(jì)App冷啟動(dòng)時(shí)間。這個(gè)任務(wù)看上去不難,但是要求統(tǒng)計(jì)出來(lái)的時(shí)間要準(zhǔn),要特別準(zhǔn)。
意思就是,我必須要按Activity繪制到屏幕上這個(gè)時(shí)間節(jié)點(diǎn)作為標(biāo)桿,來(lái)進(jìn)行我的統(tǒng)計(jì)工作。畢竟如果是因?yàn)橐晥D處理不當(dāng)而導(dǎo)致的measure/layout/draw耗時(shí)太久,這是不能忍的,需要及時(shí)統(tǒng)計(jì)到。雖然有點(diǎn)蛋疼,但是這個(gè)任務(wù)還算有意義,我就深挖一下,把過(guò)程分享出來(lái)。
注:本文所涉及源碼部分的sdk level為21
onResume真的已經(jīng)顯示了嗎?
如果你看過(guò)官方文檔中的Activity生命指引,你會(huì)發(fā)現(xiàn)它說(shuō)的是
Activity在onResume生命周期中已經(jīng)是可見(jiàn)狀態(tài)。
那么我們就去這個(gè)onResume中看一看。現(xiàn)在我在Activty的onCreate第一行(super.onCreate之前)記錄一個(gè)時(shí)間點(diǎn),onResume的最后一行(super.onResume之后)記錄一個(gè)時(shí)間點(diǎn),將兩者的差值記錄下來(lái)。
打出來(lái)的Log是:I/MainActivity: onCreate -> onResume : 70。 即這個(gè)過(guò)程花費(fèi)了70ms。那真的是只用了70ms我的Activity就已經(jīng)完全顯示了嗎?我們來(lái)看兩個(gè)官方的衡量點(diǎn):
- 通過(guò)adb shell的命令
|
? 1 2 3 4 5 6 7 8 9 |
? $ adb shell am start -W com.desmond.demo/.MainActivity Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.desmond.demo/.MainActivity } Status: ok Activity: com.desmond.demo/.MainActivity ThisTime: 314 TotalTime: 314 WaitTime: 314 Complete |
- 啟動(dòng)Activity時(shí)的可以看Tag = ActivityManager打出來(lái)的Log:
I/ActivityManager: Displayed com.desmond.testapplication/.MainActivity: +314ms
這兩個(gè)時(shí)間是一樣的,我們看哪個(gè)都行。這個(gè)314ms的啟動(dòng)過(guò)程和上面的70ms是同一次啟動(dòng)過(guò)程打出來(lái)的日志,那么問(wèn)題來(lái)了,怎么會(huì)和我打出來(lái)的Log時(shí)間相差這么大?我們先看看系統(tǒng)打出來(lái)的時(shí)間到底是什么時(shí)間。
打出這段log的代碼在ActivityRecord:
|
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
? //ActivityRecord private void reportLaunchTimeLocked(final long curTime) { final ActivityStack stack = task.stack; if (stack == null) { return; } final long thisTime = curTime - displayStartTime; final long totalTime = stack.mLaunchStartTime != 0 ? (curTime - stack.mLaunchStartTime) : thisTime; if (SHOW_ACTIVITY_START_TIME) { // ...其他代碼 StringBuilder sb = service.mStringBuilder; sb.setLength(0); sb.append("Displayed "); sb.append(shortComponentName); sb.append(": "); TimeUtils.formatDuration(thisTime, sb); if (thisTime != totalTime) { sb.append(" (total "); TimeUtils.formatDuration(totalTime, sb); sb.append(")"); } Log.i(TAG, sb.toString()); } // ...其他代碼 } |
它的調(diào)用時(shí)機(jī)我們后面再討論,首先看一下它打出了什么。這個(gè)函數(shù)中將totalTime作為Displayed時(shí)間打了出來(lái),值為當(dāng)前時(shí)間 - stack.mLaunchStartTime。那這個(gè)mLaunchStartTime是什么時(shí)候被記錄的呢?查了一下調(diào)用發(fā)現(xiàn)在ActivityStackSupervisor.startSpecificActivityLocked會(huì)調(diào)用stack.setLaunchTime(r)去設(shè)置這個(gè)時(shí)間。
那么我們可以得出第一個(gè)結(jié)論:
結(jié)論1:?系統(tǒng)打出來(lái)的時(shí)間包含了進(jìn)程啟動(dòng)的時(shí)間。
因?yàn)檫M(jìn)程啟動(dòng)都是在ActivityStackSupervisor.startSpecificActivityLocked()中進(jìn)行的,以ActivityThread.main為入口啟動(dòng)一個(gè)新進(jìn)程。如果對(duì)于這里不明白,可以參考一下老羅的Android應(yīng)用程序啟動(dòng)過(guò)程源代碼分析。
但是進(jìn)程啟動(dòng)這么耗時(shí)?我的test activiy也沒(méi)有自定義Application,更別提什么耗時(shí)操作了。我來(lái)實(shí)踐一下熱啟動(dòng),App退出,但是不殺進(jìn)程,再對(duì)比一下我打的log和系統(tǒng)log的時(shí)間區(qū)別。
我:I/MainActivity: onCrete -> onResume : 37
系統(tǒng): I/ActivityManager: Displayed com.desmond.testapplication/.MainActivity: +103ms
依然差了很多!
看來(lái)我們需要研究一下Activity的onResume過(guò)程及系統(tǒng)上報(bào)Displayed的時(shí)機(jī)了。
深入探究onResume過(guò)程
我先對(duì)AMS觸發(fā)Activity的onResume這個(gè)過(guò)程畫(huà)了一張圖:
在ActivityManagerService(AMS)告知Activity要resume時(shí),它通過(guò)調(diào)用ApplicationThread.scheduleResumeActivity給ActivityThread.H(一個(gè)Handler)發(fā)送消息RESUME_ACTIVITY,然后H開(kāi)始處理消息:
|
? 1 2 3 4 5 |
? case RESUME_ACTIVITY: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); handleResumeActivity((IBinder) msg.obj, true, msg.arg1 != 0, true); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; |
在這個(gè)handleResumeActivity中就處理了所有的Resume邏輯,我們進(jìn)去一探究竟。
|
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
? //ActivityThread.java final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { // 一些其他代碼 // 這步onResume ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; // 一些其他代碼 if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); //這步很關(guān)鍵 } } //... } } |
Activity.onResume() 這一步就是在performResumeActivity里面調(diào)用的,有興趣的同學(xué)可以去看看,里面代碼很簡(jiǎn)單。
為什么說(shuō)wm.addView這一步很關(guān)鍵?真正對(duì)Activity的視圖進(jìn)行計(jì)算、繪制、flush到window上是wm.addWindow這一步做的,這里面代碼比較多,我直接畫(huà)張圖看清晰一點(diǎn):
這張圖的代碼就不放了,關(guān)鍵的類(lèi)和方法都在里面,有興趣的同學(xué)可以自行翻閱。有幾個(gè)注意的點(diǎn):
這時(shí)候我們回頭看之前ActivityThread.handleResumeActivity的代碼:Activity的onResume在wm.addView之前!。因?yàn)閂iew的計(jì)算、繪制等都在wm.addView之后執(zhí)行,那我們可以得出第二個(gè)結(jié)論:
結(jié)論2:?在onResume的時(shí)候是肯定統(tǒng)計(jì)不到View的measure/layout/draw時(shí)間的。
這時(shí)候我們要搞清楚的是,上面系統(tǒng)的Log是在哪一步調(diào)用的,它是否包含了View的measure/layout/draw的時(shí)間?這個(gè)過(guò)程可是一頓好找,大概是如下圖所示流程:
5-6步之間被我精簡(jiǎn)了一小部分內(nèi)容,我來(lái)簡(jiǎn)單解釋一下這個(gè)過(guò)程:
那我們可以確定的是,在看到系統(tǒng)這條日志時(shí),View的計(jì)算、繪制已經(jīng)完成,并且Surface也被繪制到屏幕上。這樣我們可以得出第三個(gè)結(jié)論:
結(jié)論3:?系統(tǒng)打出來(lái)的日志時(shí)Activity已經(jīng)被完全展示到了屏幕上。
View繪制結(jié)束的回調(diào)時(shí)機(jī)
得出了結(jié)論,那我們要怎么知道什么時(shí)候View繪制結(jié)束呢?這里就仁者見(jiàn)仁,智者見(jiàn)智了。
首先可以確認(rèn)的是,所有ViewTreeObserver里面的Listener都是不夠準(zhǔn)確的。為什么呢?它們里面能統(tǒng)計(jì)到的最遲就是OnDrawListener,我們可以在ViewRootImpl#draw()函數(shù)中看到,它是在真正draw這一步之前調(diào)用的,也就是說(shuō)它沒(méi)有統(tǒng)計(jì)到draw的時(shí)間。
我提供一個(gè)思路,能夠準(zhǔn)確獲取到包括View的measure/layout/draw過(guò)程的時(shí)間。那就是在onResume中添加一個(gè)IdleHandler:
|
? 1 2 3 4 5 6 7 8 9 10 11 |
? @Override protected void onResume() { super.onResume(); Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { Log.i(TAG, "onCreate -> idle : " + (SystemClock.uptimeMillis() - time)); return false; } }); } |
這個(gè)IdleHandler是什么?它會(huì)在Looper的消息隊(duì)列處理完當(dāng)前阻塞的消息(即Idle中,等待獲取下一條消息)時(shí)被調(diào)用。我這里直接指定了主線程的消息隊(duì)列,那我在onResume中給它加入一個(gè)IdleHandler,它會(huì)什么時(shí)候調(diào)用呢,我們回顧一下Activity的onResume->ViewRoot的traversal這個(gè)過(guò)程,我做了一些修改:
這里有一個(gè)需要注意的地方:
ViewRootImpl在向Choreographer發(fā)送調(diào)度消息時(shí),特地向主線程的Looper消息循環(huán)發(fā)送了一個(gè)“障礙消息”。利用MessageQueue#postSyncBarrier可以做到這一點(diǎn),當(dāng)出現(xiàn)了這一個(gè)障礙消息的時(shí)候,消息循環(huán)就暫時(shí)無(wú)法處理后續(xù)排入消息。有興趣的同學(xué)可以自行研究這個(gè)過(guò)程。
Choreographer通過(guò)直接在native操作主進(jìn)程的MessageQueue來(lái)排入消息,從而它的執(zhí)行會(huì)無(wú)視我們加入的“障礙消息”。(這部分代碼就不深入了,可以參考老羅的Android應(yīng)用程序消息處理機(jī)制(Looper、Handler)分析)。即ViewRootImpl#doTraversal這個(gè)函數(shù)也是在一次消息處理中發(fā)生的。它此時(shí)移除了MessageQueue里面的障礙消息,并且執(zhí)行performTraversals。
需要注意的是:這整個(gè)過(guò)程都是在主線程的消息循環(huán)中發(fā)生的。這個(gè)過(guò)程可以描述為如下幾步:
所以如果我們?cè)趏nResume中向主進(jìn)程添加入一個(gè)IdleHandler,它是必然會(huì)在這三步都走完,主進(jìn)程Looper的MessageQueue才可能觸發(fā)Idle狀態(tài),并觸發(fā)IdleHandler回調(diào)。
我們可以實(shí)驗(yàn)一下:在添加入的IdleHandler內(nèi)打點(diǎn),計(jì)算與onCreate第一行打點(diǎn)時(shí)間之差,最后打出來(lái)的Log:
|
? 1 2 3 4 5 |
? MainActivity: onCrete -> onResume : 12 MainActivity: onCrete -> onPreDraw : 138 MainActivity: onCrete -> onPreDraw : 147 MainActivity: onCrete -> idleHandler : 166 ActivityManager: Displayed com.desmond.testapplication/.MainActivity: +192ms |
雖然還是離ActivityManager打出來(lái)的差了一點(diǎn),但是這也是有理由的。我們看上面的WMS繪制Surface那一步,是通過(guò)Handler發(fā)送異步消息完成的,這里統(tǒng)計(jì)不到。
總結(jié)
雖然沒(méi)有完全貼近系統(tǒng)打出來(lái)的日志,但是通過(guò)IdleHandler的方式已經(jīng)能統(tǒng)計(jì)到我想要的內(nèi)容了(由Activity的onCreate第一步到整個(gè)界面顯示)。
老羅的博客和AOSP源碼都是很棒的參考資料,建議多看看Handler/Looper的消息循環(huán)機(jī)制、AMS/WMS/主進(jìn)程之間的交互,才能比較好的理解這個(gè)過(guò)程。
參考文章:
- Android性能優(yōu)化典范(第6季)
- Android應(yīng)用程序消息處理機(jī)制(Looper、Handler)分析
- Android應(yīng)用程序窗口(Activity)與WindowManagerService服務(wù)的連接過(guò)程分析
總結(jié)
以上是生活随笔為你收集整理的Android性能优化之较精确的获取图像显示到屏幕上的时间的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: android 权限模型,android
- 下一篇: RK3399 Android7.1电脑端