天天看點

用兩張圖告訴你,為什麼你的App會卡頓?

用兩張圖告訴你,為什麼你的App會卡頓?

從這篇文章中你能獲得這些料:

知道setcontentview()之後發生了什麼?

知道android究竟是如何在螢幕上顯示我們期望的畫面的?

對android的視圖架構有整體把握。

學會從根源處分析畫面卡頓的原因。

掌握如何編寫一個流暢的app的技巧。

從源碼中學習android的細想。

收獲兩張自制圖,幫助你了解android的視圖架構。

用兩張圖告訴你,為什麼你的App會卡頓?

上面這段代碼想必androider們大都已經不能再熟悉的更多了。但是你知道這樣寫了之後發生什麼了嗎?這個布局到底被添加到哪了?我的天,知識點來了!

可能很多同學也知道這個布局是被放到了一個叫做decorview的父布局裡,但是我還是要再說一遍。且看下圖️

用兩張圖告訴你,為什麼你的App會卡頓?

這個圖可能和夥伴們在書上或者網上常見的不太一樣,為什麼不太一樣呢?因為是我自己畫的,哈哈哈...

下面就來看着圖捋一捋android最基本的視圖架構。

估計很多同學都知道,每一個activity都擁有一個window對象的執行個體。這個執行個體實際是phonewindow類型的。那麼phonewindow從名字很容易看出,它應該是window的兒子(即子類)!

那麼,phonewindow有什麼用呢?它在activity充當什麼角色呢?下面我就姑且把phonewindow等同于window來稱呼吧。

用兩張圖告訴你,為什麼你的App會卡頓?

window從字面看它是一個視窗,意思和pc上的視窗概念有點像。但也不是那麼準确。看圖說。可以看到,我們要顯示的布局是被放到它的屬性mdecor中的,這個mdecor就是decorview的一個執行個體。下面會專門撸decorview,現在先把關注點放到window上。window還有一個比較重要的屬性mwindowmanager,它是windowmanager(這是個接口)的一個實作類的一個執行個體。我們平時通過getwindowmanager()方法獲得的東西就是這個mwindowmanager。顧名思義,它是window的管理者,負責管理着視窗及其中顯示的内容。它的實際實作類是windowmanagerimpl。可能童鞋們現在正在phonewindow中尋找着這個mwindowmanager是在哪裡執行個體化的,是不是上下來復原動着這個類都找不見?stop!mwindowmanager是在它爹那裡就執行個體化好的。下面代碼是在window.java中的。

通過上面的介紹,我們已經知道了window中有負責承載布局的decorview,有負責管理的windowmanager(事實上它隻是個代理,後面會講它代理的是誰)。

前面提到過,在activity的oncreate()中通過setcontentview()設定的布局實際是被放到decorview中的。我們在圖中找到decorview。

從圖中可以看到,decorview繼承了framelayout,并且一般情況下,它會在先添加一個預設的布局。比如decorcaptionview,它是從上到下放置自己的子布局的,相當于一個linearlayout。通常它會有一個标題欄,然後有一個容納内容的mcontentroot,這個布局的類型視情況而定。我們希望顯示的布局就是放到了mcontentroot中。

前面已經提到過,windowmanager在window中具有很重要的作用。我們先在圖中找到它。這裡需要先說明一點,在phonewindow中的mwindowmanager實際是windowmanagerimpl類型的。windowmanagerimpl自然就是接口windowmanager的一個實作類喽。這一點是我沒有在圖中反映的。

繼續看圖。windowmanagerimpl持有了phonewindow的引用,是以它可以對phonewindow進行管理。同時它還持有一個非常重要的引用mglobal。這個mglobal指向一個windowmanagerglobal類型的單例對象,這個單例每個應用程式隻有唯一的一個。在圖中,我說明了windowmanagerglobal維護了本應用程式内所有window的decorview,以及與每一個decorview對應關聯的viewrootimpl。這也就是為什麼我前面提到過,windowmanager隻是一個代理,實際的管理功能是通過windowmanagerglobal實作的。我們來看個源碼的例子就比較清晰了。開始啦!

用兩張圖告訴你,為什麼你的App會卡頓?

