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