用兩張圖告訴你,為什么你的App會(huì)卡頓?
有什么料?
從這篇文章中你能獲得這些料:
- 知道setContentView()之后發(fā)生了什么?
- 知道Android究竟是如何在屏幕上顯示我們期望的畫面的?
- 對(duì)Android的視圖架構(gòu)有整體把握。
- 學(xué)會(huì)從根源處分析畫面卡頓的原因。
- 掌握如何編寫一個(gè)流暢的App的技巧。
- 從源碼中學(xué)習(xí)Android的細(xì)想。
- 收獲兩張自制圖,幫助你理解Android的視圖架構(gòu)。
從setContentView()說(shuō)起
- public class AnalyzeViewFrameworkActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_analyze_view_framwork);
- }
- }
上面這段代碼想必Androider們大都已經(jīng)不能再熟悉的更多了。但是你知道這樣寫了之后發(fā)生什么了嗎?這個(gè)布局到底被添加到哪了?我的天,知識(shí)點(diǎn)來(lái)了!
可能很多同學(xué)也知道這個(gè)布局是被放到了一個(gè)叫做DecorView的父布局里,但是我還是要再說(shuō)一遍。且看下圖
這個(gè)圖可能和伙伴們?cè)跁匣蛘呔W(wǎng)上常見(jiàn)的不太一樣,為什么不太一樣呢?因?yàn)槭俏易约寒嫷?,哈哈?..
下面就來(lái)看著圖捋一捋Android最基本的視圖框架。
PhoneWindow
估計(jì)很多同學(xué)都知道,每一個(gè)Activity都擁有一個(gè)Window對(duì)象的實(shí)例。這個(gè)實(shí)例實(shí)際是PhoneWindow類型的。那么PhoneWindow從名字很容易看出,它應(yīng)該是Window的兒子(即子類)!
知識(shí)點(diǎn):每一個(gè)Activity都有一個(gè)PhoneWindow對(duì)象。
那么,PhoneWindow有什么用呢?它在Activity充當(dāng)什么角色呢?下面我就姑且把PhoneWindow等同于Window來(lái)稱呼吧。
Window從字面看它是一個(gè)窗口,意思和PC上的窗口概念有點(diǎn)像。但也不是那么準(zhǔn)確??磮D說(shuō)。可以看到,我們要顯示的布局是被放到它的屬性mDecor中的,這個(gè)mDecor就是DecorView的一個(gè)實(shí)例。下面會(huì)專門擼DecorView,現(xiàn)在先把關(guān)注點(diǎn)放到Window上。Window還有一個(gè)比較重要的屬性mWindowManager,它是WindowManager(這是個(gè)接口)的一個(gè)實(shí)現(xiàn)類的一個(gè)實(shí)例。我們平時(shí)通過(guò)getWindowManager()方法獲得的東西就是這個(gè)mWindowManager。顧名思義,它是Window的管理者,負(fù)責(zé)管理著窗口及其中顯示的內(nèi)容。它的實(shí)際實(shí)現(xiàn)類是WindowManagerImpl。可能童鞋們現(xiàn)在正在PhoneWindow中尋找著這個(gè)mWindowManager是在哪里實(shí)例化的,是不是上下來(lái)回滾動(dòng)著這個(gè)類都找不見(jiàn)?STOP!mWindowManager是在它爹那里就實(shí)例化好的。下面代碼是在Window.java中的。
- public void setWindowManager(WindowManager wm,
- IBinder appToken,
- String appName,
- boolean hardwareAccelerated) {
- ...
- if (wm == null) {
- wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
- //獲取了一個(gè)WindowManager
- }
- mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
- //通過(guò)這里我們可以知道,上面獲取到的wm實(shí)際是WindowManagerImpl類型的。
- }
通過(guò)上面的介紹,我們已經(jīng)知道了Window中有負(fù)責(zé)承載布局的DecorView,有負(fù)責(zé)管理的WindowManager(事實(shí)上它只是個(gè)代理,后面會(huì)講它代理的是誰(shuí))。
DecorView
前面提到過(guò),在Activity的onCreate()中通過(guò)setContentView()設(shè)置的布局實(shí)際是被放到DecorView中的。我們?cè)趫D中找到DecorView。
從圖中可以看到,DecorView繼承了FrameLayout,并且一般情況下,它會(huì)在先添加一個(gè)預(yù)設(shè)的布局。比如DecorCaptionView,它是從上到下放置自己的子布局的,相當(dāng)于一個(gè)LinearLayout。通常它會(huì)有一個(gè)標(biāo)題欄,然后有一個(gè)容納內(nèi)容的mContentRoot,這個(gè)布局的類型視情況而定。我們希望顯示的布局就是放到了mContentRoot中。
知識(shí)點(diǎn):通過(guò)setContentView()設(shè)置的布局是被放到DecorView中,DecorView是視圖樹(shù)的最頂層。
WindowManager
前面已經(jīng)提到過(guò),WindowManager在Window中具有很重要的作用。我們先在圖中找到它。這里需要先說(shuō)明一點(diǎn),在PhoneWindow中的mWindowManager實(shí)際是WindowManagerImpl類型的。WindowManagerImpl自然就是接口WindowManager的一個(gè)實(shí)現(xiàn)類嘍。這一點(diǎn)是我沒(méi)有在圖中反映的。
WindowManager是在Activity執(zhí)行attach()時(shí)被創(chuàng)建的,attach()方法是在onCreate()之前被調(diào)用的。關(guān)于Activity的創(chuàng)建可以看看我的這篇:【可能是史上最簡(jiǎn)單的!一張圖3分鐘讓你明白Activity啟動(dòng)流程,不看后悔!http://www.jianshu.com/p/9ecea420eb52】。
Activity.java
- final void attach(Context context, ActivityThread aThread,
- Instrumentation instr, IBinder token, int ident,
- Application application, Intent intent, ActivityInfo info,
- CharSequence title, Activity parent, String id,
- NonConfigurationInstances lastNonConfigurationInstances,
- Configuration config, String referrer, IVoiceInteractor voiceInteractor,
- Window window){
- ...
- mWindow = new PhoneWindow(this, window);
- //創(chuàng)建Window
- ...
- mWindow.setWindowManager(
- (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
- mToken, mComponent.flattenToString(),
- (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
- //注意!這里就是在創(chuàng)建WindowManager。
- //這個(gè)方法在前面已經(jīng)說(shuō)過(guò)了。
- if (mParent != null) {
- mWindow.setContainer(mParent.getWindow());
- }
- mWindowManager = mWindow.getWindowManager();
- }
繼續(xù)看圖。WindowManagerImpl持有了PhoneWindow的引用,因此它可以對(duì)PhoneWindow進(jìn)行管理。同時(shí)它還持有一個(gè)非常重要的引用mGlobal。這個(gè)mGlobal指向一個(gè)WindowManagerGlobal類型的單例對(duì)象,這個(gè)單例每個(gè)應(yīng)用程序只有唯一的一個(gè)。在圖中,我說(shuō)明了WindowManagerGlobal維護(hù)了本應(yīng)用程序內(nèi)所有Window的DecorView,以及與每一個(gè)DecorView對(duì)應(yīng)關(guān)聯(lián)的ViewRootImpl。這也就是為什么我前面提到過(guò),WindowManager只是一個(gè)代理,實(shí)際的管理功能是通過(guò)WindowManagerGlobal實(shí)現(xiàn)的。我們來(lái)看個(gè)源碼的例子就比較清晰了。開(kāi)始啦!
WimdowManagerImpl.java
- public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
- ...
- mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
- //實(shí)際是通過(guò)WindowManagerGlobal實(shí)現(xiàn)的。
- }
從上面的代碼可以看出,WindowManagerImpl確實(shí)只是WindowManagerGlobal的一個(gè)代理而已。同時(shí),上面這個(gè)方法在整個(gè)Android的視圖框架流程中十分的重要。我們知道,在Activity執(zhí)行onResume()后界面就要開(kāi)始渲染了。原因是在onResume()時(shí),會(huì)調(diào)用WindowManager的addView()方法(實(shí)際最后調(diào)用的是WindowManagerGlobal的addView()方法),把視圖添加到窗口上。結(jié)合我的這篇【可能是史上最簡(jiǎn)單的!一張圖3分鐘讓你明白Activity啟動(dòng)流程,不看后悔!http://www.jianshu.com/p/9ecea420eb52】看,可以幫助你更好的理解Android的視圖框架。
ActivityThread.java
- final void handleResumeActivity(IBinder token,
- boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
- ...
- ViewManager wm = a.getWindowManager();
- //獲得WindowManager,實(shí)際是WindowManagerImpl
- ...
- wm.addView(decor, l);
- //添加視圖
- ...
- wm.updateViewLayout(decor, l);
- //需要刷新的時(shí)候會(huì)走這里
- ...
- }
從上面可以看到,當(dāng)Activity執(zhí)行onResume()的時(shí)候就會(huì)添加視圖,或者刷新視圖。需要解釋一點(diǎn):WindowManager實(shí)現(xiàn)了ViewManager接口。
如圖中所說(shuō),WindowManagerGlobal調(diào)用addView()的時(shí)候會(huì)把DecorView添加到它維護(hù)的數(shù)組中去,并且會(huì)創(chuàng)建另一個(gè)關(guān)鍵且極其重要的ViewRootImpl(這個(gè)必須要專門講一下)類型的對(duì)象,并且也會(huì)把它存到一個(gè)數(shù)組中維護(hù)。
WindowManagerGlobal.java
- public void addView(View view, ViewGroup.LayoutParams params,
- Display display, Window parentWindow) {
- ...
- root = new ViewRootImpl(view.getContext(), display);
- //重要角色登場(chǎng)
- view.setLayoutParams(wparams);
- mViews.add(view);
- mRoots.add(root);
- //保存起來(lái)維護(hù)
- mParams.add(wparams);
- ...
- root.setView(view, wparams, panelParentView);
- //設(shè)置必要屬性view是DecorView,panelParentView是PhoneWindow
- ...
- }
可以看出ViewRootImpl是在Activity執(zhí)行onResume()的時(shí)候才被創(chuàng)建的,并且此時(shí)才把DecorView傳進(jìn)去讓它管理。
知識(shí)點(diǎn):WindowManager是在onCreate()時(shí)被創(chuàng)建。它對(duì)窗口的管理能力實(shí)際是通過(guò)WindowManagerGlobal實(shí)現(xiàn)的。在onResume()是視圖才通過(guò)WindowManager被添加到窗口上。
ViewRootImpl
ViewRootImpl能夠和系統(tǒng)的WindowManagerService進(jìn)行交互,并且管理著DecorView的繪制和窗口狀態(tài)。非常的重要。趕緊在圖中找到對(duì)應(yīng)位置吧!
ViewRootImpl并不是一個(gè)View,而是負(fù)責(zé)管理視圖的。它配合系統(tǒng)來(lái)完成對(duì)一個(gè)Window內(nèi)的視圖樹(shù)的管理。從圖中也可以看到,它持有了DecorView的引用,并且視圖樹(shù)它是視圖樹(shù)繪制的起點(diǎn)。因此,ViewRootImpl會(huì)稍微復(fù)雜一點(diǎn),需要我們更深入的去了解,在圖中我標(biāo)出了它比較重要的組成Surface和Choreographer等都會(huì)在后面提到。
到此,我們已經(jīng)一起把第一張圖擼了一遍了,現(xiàn)在童鞋們因該對(duì)Android視圖框架有了大致的了解。下面將更進(jìn)一步的去了解Android的繪制機(jī)制。
App總是卡頓到底是什么原因?
下面將會(huì)詳細(xì)的講解為什么我們?cè)O(shè)置的視圖能夠被繪制到屏幕上?這中間究竟隱藏著怎樣的離奇?看完之后,你自然就能夠從根源知道為什么你的App會(huì)那么卡,以及開(kāi)始有思路著手解決這些卡頓。
同樣用一張圖來(lái)展示這個(gè)過(guò)程。由于Android繪制機(jī)制確實(shí)有點(diǎn)復(fù)雜,所以第一眼看到的時(shí)候你的內(nèi)心中可能蹦騰了一萬(wàn)只草泥馬😂。不要怕!我們從源頭開(kāi)始,一點(diǎn)一點(diǎn)的梳理這個(gè)看似復(fù)雜的繪制機(jī)制。為什么說(shuō)看似復(fù)雜呢?因?yàn)檫@個(gè)過(guò)程只需要幾分鐘。Just Do It!
CPU、GPU是搞什么鬼的?
整天聽(tīng)到CPU、GPU的,你知道他們是干什么的嗎?這里簡(jiǎn)單的提一下,幫助理解后面的內(nèi)容。
在Android的繪制架構(gòu)中,CPU主要負(fù)責(zé)了視圖的測(cè)量、布局、記錄、把內(nèi)容計(jì)算成Polygons多邊形或者Texture紋理,而GPU主要負(fù)責(zé)把Polygons或者Textture進(jìn)行Rasterization柵格化,這樣才能在屏幕上成像。在使用硬件加速后,GPU會(huì)分擔(dān)CPU的計(jì)算任務(wù),而CPU會(huì)專注處理邏輯,這樣減輕CPU的負(fù)擔(dān),使得整個(gè)系統(tǒng)效率更高。
RefreshRate刷新率和FrameRate幀率
RefreshRate刷新率是屏幕每秒刷新的次數(shù),是一個(gè)與硬件有關(guān)的固定值。在Android平臺(tái)上,這個(gè)值一般為60HZ,即屏幕每秒刷新60次。
FrameRate幀率是每秒繪制的幀數(shù)。通常只要幀數(shù)和刷新率保持一致,就能夠看到流暢的畫面。在Android平臺(tái),我們應(yīng)該盡量維持60FPS的幀率。但有時(shí)候由于視圖的復(fù)雜,它們可能就會(huì)出現(xiàn)不一致的情況。
如圖,當(dāng)幀率小于刷新率時(shí),比如圖中的30FPS < 60HZ,就會(huì)出現(xiàn)相鄰兩幀看到的是同一個(gè)畫面,這就造成了卡頓。這就是為什么我們總會(huì)說(shuō),要盡量保證一幀畫面能夠在16ms內(nèi)繪制完成,就是為了和屏幕的刷新率保持同步。
下面將會(huì)介紹Android是如何來(lái)確保刷新率和幀率保持同步的。
Vsync(垂直同步)是什么?
你可能在游戲的設(shè)置中見(jiàn)過(guò)Vsync,開(kāi)啟它通常能夠提高游戲性能。在Android中,同樣使用Vsync垂直同步來(lái)提高顯示性能。它能夠使幀率FrameRate和硬件的RefreshRate刷新強(qiáng)制保持一致。
HWComposer與Vsync不得不說(shuō)的事
看圖啦看圖啦。首先在最左邊我們看到有個(gè)叫HWComposer的類,這是一個(gè)c++編寫的類。它Android系統(tǒng)初始化時(shí)就被創(chuàng)建,然后開(kāi)始配合硬件產(chǎn)生Vsync信號(hào),也就是圖中的HW_Vsync信號(hào)。當(dāng)然它不是一直不停的在產(chǎn)生,這樣會(huì)導(dǎo)致Vsync信號(hào)的接收者不停的接收到繪制、渲染命令,即使它們并不需要,這樣會(huì)帶來(lái)嚴(yán)重的性能損耗,因?yàn)檫M(jìn)行了很多無(wú)用的繪制。所以它被設(shè)計(jì)設(shè)計(jì)成能夠喚醒和睡眠的。這使得HWComposer在需要時(shí)才產(chǎn)生Vsync信號(hào)(比如當(dāng)屏幕上的內(nèi)容需要改變時(shí)),不需要時(shí)進(jìn)入睡眠狀態(tài)(比如當(dāng)屏幕上的內(nèi)容保持不變時(shí),此時(shí)屏幕每次刷新都是顯示緩沖區(qū)里沒(méi)發(fā)生變化的內(nèi)容)。
如圖,Vsync的兩個(gè)接收者,一個(gè)是SurfaceFlinger(負(fù)責(zé)合成各個(gè)Surface),一個(gè)是Choreographer(負(fù)責(zé)控制視圖的繪制)。我們稍后再介紹,現(xiàn)在先知道它們是干什么的就行了。
Vsync offset機(jī)制
為了提高效率,盡量減少卡頓,在Android 4.1時(shí)引入了Vsync機(jī)制,并在隨后的4.4版本中加入Vsync offset偏移機(jī)制。
圖1. 為4.1時(shí)期的Vsync機(jī)制。可以看到,當(dāng)一個(gè)Vsync信號(hào)到來(lái)時(shí),SurfaceFlinger和UI繪制進(jìn)程會(huì)同時(shí)啟動(dòng),導(dǎo)致它們競(jìng)爭(zhēng)CPU資源,而CPU分配資源會(huì)耗費(fèi)時(shí)間,著降低系統(tǒng)性能。同時(shí)當(dāng)收到一個(gè)Vsync信號(hào)時(shí),第N幀開(kāi)始繪制。等再收到一個(gè)Vsync信號(hào)時(shí),第N幀才被SurfaceFlinger合成。而需要顯示到屏幕上,需要等都第三個(gè)Vsync信號(hào)。這是比較低效率。于是才有了圖2. 4.4版本加入的Vsync offset機(jī)制。
圖2. Google加入Vsync offset機(jī)制后,原本的HW_Vsync信號(hào)會(huì)經(jīng)過(guò)DispSync會(huì)分成Vsync和SF_Vsync兩個(gè)虛擬化的Vsync信號(hào)。其中Vsync信號(hào)會(huì)發(fā)送到Choreographer中,而SF_Vsync會(huì)發(fā)送到SurfaceFlinger中。理論上只要phase_app和phase_sf這兩個(gè)偏移參數(shù)設(shè)置合理,在繪制階段消耗的時(shí)間控制好,那么畫面就會(huì)像圖2中的前幾幀那樣有序流暢的進(jìn)行。理想總是美好的。實(shí)際上很難一直維持這種有序和流暢,比如frame_3是比較復(fù)雜的一幀,它的繪制完成的時(shí)間超過(guò)了SurfaceFlinger開(kāi)始合成的時(shí)間,所以它必須要等到下一個(gè)Vsync信號(hào)到來(lái)時(shí)才能被合成。這樣便造成了一幀的丟失。但即使是這樣,如你所見(jiàn),加入了Vsync offset機(jī)制后,繪制效率還是提高了很多。
從圖中可以看到,Vsync和SF_Vsync的偏移量分別由phase_app和phase_sf控制,這兩個(gè)值是可以調(diào)節(jié)的,默認(rèn)為0,可為負(fù)值。你只需要找到BoardConfig.mk文件,就可以對(duì)這兩個(gè)值進(jìn)行調(diào)節(jié)。
回到ViewRootImpl
前面介紹了幾個(gè)關(guān)鍵的概念,現(xiàn)在我們回到ViewRootImpl中去,在圖中找到ViewRootImpl的對(duì)應(yīng)位置。
前面說(shuō)過(guò),ViewRootImpl控制著一個(gè)Window中的整個(gè)視圖樹(shù)的繪制。那它是如何進(jìn)行控制的呢?一次繪制究竟是如何開(kāi)始的呢?
在ViewRootImpl創(chuàng)建的時(shí)候,會(huì)獲取到前面提到過(guò)過(guò)的一個(gè)關(guān)鍵對(duì)象Choreographer。Choreographer在一個(gè)線程中僅存在一個(gè)實(shí)例,因此在UI線程只有一個(gè)Choreographer存在。也就說(shuō),通常情況下,它相當(dāng)于一個(gè)應(yīng)用中的單例。
在ViewRootImpl初始化時(shí),會(huì)實(shí)現(xiàn)一個(gè)Choreographer.FrameCallback(這是一個(gè)Choreographer中的內(nèi)部類),并向Choreographer中post。顧名思義,F(xiàn)rameCallback會(huì)在每次接收到Vsync信號(hào)時(shí)被回調(diào)。
Choreographer.java
- public interface FrameCallback {
- public void doFrame(long frameTimeNanos);
- //一旦注冊(cè)到CallbackQueue中,那么
- //每次Choreographer接收到Vsync信號(hào)時(shí)都會(huì)回調(diào)。
- }
FrameCallback一旦被注冊(cè),那么每次收到Vsync信號(hào)時(shí)它都會(huì)被回調(diào)。利用它,我們可以實(shí)現(xiàn)會(huì)幀率的監(jiān)聽(tīng)。
ViewRootImpl.java
- //這個(gè)方法只有在ViewRootImpl初始化時(shí)才會(huì)被調(diào)用
- private void profileRendering(boolean enabled) {
- ...
- mRenderProfiler = new Choreographer.FrameCallback() {
- @Override
- public void doFrame(long frameTimeNanos) {
- ...
- scheduleTraversals();
- //請(qǐng)求一個(gè)Vsync信號(hào),后面還會(huì)提到這個(gè)方法
- mChoreographer.postFrameCallback(mRenderProfiler);
- //每次回調(diào)時(shí),重新將FrameCallback post到Choreographer中
- ...
- }
- };
- ...
- mChoreographer.postFrameCallback(mRenderProfiler);
- //將FrameCallback post到Choreographer中
- ...
- }
上面代碼出現(xiàn)了一個(gè)重要方法scheduleTraversals()。下面我們看看它究竟為何重要。 ViewRootImpl.java
- void scheduleTraversals() {
- ...
- mChoreographer.postCallback(
- Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
- //向Choreographer中post一個(gè)TraversalRunnable
- //這又是一個(gè)十分重要的對(duì)象
- ...
- }
可以看出scheduleTraversals()每次調(diào)用時(shí)會(huì)向Choreographer中post一個(gè)TraversalRunnable,它會(huì)促使Choreographer去請(qǐng)求一個(gè)Vsync信號(hào)。所以這個(gè)方法的作用就是用來(lái)請(qǐng)求一次Vsync信號(hào)刷新界面的。事實(shí)上,你可以看到,在invalidate()、requestLayout()等操作中,都能夠看到它被調(diào)用。原因就是這些操作需要刷新界面,所以需要請(qǐng)求一個(gè)Vsync信號(hào)來(lái)出發(fā)新界面的繪制。
ViewRootImpl.java
- final class TraversalRunnable implements Runnable {
- @Override
- public void run() {
- doTraversal();
- //開(kāi)始遍歷視圖樹(shù),這意味著開(kāi)始繪制一幀內(nèi)容了
- }
- }
從圖中可以看到,每當(dāng)doTraversal()被調(diào)用時(shí),一系列的測(cè)量、布局和繪制操作就開(kāi)始了。在繪制時(shí),會(huì)通過(guò)Surface來(lái)獲取一個(gè)Canvas內(nèi)存塊交給DecorView,用于視圖的繪制。整個(gè)View視圖的內(nèi)容都是被繪制到這個(gè)Canvas中。
Choreographer中的風(fēng)起云涌
前面反復(fù)提到向Choreographer中post回調(diào),那么post過(guò)去發(fā)生了些什么呢?從圖中可以看到,所有的post操作最終都進(jìn)入到postCallbackDelayedInternal()中。
Choreographer.java
- private void postCallbackDelayedInternal(int callbackType,
- Object action, Object token, long delayMillis) {
- ...
- synchronized (mLock) {
- final long now = SystemClock.uptimeMillis();
- final long dueTime = now + delayMillis;
- mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
- //將Callback添加到CallbackQueue[]中
- if (dueTime <= now) {
- scheduleFrameLocked(now);
- //如果回調(diào)時(shí)間到了,請(qǐng)求一個(gè)Vsync信號(hào)
- //在接收到后會(huì)調(diào)用doFrame()回調(diào)這個(gè)Callback。
- } else {
- Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
- msg.arg1 = callbackType;
- msg.setAsynchronous(true);
- //異步消息,避免被攔截器攔截
- mHandler.sendMessageAtTime(msg, dueTime);
- //如果還沒(méi)到回調(diào)的時(shí)間,向FrameHandelr中發(fā)送
- //MSG_DO_SCHEDULE_CALLBACK消息
- }
- }
- ...
- }
上面這段代碼會(huì)把post到Choreographer中的Callback添加到Callback[]中,并且當(dāng)它因該被回調(diào)時(shí),請(qǐng)求一個(gè)Vsync信號(hào),在接收到下一個(gè)Vsync信號(hào)時(shí)回調(diào)這個(gè)Callback。如果沒(méi)有到回調(diào)的時(shí)間,則向FrameHandler中發(fā)送一個(gè)MSG_DO_SCHEDULE_CALLBACK消息,但最終還是會(huì)請(qǐng)求一個(gè)Vsync信號(hào),然后回調(diào)這個(gè)Callback。
簡(jiǎn)單提一下CallbackQueue:簡(jiǎn)單說(shuō)一下CallbackQueue。它和MessageQueue差不多,都是單鏈表結(jié)構(gòu)。在我的這篇【驚天秘密!從Thread開(kāi)始,揭露Android線程通訊的詭計(jì)和主線程的陰謀http://www.jianshu.com/p/8862bd2b6a29】文章中,你能夠看到更多關(guān)于MessageQueue和Handler機(jī)制的內(nèi)容。不同的是它同時(shí)還是一個(gè)一維數(shù)組,下標(biāo)表示Callback類型。事實(shí)上,算上每種類型的單鏈表結(jié)構(gòu),它更像是二維數(shù)組的樣子。簡(jiǎn)單點(diǎn)描述,假設(shè)有一個(gè)MessageQueue[]數(shù)組,里面存了幾個(gè)MessageQueue。來(lái)看看它的創(chuàng)建你可能就明白,它是在Choreographer初始化時(shí)創(chuàng)建的。
- private Choreographer(Looper looper) {
- mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
- //CALLBACK_LAST值為3。
- for (int i = 0; i <= CALLBACK_LAST; i++) {
- mCallbackQueues[i] = new CallbackQueue();
- }
- }
現(xiàn)在來(lái)看看前面代碼中調(diào)用的scheduleFrameLocked()是如何請(qǐng)求一個(gè)Vsync信號(hào)的。
- private void scheduleFrameLocked(long now) {
- ...
- //先判斷當(dāng)前是不是在UI線程
- if (isRunningOnLooperThreadLocked()) {
- scheduleVsyncLocked();
- //是UI線程就請(qǐng)求一個(gè)Vsync信號(hào)
- } else {
- Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
- msg.setAsynchronous(true);
- mHandler.sendMessageAtFrontOfQueue(msg);
- //不在UI線程向FrameHandler發(fā)送一個(gè)MSG_DO_SCHEDULE_VSYNC消息
- //來(lái)請(qǐng)求一個(gè)Vsync信號(hào)
- }
- }
- private void scheduleVsyncLocked() {
- mDisplayEventReceiver.scheduleVsync();
- //通過(guò)DisplayEventReceiver請(qǐng)求一個(gè)Vsync信號(hào)
- //這是個(gè)恨角色,待會(huì)兒會(huì)聊聊它。
- //MSG_DO_SCHEDULE_VSYNC消息也是通過(guò)調(diào)用這個(gè)方法請(qǐng)求Vsync信號(hào)的。
- }
上面我們提到過(guò),Choreographer在一個(gè)線程中只有一個(gè)。所以,如果在其它線程,需要通過(guò)Handler來(lái)切換到UI線程,然后再請(qǐng)求Vsync信號(hào)。
下面看看剛剛出場(chǎng)的mDisplayEventReceiver是個(gè)什么鬼?
- private final class FrameDisplayEventReceiver extends DisplayEventReceiver
- implements Runnable {
- //這個(gè)方法用于接收Vsync信號(hào)
- public void onVsync(){
- ...
- Message msg = Message.obtain(mHandler, this);
- msg.setAsynchronous(true);
- mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
- //這里并沒(méi)有設(shè)置消息的類型
- //其實(shí)就是默認(rèn)為0,即MSG_DO_FRAME類型的消息
- //它其實(shí)就是通知Choreographer開(kāi)始回調(diào)CallbackQueue[]中的Callback了
- //也就是開(kāi)始繪制下一幀的內(nèi)容了
- }
- //這個(gè)方法是在父類中的,寫在這方便看
- public void scheduleVsync() {
- ...
- nativeScheduleVsync(mReceiverPtr);
- //請(qǐng)求一個(gè)Vsync信號(hào)
- }
- }
這給類功能比較明確,而且很重要!
上面一直在說(shuō)向FrameHandler中發(fā)消息,搞得神神秘秘的。接下來(lái)就來(lái)看看FrameHandler本尊。請(qǐng)?jiān)趫D中找到對(duì)應(yīng)位置哦。
- private final class FrameHandler extends Handler {
- public FrameHandler(Looper looper) {
- super(looper);
- }
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_DO_FRAME:
- //開(kāi)始回調(diào)Callback,以開(kāi)始繪制下一幀內(nèi)容
- doFrame(System.nanoTime(), 0);
- break;
- case MSG_DO_SCHEDULE_VSYNC:
- //請(qǐng)求一個(gè)Vsync信號(hào)
- doScheduleVsync();
- break;
- case MSG_DO_SCHEDULE_CALLBACK:
- //實(shí)際也是請(qǐng)求一個(gè)Vsync信號(hào)
- doScheduleCallback(msg.arg1);
- break;
- }
- }
- }
FrameHandler主要在UI線程處理3種類型的消息。
- MSG_DO_FRAME:值為0。當(dāng)接收到一個(gè)Vsync信號(hào)時(shí)會(huì)發(fā)送該種類型的消息,然后開(kāi)始回調(diào)CallbackQueue[]中的Callback。比如上面說(shuō)過(guò),在ViewRootImpl有兩個(gè)重要的Callback,F(xiàn)rameCallback(請(qǐng)求Vsync并再次注冊(cè)回調(diào))和TraversalRunnable(執(zhí)行doTraversal()開(kāi)始繪制界面)頻繁被注冊(cè)。
- MSG_DO_SCHEDULE_VSYNC:值為1。當(dāng)需要請(qǐng)求一個(gè)Vsync消息(即屏幕上的內(nèi)容需要更新時(shí))會(huì)發(fā)送這個(gè)消息。接收到Vsync后,同上一步。
- MSG_DO_SCHEDULE_CALLBACK:值為2。請(qǐng)求回調(diào)一個(gè)Callback。實(shí)際上會(huì)先請(qǐng)求一個(gè)Vsync信號(hào),然后再發(fā)送MSG_DO_FRAME消息,然后再回調(diào)。
FrameHandler并不復(fù)雜,但在UI的繪制過(guò)程中具有重要的作用,所以一定要結(jié)合圖梳理下這個(gè)流程。
SurfaceFlinger和Surface簡(jiǎn)單說(shuō)
在介紹Vsync的時(shí)候,我們可能已經(jīng)看到了,現(xiàn)在Android系統(tǒng)會(huì)將HW_VSYNC虛擬化為兩個(gè)Vsync信號(hào)。一個(gè)是VSYNC,被發(fā)送給上面一直在講的Choreographer,用于觸發(fā)視圖樹(shù)的繪制渲染。另一個(gè)是SF_VSYNC,被發(fā)送給我接下來(lái)要講的SurfaceFlinger,用于觸發(fā)Surface的合成,即各個(gè)Window窗口畫面的合成。接下來(lái)我們就簡(jiǎn)單的看下SurfaceFlinger和Surface。由于這部分基本是c++編寫的,我著重講原理。
隱藏在背后的Surface
平時(shí)同學(xué)們都知道,我們的視圖需要被繪制。那么它們被繪制到那了呢?也許很多童鞋腦海里立即浮現(xiàn)出一個(gè)詞:Canvas。但是,~沒(méi)錯(cuò)!就是繪制到了Canvas上。那么Canvas又是怎么來(lái)的呢?是的,它可以New出來(lái)的。但是前面提到過(guò),我們Window中的視圖樹(shù)都是被繪制到一個(gè)由Surface提供的Canvas上。忘了的童鞋面壁思過(guò)😄。
Canvas實(shí)際代表了一塊內(nèi)存,用于儲(chǔ)存繪制出來(lái)的數(shù)據(jù)。在Canvas的構(gòu)造器中你可以看到:
- public Canvas() {
- ...
- mNativeCanvasWrapper = initRaster(null);
- //申請(qǐng)一塊內(nèi)存,并且返回該內(nèi)存的一個(gè)long類型的標(biāo)記或者索引。
- ...
- }
可以看到,Canvas實(shí)際主要就是持有了一塊用于繪制的內(nèi)存塊的索引long mNativeCanvasWrapper。每次繪制時(shí)就通過(guò)這個(gè)索引找到對(duì)應(yīng)的內(nèi)存塊,然后將數(shù)據(jù)繪制到內(nèi)存中。比如:
- public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {
- native_drawRect(mNativeCanvasWrapper,
- rect.left, rect.top, rect.right, rect.bottom, paint.getNativeInstance());
- //在mNativeCanvasWrapper標(biāo)記的內(nèi)存中繪制一個(gè)矩形。
- }
簡(jiǎn)單的說(shuō)一下。Android繪制圖形是通過(guò)圖形庫(kù)Skia(主要針對(duì)2D)或OpenGL(主要針對(duì)3D)進(jìn)行。圖形庫(kù)是個(gè)什么概念?就好比你在PC上用畫板畫圖,此時(shí)畫板就相當(dāng)于Android中的圖形庫(kù),它提供了一系列標(biāo)準(zhǔn)化的工具供我們畫圖使用。比如我們drawRect()實(shí)際就是操作圖形庫(kù)在內(nèi)存上寫入了一個(gè)矩形的數(shù)據(jù)。
扯多了,我們繼續(xù)回到Surface上。當(dāng)ViewRootImpl執(zhí)行到draw()方法(即開(kāi)始繪制圖形數(shù)據(jù)了),會(huì)根據(jù)是否開(kāi)啟了硬件(從Android 4.0開(kāi)始默認(rèn)是開(kāi)啟的)加速來(lái)決定是使用CPU軟繪制還是使用GPU硬繪制。如果使用軟繪制,圖形數(shù)據(jù)會(huì)繪制在Surface默認(rèn)的CompatibleCanvas上(和普通Canvas的唯一區(qū)別就是對(duì)Matrix進(jìn)行了處理,提高在不同設(shè)備上的兼容性)。如果使用了硬繪制,圖形數(shù)據(jù)會(huì)被繪制在DisplayListCanvas上。DisplayListCanvas會(huì)通過(guò)GPU使用openGL圖形庫(kù)進(jìn)行繪制,因此具有更高的效率。
前面也簡(jiǎn)單說(shuō)了一下,每一個(gè)Window都會(huì)有一個(gè)自己的Surface,也就是說(shuō)一個(gè)應(yīng)用程序中會(huì)存在多個(gè)Surface。通過(guò)上面的講解,童鞋們也都知道了Surface的作用就是管理用于繪制視圖樹(shù)的Canvas的。這個(gè)Surface是和SurfaceFlinger共享,從它實(shí)現(xiàn)了Parcelable接口也可以才想到它會(huì)被序列化傳遞。事實(shí)上,Surface中的繪制數(shù)據(jù)是通過(guò)匿名共享內(nèi)存的方式和SurfaceFlinger共享的,這樣SurfaceFlinger可以根據(jù)不同的Surface,找到它所對(duì)應(yīng)的內(nèi)存區(qū)域中的繪制數(shù)據(jù),然后進(jìn)行合成。
合成師SurfaceFlinger
SurfaceFlinger是系統(tǒng)的一個(gè)服務(wù)。前面也一直在提到它專門負(fù)責(zé)把每個(gè)Surface中的內(nèi)容合成緩存,以待顯示到屏幕上。SurfaceFlinger在合成Surface時(shí)是根據(jù)Surface的Z-order順序一層一層進(jìn)行。比如一個(gè)Dialog的Surface就會(huì)在Activity的Surface上面。然后這個(gè)東西不多提了。
終于可以說(shuō)說(shuō)你的App為什么這么卡的原因了
通過(guò)對(duì)Android繪制機(jī)制的了解,我們知道造成應(yīng)用卡頓的根源就在于16ms內(nèi)不能完成繪制渲染合成過(guò)程,因?yàn)锳ndroid平臺(tái)的硬件刷新率為60HZ,大概就是16ms刷新一次。如果沒(méi)能在16ms內(nèi)完成這個(gè)過(guò)程,就會(huì)使屏幕重復(fù)顯示上一幀的內(nèi)容,即造成了卡頓。在這16ms內(nèi),需要完成視圖樹(shù)的所有測(cè)量、布局、繪制渲染及合成。而我們的優(yōu)化工作主要就是針對(duì)這個(gè)過(guò)程的。
復(fù)雜的視圖樹(shù)
如果視圖樹(shù)復(fù)雜,會(huì)使整個(gè)Traversal過(guò)程變長(zhǎng)。因此,我們?cè)陂_(kāi)發(fā)過(guò)程中要控制視圖樹(shù)的復(fù)雜程度。減少不必要的層級(jí)嵌套。比如使用RelativeLayout可以減少?gòu)?fù)雜布局的嵌套。比如使用【震驚!這個(gè)控件絕對(duì)值得收藏。輕松實(shí)現(xiàn)圓角、文字描邊、狀態(tài)指示等效果http://www.jianshu.com/p/cfe18cbc6924】😄,這個(gè)控件可以減少既需要顯示文字,又需要圖片和特殊背景的需求的布局復(fù)雜程度,所有的東西由一個(gè)控件實(shí)現(xiàn)。
頻繁的requestlayout()
如果頻繁的觸發(fā)requestLayout(),就可能會(huì)導(dǎo)致在一幀的周期內(nèi),頻繁的發(fā)生布局計(jì)算,這也會(huì)導(dǎo)致整個(gè)Traversal過(guò)程變長(zhǎng)。有的ViewGroup類型的控件,比如RelativeLayout,在一幀的周期內(nèi)會(huì)通過(guò)兩次layout()操作來(lái)計(jì)算確認(rèn)子View的位置,這種少量的操作并不會(huì)引起能夠被注意到的性能問(wèn)題。但是如果在一幀的周期內(nèi)頻繁的發(fā)生layout()計(jì)算,就會(huì)導(dǎo)致嚴(yán)重的性能,每次計(jì)算都是要消耗時(shí)間的!而requestLayout()操作,會(huì)向ViewRootImpl中一個(gè)名為mLayoutRequesters的List集合里添加需要重新Layout的View,這些View將在下一幀中全部重新layout()一遍。通常在一個(gè)控件加載之后,如果沒(méi)什么變化的話,它不會(huì)在每次的刷新中都重新layout()一次,因?yàn)檫@是一個(gè)費(fèi)時(shí)的計(jì)算過(guò)程。所以,如果每一幀都有許多View需要進(jìn)行l(wèi)ayout()操作,可想而知你的界面將會(huì)卡到爆!卡到爆!需要注意,setLayoutParams()最終也會(huì)調(diào)用requestLayout(),所以也不能爛用!同學(xué)們?cè)趯懘a的過(guò)程中一定要謹(jǐn)慎注意那些可能引起requestLayout()的地方啊!
UI線程被阻塞
如果UI線程受到阻塞,顯而易見(jiàn)的是,我們的Traversal過(guò)程也將受阻塞!畫面卡頓是妥妥的發(fā)生啊。這就是為什么大家一直在強(qiáng)調(diào)不要在UI線程做耗時(shí)操作的原因。通常UI線程的阻塞和以下原因脫不了關(guān)系。
在UI線程中進(jìn)行IO讀寫數(shù)據(jù)的操作。這是一個(gè)很費(fèi)時(shí)的過(guò)程好嗎?千萬(wàn)別這么干。如果不想獲得一個(gè)卡到爆的App的話,把IO操作統(tǒng)統(tǒng)放到子線程中去。
在UI線程中進(jìn)行復(fù)雜的運(yùn)算操作。運(yùn)算本身是一個(gè)耗時(shí)的操作,當(dāng)然簡(jiǎn)單的運(yùn)算幾乎瞬間完成,所以不會(huì)讓你感受到它在耗時(shí)。但是對(duì)于十分復(fù)雜的運(yùn)算,對(duì)時(shí)間的消耗是十分辣眼睛的!如果不想獲得一個(gè)卡到爆的App的話,把復(fù)雜的運(yùn)算操作放到子線程中去。
在UI線程中進(jìn)行復(fù)雜的數(shù)據(jù)處理。我說(shuō)的是比如數(shù)據(jù)的加密、解密、編碼等等。這些操作都需要進(jìn)行復(fù)雜運(yùn)算,特別是在數(shù)據(jù)比較復(fù)雜的時(shí)候。如果不想獲得一個(gè)卡到爆的App的話,把復(fù)雜數(shù)據(jù)的處理工作放到子線程中去。
頻繁的發(fā)生GC,導(dǎo)致UI線程被頻繁中斷。在Java中,發(fā)生GC(垃圾回收)意味著Stop-The-World,就是說(shuō)其它線程全部會(huì)被暫停啊。好可怕!正常的GC導(dǎo)致偶然的畫面卡頓是可以接受的,但是頻繁發(fā)生就讓人很蛋疼了!頻繁GC的罪魁禍?zhǔn)资莾?nèi)存抖動(dòng),這個(gè)時(shí)候就需要看下我的這篇【Android內(nèi)存基礎(chǔ)——內(nèi)存抖動(dòng)http://www.jianshu.com/p/69e6f894c698】文章了。簡(jiǎn)單的說(shuō)就是在短時(shí)間內(nèi)頻繁的創(chuàng)建大量對(duì)象,導(dǎo)致達(dá)到GC的閥值,然后GC就發(fā)生了。如果不想獲得一個(gè)卡到爆的App的話,把內(nèi)存的管理做好,即使這是Java。
故意阻塞UI線程。好吧,相信沒(méi)人會(huì)這么干吧。比如sleep()一下?
總結(jié)
抽出空余時(shí)間寫文章分享需要?jiǎng)恿Γ€請(qǐng)各位看官動(dòng)動(dòng)小手點(diǎn)個(gè)贊,鼓勵(lì)下嘍😄
我一直在不定期的創(chuàng)作新的干貨,想要上車只需進(jìn)到我的個(gè)人主頁(yè)點(diǎn)個(gè)關(guān)注就好了哦。發(fā)車嘍~
整篇下來(lái),相信童鞋對(duì)Android的繪制機(jī)制也有了一個(gè)比較全面的了解?,F(xiàn)在回過(guò)頭來(lái)再寫代碼時(shí)是不是有種知根知底的自信呢?😄
參考鏈接
- Implementing VSYNC:https://source.android.com/devices/graphics/implement-vsync
- SurfaceFlinger and Hardware Composer:https://source.android.com/devices/graphics/arch-sf-hwc
- Surface and SurfaceHolder:https://source.android.com/devices/graphics/arch-sh
- Implementing the Hardware Composer HAL:https://source.android.com/devices/graphics/implement-hwc
- 可能是史上最簡(jiǎn)單的!一張圖3分鐘讓你明白Activity啟動(dòng)流程,不看后悔!http://www.jianshu.com/p/9ecea420eb52
- 驚天秘密!從Thread開(kāi)始,揭露Android線程通訊的詭計(jì)和主線程的陰謀http://www.jianshu.com/p/8862bd2b6a29
- 震驚!這個(gè)控件絕對(duì)值得收藏。輕松實(shí)現(xiàn)圓角、文字描邊、狀態(tài)指示等效果http://www.jianshu.com/p/cfe18cbc6924
- Android內(nèi)存基礎(chǔ)——內(nèi)存抖動(dòng)http://www.jianshu.com/p/69e6f894c698
- Android性能優(yōu)化之渲染篇http://hukai.me/android-performance-render/
- Android硬件加速原理與實(shí)現(xiàn)簡(jiǎn)介http://tech.meituan.com/hardware-accelerate.html
- Android SurfaceFlinger對(duì)VSync信號(hào)的處理過(guò)程分析http://blog.csdn.net/yangwen123/article/details/17001405
- Android Vsync 原理http://www.10tiao.com/html/431/201601/401709603/1.html
- Android Choreographer 源碼分析http://www.jianshu.com/p/996bca12eb1d?utm_campaign=hugo&utm_medium=reader_share&utm_content=note
- Android應(yīng)用程序窗口(Activity)的視圖對(duì)象(View)的創(chuàng)建過(guò)程分析:http://blog.csdn.net/luoshengyang/article/details/8245546
- Android 4.4(KitKat)中VSync信號(hào)的虛擬化http://blog.csdn.net/jinzhuojun/article/details/17293325
- Understanding necessity of Android VSYNC signals:http://stackoverflow.com/questions/27947848/understanding-necessity-of-android-vsync-signals