從上面可以看到,當activity執行onresume()的時候就會添加視圖,或者重新整理視圖。需要解釋一點:windowmanager實作了viewmanager接口。

如圖中所說,windowmanagerglobal調用addview()的時候會把decorview添加到它維護的數組中去,并且會建立另一個關鍵且極其重要的viewrootimpl(這個必須要專門講一下)類型的對象,并且也會把它存到一個數組中維護。

可以看出viewrootimpl是在activity執行onresume()的時候才被建立的,并且此時才把decorview傳進去讓它管理。

viewrootimpl能夠和系統的windowmanagerservice進行互動,并且管理着decorview的繪制和視窗狀态。非常的重要。趕緊在圖中找到對應位置吧!

viewrootimpl并不是一個view,而是負責管理視圖的。它配合系統來完成對一個window内的視圖樹的管理。從圖中也可以看到,它持有了decorview的引用,并且視圖樹它是視圖樹繪制的起點。是以,viewrootimpl會稍微複雜一點,需要我們更深入的去了解,在圖中我标出了它比較重要的組成surface和choreographer等都會在後面提到。

到此,我們已經一起把第一張圖撸了一遍了,現在童鞋們因該對android視圖架構有了大緻的了解。下面将更進一步的去了解android的繪制機制。

下面将會詳細的講解為什麼我們設定的視圖能夠被繪制到螢幕上?這中間究竟隐藏着怎樣的離奇?看完之後,你自然就能夠從根源知道為什麼你的app會那麼卡,以及開始有思路着手解決這些卡頓。

用兩張圖告訴你,為什麼你的App會卡頓?

同樣用一張圖來展示這個過程。由于android繪制機制确實有點複雜,是以第一眼看到的時候你的内心中可能蹦騰了一萬隻草泥馬。不要怕!我們從源頭開始,一點一點的梳理這個看似複雜的繪制機制。為什麼說看似複雜呢?因為這個過程隻需要幾分鐘。just do it!

整天聽到cpu、gpu的,你知道他們是幹什麼的嗎?這裡簡單的提一下,幫助了解後面的内容。

在android的繪制架構中,cpu主要負責了視圖的測量、布局、記錄、把内容計算成polygons多邊形或者texture紋理,而gpu主要負責把polygons或者textture進行rasterization栅格化,這樣才能在螢幕上成像。在使用硬體加速後,gpu會分擔cpu的計算任務,而cpu會專注處理邏輯,這樣減輕cpu的負擔,使得整個系統效率更高。

用兩張圖告訴你,為什麼你的App會卡頓?

refreshrate重新整理率是螢幕每秒重新整理的次數,是一個與硬體有關的固定值。在android平台上,這個值一般為60hz,即螢幕每秒重新整理60次。

framerate幀率是每秒繪制的幀數。通常隻要幀數和重新整理率保持一緻,就能夠看到流暢的畫面。在android平台,我們應該盡量維持60fps的幀率。但有時候由于視圖的複雜,它們可能就會出現不一緻的情況。

用兩張圖告訴你,為什麼你的App會卡頓?

如圖,當幀率小于重新整理率時,比如圖中的30fps < 60hz,就會出現相鄰兩幀看到的是同一個畫面,這就造成了卡頓。這就是為什麼我們總會說,要盡量保證一幀畫面能夠在16ms内繪制完成,就是為了和螢幕的重新整理率保持同步。

下面将會介紹android是如何來確定重新整理率和幀率保持同步的。

你可能在遊戲的設定中見過vsync,開啟它通常能夠提高遊戲性能。在android中,同樣使用vsync垂直同步來提高顯示性能。它能夠使幀率framerate和硬體的refreshrate重新整理強制保持一緻。

hwcomposer與vsync不得不說的事

看圖啦看圖啦。首先在最左邊我們看到有個叫hwcomposer的類,這是一個c++編寫的類。它android系統初始化時就被建立,然後開始配合硬體産生vsync信号,也就是圖中的hw_vsync信号。當然它不是一直不停的在産生,這樣會導緻vsync信号的接收者不停的接收到繪制、渲染指令,即使它們并不需要,這樣會帶來嚴重的性能損耗,因為進行了很多無用的繪制。是以它被設計設計成能夠喚醒和睡眠的。這使得hwcomposer在需要時才産生vsync信号(比如當螢幕上的内容需要改變時),不需要時進入睡眠狀态(比如當螢幕上的内容保持不變時,此時螢幕每次重新整理都是顯示緩沖區裡沒發生變化的内容)。

如圖,vsync的兩個接收者,一個是surfaceflinger(負責合成各個surface),一個是choreographer(負責控制視圖的繪制)。我們稍後再介紹,現在先知道它們是幹什麼的就行了。

vsync offset機制

為了提高效率,盡量減少卡頓,在android 4.1時引入了vsync機制,并在随後的4.4版本中加入vsync offset偏移機制。

用兩張圖告訴你,為什麼你的App會卡頓?

圖1. 為4.1時期的vsync機制。可以看到,當一個vsync信号到來時,surfaceflinger和ui繪制程序會同時啟動,導緻它們競争cpu資源,而cpu配置設定資源會耗費時間,着降低系統性能。同時當收到一個vsync信号時,第n幀開始繪制。等再收到一個vsync信号時,第n幀才被surfaceflinger合成。而需要顯示到螢幕上,需要等都第三個vsync信号。這是比較低效率。于是才有了圖2. 4.4版本加入的vsync offset機制。

圖2. google加入vsync offset機制後,原本的hw_vsync信号會經過dispsync會分成vsync和sf_vsync兩個虛拟化的vsync信号。其中vsync信号會發送到choreographer中,而sf_vsync會發送到surfaceflinger中。理論上隻要phase_app和phase_sf這兩個偏移參數設定合理,在繪制階段消耗的時間控制好,那麼畫面就會像圖2中的前幾幀那樣有序流暢的進行。理想總是美好的。實際上很難一直維持這種有序和流暢,比如frame_3是比較複雜的一幀,它的繪制完成的時間超過了surfaceflinger開始合成的時間,是以它必須要等到下一個vsync信号到來時才能被合成。這樣便造成了一幀的丢失。但即使是這樣,如你所見,加入了vsync offset機制後,繪制效率還是提高了很多。

從圖中可以看到,vsync和sf_vsync的偏移量分别由phase_app和phase_sf控制,這兩個值是可以調節的,預設為0,可為負值。你隻需要找到boardconfig.mk檔案,就可以對這兩個值進行調節。

前面介紹了幾個關鍵的概念,現在我們回到viewrootimpl中去,在圖中找到viewrootimpl的對應位置。

前面說過,viewrootimpl控制着一個window中的整個視圖樹的繪制。那它是如何進行控制的呢?一次繪制究竟是如何開始的呢?

用兩張圖告訴你,為什麼你的App會卡頓?

在viewrootimpl建立的時候,會擷取到前面提到過過的一個關鍵對象choreographer。choreographer在一個線程中僅存在一個執行個體,是以在ui線程隻有一個choreographer存在。也就說,通常情況下,它相當于一個應用中的單例。

在viewrootimpl初始化時,會實作一個choreographer.framecallback(這是一個choreographer中的内部類),并向choreographer中post。顧名思義,framecallback會在每次接收到vsync信号時被回調。

framecallback一旦被注冊,那麼每次收到vsync信号時它都會被回調。利用它,我們可以實作會幀率的監聽。

上面代碼出現了一個重要方法scheduletraversals()。下面我們看看它究竟為何重要。 viewrootimpl.java

可以看出scheduletraversals()每次調用時會向choreographer中post一個traversalrunnable,它會促使choreographer去請求一個vsync信号。是以這個方法的作用就是用來請求一次vsync信号重新整理界面的。事實上,你可以看到,在invalidate()、requestlayout()等操作中,都能夠看到它被調用。原因就是這些操作需要重新整理界面,是以需要請求一個vsync信号來出發新界面的繪制。

從圖中可以看到,每當dotraversal()被調用時,一系列的測量、布局和繪制操作就開始了。在繪制時,會通過surface來擷取一個canvas記憶體塊交給decorview,用于視圖的繪制。整個view視圖的内容都是被繪制到這個canvas中。

前面反複提到向choreographer中post回調,那麼post過去發生了些什麼呢?從圖中可以看到,所有的post操作最終都進入到postcallbackdelayedinternal()中。

用兩張圖告訴你,為什麼你的App會卡頓?

上面這段代碼會把post到choreographer中的callback添加到callback[]中,并且當它因該被回調時,請求一個vsync信号,在接收到下一個vsync信号時回調這個callback。如果沒有到回調的時間,則向framehandler中發送一個msg_do_schedule_callback消息,但最終還是會請求一個vsync信号,然後回調這個callback。

現在來看看前面代碼中調用的scheduleframelocked()是如何請求一個vsync信号的。

上面我們提到過,choreographer在一個線程中隻有一個。是以,如果在其它線程,需要通過handler來切換到ui線程,然後再請求vsync信号。

下面看看剛剛出場的mdisplayeventreceiver是個什麼鬼?

用兩張圖告訴你,為什麼你的App會卡頓?

這給類功能比較明确,而且很重要!

用兩張圖告訴你,為什麼你的App會卡頓?

上面一直在說向framehandler中發消息,搞得神神秘秘的。接下來就來看看framehandler本尊。請在圖中找到對應位置哦。

framehandler主要在ui線程處理3種類型的消息。

msg_do_frame:值為0。當接收到一個vsync信号時會發送該種類型的消息,然後開始回調callbackqueue[]中的callback。比如上面說過,在viewrootimpl有兩個重要的callback,framecallback(請求vsync并再次注冊回調)和traversalrunnable(執行dotraversal()開始繪制界面)頻繁被注冊。

msg_do_schedule_vsync:值為1。當需要請求一個vsync消息(即螢幕上的内容需要更新時)會發送這個消息。接收到vsync後,同上一步。

msg_do_schedule_callback:值為2。請求回調一個callback。實際上會先請求一個vsync信号,然後再發送msg_do_frame消息,然後再回調。

framehandler并不複雜,但在ui的繪制過程中具有重要的作用,是以一定要結合圖梳理下這個流程。

在介紹vsync的時候,我們可能已經看到了,現在android系統會将hw_vsync虛拟化為兩個vsync信号。一個是vsync,被發送給上面一直在講的choreographer,用于觸發視圖樹的繪制渲染。另一個是sf_vsync,被發送給我接下來要講的surfaceflinger,用于觸發surface的合成,即各個window視窗畫面的合成。接下來我們就簡單的看下surfaceflinger和surface。由于這部分基本是c++編寫的,我着重講原理。

隐藏在背後的surface

平時同學們都知道,我們的視圖需要被繪制。那麼它們被繪制到那了呢?也許很多童鞋腦海裡立即浮現出一個詞:canvas。但是,~沒錯!就是繪制到了canvas上。那麼canvas又是怎麼來的呢?是的,它可以new出來的。但是前面提到過,我們window中的視圖樹都是被繪制到一個由surface提供的canvas上。忘了的童鞋面壁思過。

用兩張圖告訴你,為什麼你的App會卡頓?

canvas實際代表了一塊記憶體,用于儲存繪制出來的資料。在canvas的構造器中你可以看到:

可以看到,canvas實際主要就是持有了一塊用于繪制的記憶體塊的索引long mnativecanvaswrapper。每次繪制時就通過這個索引找到對應的記憶體塊,然後将資料繪制到記憶體中。比如:

簡單的說一下。android繪制圖形是通過圖形庫skia(主要針對2d)或opengl(主要針對3d)進行。圖形庫是個什麼概念?就好比你在pc上用畫闆畫圖,此時畫闆就相當于android中的圖形庫,它提供了一系列标準化的工具供我們畫圖使用。比如我們drawrect()實際就是操作圖形庫在記憶體上寫入了一個矩形的資料。

扯多了,我們繼續回到surface上。當viewrootimpl執行到draw()方法(即開始繪制圖形資料了),會根據是否開啟了硬體(從android 4.0開始預設是開啟的)加速來決定是使用cpu軟繪制還是使用gpu硬繪制。如果使用軟繪制,圖形資料會繪制在surface預設的compatiblecanvas上(和普通canvas的唯一差別就是對matrix進行了處理,提高在不同裝置上的相容性)。如果使用了硬繪制,圖形資料會被繪制在displaylistcanvas上。displaylistcanvas會通過gpu使用opengl圖形庫進行繪制,是以具有更高的效率。

前面也簡單說了一下,每一個window都會有一個自己的surface,也就是說一個應用程式中會存在多個surface。通過上面的講解,童鞋們也都知道了surface的作用就是管理用于繪制視圖樹的canvas的。這個surface是和surfaceflinger共享,從它實作了parcelable接口也可以才想到它會被序列化傳遞。事實上,surface中的繪制資料是通過匿名共享記憶體的方式和surfaceflinger共享的,這樣surfaceflinger可以根據不同的surface,找到它所對應的記憶體區域中的繪制資料,然後進行合成。

合成師surfaceflinger

surfaceflinger是系統的一個服務。前面也一直在提到它專門負責把每個surface中的内容合成緩存,以待顯示到螢幕上。surfaceflinger在合成surface時是根據surface的z-order順序一層一層進行。比如一個dialog的surface就會在activity的surface上面。然後這個東西不多提了。

通過對android繪制機制的了解,我們知道造成應用卡頓的根源就在于16ms内不能完成繪制渲染合成過程,因為android平台的硬體重新整理率為60hz,大概就是16ms重新整理一次。如果沒能在16ms内完成這個過程,就會使螢幕重複顯示上一幀的内容,即造成了卡頓。在這16ms内,需要完成視圖樹的所有測量、布局、繪制渲染及合成。而我們的優化工作主要就是針對這個過程的。

複雜的視圖樹

頻繁的requestlayout()

如果頻繁的觸發requestlayout(),就可能會導緻在一幀的周期内,頻繁的發生布局計算,這也會導緻整個traversal過程變長。有的viewgroup類型的控件,比如relativelayout,在一幀的周期内會通過兩次layout()操作來計算确認子view的位置,這種少量的操作并不會引起能夠被注意到的性能問題。但是如果在一幀的周期内頻繁的發生layout()計算,就會導緻嚴重的性能,每次計算都是要消耗時間的!而requestlayout()操作,會向viewrootimpl中一個名為mlayoutrequesters的list集合裡添加需要重新layout的view,這些view将在下一幀中全部重新layout()一遍。通常在一個控件加載之後,如果沒什麼變化的話,它不會在每次的重新整理中都重新layout()一次,因為這是一個費時的計算過程。是以,如果每一幀都有許多view需要進行layout()操作,可想而知你的界面将會卡到爆!卡到爆!需要注意,setlayoutparams()最終也會調用requestlayout(),是以也不能爛用!同學們在寫代碼的過程中一定要謹慎注意那些可能引起requestlayout()的地方啊!

ui線程被阻塞

如果ui線程受到阻塞,顯而易見的是,我們的traversal過程也将受阻塞!畫面卡頓是妥妥的發生啊。這就是為什麼大家一直在強調不要在ui線程做耗時操作的原因。通常ui線程的阻塞和以下原因脫不了關系。

在ui線程中進行io讀寫資料的操作。這是一個很費時的過程好嗎?千萬别這麼幹。如果不想獲得一個卡到爆的app的話,把io操作統統放到子線程中去。

在ui線程中進行複雜的運算操作。運算本身是一個耗時的操作,當然簡單的運算幾乎瞬間完成,是以不會讓你感受到它在耗時。但是對于十分複雜的運算,對時間的消耗是十分辣眼睛的!如果不想獲得一個卡到爆的app的話,把複雜的運算操作放到子線程中去。

在ui線程中進行複雜的資料處理。我說的是比如資料的加密、解密、編碼等等。這些操作都需要進行複雜運算,特别是在資料比較複雜的時候。如果不想獲得一個卡到爆的app的話,把複雜資料的處理工作放到子線程中去。

故意阻塞ui線程。好吧,相信沒人會這麼幹吧。比如sleep()一下?

用兩張圖告訴你,為什麼你的App會卡頓?

整篇下來,相信童鞋對android的繪制機制也有了一個比較全面的了解。現在回過頭來再寫代碼時是不是有種知根知底的自信呢?

看到這裡的童鞋快獎勵自己一口辣條吧!

繼續閱讀