用兩張圖告訴你,為什麼你的App會卡頓?
有什麼料?
從這篇文章中你能獲得這些料:
- 知道setContentView()之後發生了什麼?
- 知道Android究竟是如何在螢幕上顯示我們期望的畫面的?
- 對Android的檢視架構有整體把握。
- 學會從根源處分析畫面卡頓的原因。
- 掌握如何編寫一個流暢的App的技巧。
- 從原始碼中學習Android的細想。
- 收穫兩張自制圖,幫助你理解Android的檢視架構。
從setContentView()說起
public class AnalyzeViewFrameworkActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_analyze_view_framwork);
}
}
上面這段程式碼想必Androider們大都已經不能再熟悉的更多了。但是你知道這樣寫了之後發生什麼了嗎?這個佈局到底被新增到哪了?我的天,知識點來了!
可能很多同學也知道這個佈局是被放到了一個叫做DecorView的父佈局裡,但是我還是要再說一遍。且看下圖✌️
這個圖可能和夥伴們在書上或者網上常見的不太一樣,為什麼不太一樣呢?因為是我自己畫的,哈哈哈…
下面就來看著圖捋一捋Android最基本的檢視框架。
PhoneWindow
估計很多同學都知道,每一個Activity都擁有一個Window物件的例項。這個例項實際是PhoneWindow型別的。那麼PhoneWindow從名字很容易看出,它應該是Window的兒子(即子類)!
知識點:每一個Activity都有一個PhoneWindow物件。
那麼,PhoneWindow有什麼用呢?它在Activity充當什麼角色呢?下面我就姑且把PhoneWindow等同於Window來稱呼吧。
Window從字面看它是一個視窗,意思和PC上的視窗概念有點像。但也不是那麼準確。看圖說。可以看到,我們要顯示的佈局是被放到它的屬性mDecor中的,這個mDecor就是DecorView的一個例項。下面會專門擼DecorView,現在先把關注點放到Window上。Window還有一個比較重要的屬性mWindowManager,它是WindowManager(這是個介面)的一個實現類的一個例項。我們平時通過getWindowManager()方法獲得的東西就是這個mWindowManager。顧名思義,它是Window的管理者,負責管理著視窗及其中顯示的內容。它的實際實現類是WindowManagerImpl。可能童鞋們現在正在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型別的。
}
通過上面的介紹,我們已經知道了Window中有負責承載佈局的DecorView,有負責管理的WindowManager(事實上它只是個代理,後面會講它代理的是誰)。
DecorView
前面提到過,在Activity的onCreate()中通過setContentView()設定的佈局實際是被放到DecorView中的。我們在圖中找到DecorView。
從圖中可以看到,DecorView繼承了FrameLayout,並且一般情況下,它會在先新增一個預設的佈局。比如DecorCaptionView,它是從上到下放置自己的子佈局的,相當於一個LinearLayout。通常它會有一個標題欄,然後有一個容納內容的mContentRoot,這個佈局的型別視情況而定。我們希望顯示的佈局就是放到了mContentRoot中。
知識點:通過setContentView()設定的佈局是被放到DecorView中,DecorView是檢視樹的最頂層。
WindowManager
前面已經提到過,WindowManager在Window中具有很重要的作用。我們先在圖中找到它。這裡需要先說明一點,在PhoneWindow中的mWindowManager實際是WindowManagerImpl型別的。WindowManagerImpl自然就是介面WindowManager的一個實現類嘍。這一點是我沒有在圖中反映的。
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);
//建立Window
...
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
//注意!這裡就是在建立WindowManager。
//這個方法在前面已經說過了。
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
}
繼續看圖。WindowManagerImpl持有了PhoneWindow的引用,因此它可以對PhoneWindow進行管理。同時它還持有一個非常重要的引用mGlobal。這個mGlobal指向一個WindowManagerGlobal型別的單例物件,這個單例每個應用程式只有唯一的一個。在圖中,我說明了WindowManagerGlobal維護了本應用程式內所有Window的DecorView,以及與每一個DecorView對應關聯的ViewRootImpl。這也就是為什麼我前面提到過,WindowManager只是一個代理,實際的管理功能是通過WindowManagerGlobal實現的。我們來看個原始碼的例子就比較清晰了。開始啦!
WimdowManagerImpl.java
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
...
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
//實際是通過WindowManagerGlobal實現的。
}
從上面的程式碼可以看出,WindowManagerImpl確實只是WindowManagerGlobal的一個代理而已。同時,上面這個方法在整個Android的檢視框架流程中十分的重要。我們知道,在Activity執行onResume()後介面就要開始渲染了。原因是在onResume()時,會呼叫WindowManager的addView()方法(實際最後呼叫的是WindowManagerGlobal的addView()方法),把檢視新增到視窗上。結合我的這篇【可能是史上最簡單的!一張圖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執行onResume()的時候就會新增檢視,或者重新整理檢視。需要解釋一點:WindowManager實現了ViewManager介面。
如圖中所說,WindowManagerGlobal呼叫addView()的時候會把DecorView新增到它維護的陣列中去,並且會建立另一個關鍵且極其重要的ViewRootImpl(這個必須要專門講一下)型別的物件,並且也會把它存到一個數組中維護。
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執行onResume()的時候才被建立的,並且此時才把DecorView傳進去讓它管理。
知識點:WindowManager是在onCreate()時被建立。它對視窗的管理能力實際是通過WindowManagerGlobal實現的。在onResume()是檢視才通過WindowManager被新增到視窗上。
ViewRootImpl
ViewRootImpl能夠和系統的WindowManagerService進行互動,並且管理著DecorView的繪製和視窗狀態。非常的重要。趕緊在圖中找到對應位置吧!
ViewRootImpl並不是一個View,而是負責管理檢視的。它配合系統來完成對一個Window內的檢視樹的管理。從圖中也可以看到,它持有了DecorView的引用,並且檢視樹它是檢視樹繪製的起點。因此,ViewRootImpl會稍微複雜一點,需要我們更深入的去了解,在圖中我標出了它比較重要的組成Surface和Choreographer等都會在後面提到。
到此,我們已經一起把第一張圖擼了一遍了,現在童鞋們因該對Android檢視框架有了大致的瞭解。下面將更進一步的去了解Android的繪製機制。
App總是卡頓到底是什麼原因?
下面將會詳細的講解為什麼我們設定的檢視能夠被繪製到螢幕上?這中間究竟隱藏著怎樣的離奇?看完之後,你自然就能夠從根源知道為什麼你的App會那麼卡,以及開始有思路著手解決這些卡頓。
同樣用一張圖來展示這個過程。由於Android繪製機制確實有點複雜,所以第一眼看到的時候你的內心中可能蹦騰了一萬隻草泥馬��。不要怕!我們從源頭開始,一點一點的梳理這個看似複雜的繪製機制。為什麼說看似複雜呢?因為這個過程只需要幾分鐘。Just Do It!
CPU、GPU是搞什麼鬼的?
整天聽到CPU、GPU的,你知道他們是幹什麼的嗎?這裡簡單的提一下,幫助理解後面的內容。
在Android的繪製架構中,CPU主要負責了檢視的測量、佈局、記錄、把內容計算成Polygons多邊形或者Texture紋理,而GPU主要負責把Polygons或者Textture進行Rasterization柵格化,這樣才能在螢幕上成像。在使用硬體加速後,GPU會分擔CPU的計算任務,而CPU會專注處理邏輯,這樣減輕CPU的負擔,使得整個系統效率更高。
RefreshRate重新整理率和FrameRate幀率
RefreshRate重新整理率是螢幕每秒重新整理的次數,是一個與硬體有關的固定值。在Android平臺上,這個值一般為60HZ,即螢幕每秒重新整理60次。
FrameRate幀率是每秒繪製的幀數。通常只要幀數和重新整理率保持一致,就能夠看到流暢的畫面。在Android平臺,我們應該儘量維持60FPS的幀率。但有時候由於檢視的複雜,它們可能就會出現不一致的情況。
如圖,當幀率小於重新整理率時,比如圖中的30FPS < 60HZ,就會出現相鄰兩幀看到的是同一個畫面,這就造成了卡頓。這就是為什麼我們總會說,要儘量保證一幀畫面能夠在16ms內繪製完成,就是為了和螢幕的重新整理率保持同步。
下面將會介紹Android是如何來確保重新整理率和幀率保持同步的。
Vsync(垂直同步)是什麼?
你可能在遊戲的設定中見過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偏移機制。
圖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的對應位置。
前面說過,ViewRootImpl控制著一個Window中的整個檢視樹的繪製。那它是如何進行控制的呢?一次繪製究竟是如何開始的呢?
在ViewRootImpl建立的時候,會獲取到前面提到過過的一個關鍵物件Choreographer。Choreographer在一個執行緒中僅存在一個例項,因此在UI執行緒只有一個Choreographer存在。也就說,通常情況下,它相當於一個應用中的單例。
在ViewRootImpl初始化時,會實現一個Choreographer.FrameCallback(這是一個Choreographer中的內部類),並向Choreographer中post。顧名思義,FrameCallback會在每次接收到Vsync訊號時被回撥。
Choreographer.java
public interface FrameCallback {
public void doFrame(long frameTimeNanos);
//一旦註冊到CallbackQueue中,那麼
//每次Choreographer接收到Vsync訊號時都會回撥。
}
FrameCallback一旦被註冊,那麼每次收到Vsync訊號時它都會被回撥。利用它,我們可以實現會幀率的監聽。
ViewRootImpl.java
//這個方法只有在ViewRootImpl初始化時才會被呼叫
private void profileRendering(boolean enabled) {
...
mRenderProfiler = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
...
scheduleTraversals();
//請求一個Vsync訊號,後面還會提到這個方法
mChoreographer.postFrameCallback(mRenderProfiler);
//每次回撥時,重新將FrameCallback post到Choreographer中
...
}
};
...
mChoreographer.postFrameCallback(mRenderProfiler);
//將FrameCallback post到Choreographer中
...
}
上面程式碼出現了一個重要方法scheduleTraversals()。下面我們看看它究竟為何重要。
ViewRootImpl.java
void scheduleTraversals() {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//向Choreographer中post一個TraversalRunnable
//這又是一個十分重要的物件
...
}
可以看出scheduleTraversals()每次呼叫時會向Choreographer中post一個TraversalRunnable,它會促使Choreographer去請求一個Vsync訊號。所以這個方法的作用就是用來請求一次Vsync訊號重新整理介面的。事實上,你可以看到,在invalidate()、requestLayout()等操作中,都能夠看到它被呼叫。原因就是這些操作需要重新整理介面,所以需要請求一個Vsync訊號來出發新介面的繪製。
ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
//開始遍歷檢視樹,這意味著開始繪製一幀內容了
}
}
從圖中可以看到,每當doTraversal()被呼叫時,一系列的測量、佈局和繪製操作就開始了。在繪製時,會通過Surface來獲取一個Canvas記憶體塊交給DecorView,用於檢視的繪製。整個View檢視的內容都是被繪製到這個Canvas中。
Choreographer中的風起雲湧
前面反覆提到向Choreographer中post回撥,那麼post過去發生了些什麼呢?從圖中可以看到,所有的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);
//如果回撥時間到了,請求一個Vsync訊號
//在接收到後會呼叫doFrame()回撥這個Callback。
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
//非同步訊息,避免被攔截器攔截
mHandler.sendMessageAtTime(msg, dueTime);
//如果還沒到回撥的時間,向FrameHandelr中傳送
//MSG_DO_SCHEDULE_CALLBACK訊息
}
}
...
}
上面這段程式碼會把post到Choreographer中的Callback新增到Callback[]中,並且當它因該被回撥時,請求一個Vsync訊號,在接收到下一個Vsync訊號時回撥這個Callback。如果沒有到回撥的時間,則向FrameHandler中傳送一個MSG_DO_SCHEDULE_CALLBACK訊息,但最終還是會請求一個Vsync訊號,然後回撥這個Callback。
簡單提一下CallbackQueue:簡單說一下CallbackQueue。它和MessageQueue差不多,都是單鏈表結構。在我的這篇【驚天祕密!從Thread開始,揭露Android執行緒通訊的詭計和主執行緒的陰謀http://www.jianshu.com/p/8862bd2b6a29】文章中,你能夠看到更多關於MessageQueue和Handler機制的內容。不同的是它同時還是一個一維陣列,下標表示Callback型別。事實上,算上每種型別的單鏈表結構,它更像是二維陣列的樣子。簡單點描述,假設有一個MessageQueue[]陣列,裡面存了幾個MessageQueue。來看看它的建立你可能就明白,它是在Choreographer初始化時建立的。
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();
}
}
現在來看看前面程式碼中呼叫的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傳送一個MSG_DO_SCHEDULE_VSYNC訊息
//來請求一個Vsync訊號
}
}
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
//通過DisplayEventReceiver請求一個Vsync訊號
//這是個恨角色,待會兒會聊聊它。
//MSG_DO_SCHEDULE_VSYNC訊息也是通過呼叫這個方法請求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開始回撥CallbackQueue[]中的Callback了
//也就是開始繪製下一幀的內容了
}
//這個方法是在父類中的,寫在這方便看
public void scheduleVsync() {
...
nativeScheduleVsync(mReceiverPtr);
//請求一個Vsync訊號
}
}
這給類功能比較明確,而且很重要!
上面一直在說向FrameHandler中發訊息,搞得神神祕祕的。接下來就來看看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:
//開始回撥Callback,以開始繪製下一幀內容
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訊號時會發送該種類型的訊息,然後開始回撥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的繪製過程中具有重要的作用,所以一定要結合圖梳理下這個流程。
SurfaceFlinger和Surface簡單說
在介紹Vsync的時候,我們可能已經看到了,現在Android系統會將HW_VSYNC虛擬化為兩個Vsync訊號。一個是VSYNC,被髮送給上面一直在講的Choreographer,用於觸發檢視樹的繪製渲染。另一個是SF_VSYNC,被髮送給我接下來要講的SurfaceFlinger,用於觸發Surface的合成,即各個Window視窗畫面的合成。接下來我們就簡單的看下SurfaceFlinger和Surface。由於這部分基本是c++編寫的,我著重講原理。
隱藏在背後的Surface
平時同學們都知道,我們的檢視需要被繪製。那麼它們被繪製到那了呢?也許很多童鞋腦海裡立即浮現出一個詞:Canvas。但是,~沒錯!就是繪製到了Canvas上。那麼Canvas又是怎麼來的呢?是的,它可以New出來的。但是前面提到過,我們Window中的檢視樹都是被繪製到一個由Surface提供的Canvas上。忘了的童鞋面壁思過��。
Canvas實際代表了一塊記憶體,用於儲存繪製出來的資料。在Canvas的構造器中你可以看到:
public Canvas() {
...
mNativeCanvasWrapper = initRaster(null);
//申請一塊記憶體,並且返回該記憶體的一個long型別的標記或者索引。
...
}
可以看到,Canvas實際主要就是持有了一塊用於繪製的記憶體塊的索引long mNativeCanvasWrapper
。每次繪製時就通過這個索引找到對應的記憶體塊,然後將資料繪製到記憶體中。比如:
public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {
native_drawRect(mNativeCanvasWrapper,
rect.left, rect.top, rect.right, rect.bottom, paint.getNativeInstance());
//在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上面。然後這個東西不多提了。
終於可以說說你的App為什麼這麼卡的原因了
通過對Android繪製機制的瞭解,我們知道造成應用卡頓的根源就在於16ms內不能完成繪製渲染合成過程,因為Android平臺的硬體重新整理率為60HZ,大概就是16ms重新整理一次。如果沒能在16ms內完成這個過程,就會使螢幕重複顯示上一幀的內容,即造成了卡頓。在這16ms內,需要完成檢視樹的所有測量、佈局、繪製渲染及合成。而我們的優化工作主要就是針對這個過程的。
複雜的檢視樹
如果檢視樹複雜,會使整個Traversal過程變長。因此,我們在開發過程中要控制檢視樹的複雜程度。減少不必要的層級巢狀。比如使用RelativeLayout可以減少複雜佈局的巢狀。比如使用【震驚!這個控制元件絕對值得收藏。輕鬆實現圓角、文字描邊、狀態指示等效果http://www.jianshu.com/p/cfe18cbc6924】��,這個控制元件可以減少既需要顯示文字,又需要圖片和特殊背景的需求的佈局複雜程度,所有的東西由一個控制元件實現。
頻繁的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的話,把複雜資料的處理工作放到子執行緒中去。
- 頻繁的發生GC,導致UI執行緒被頻繁中斷。在Java中,發生GC(垃圾回收)意味著Stop-The-World,就是說其它執行緒全部會被暫停啊。好可怕!正常的GC導致偶然的畫面卡頓是可以接受的,但是頻繁發生就讓人很蛋疼了!頻繁GC的罪魁禍首是記憶體抖動,這個時候就需要看下我的這篇【Android記憶體基礎——記憶體抖動http://www.jianshu.com/p/69e6f894c698】文章了。簡單的說就是在短時間內頻繁的建立大量物件,導致達到GC的閥值,然後GC就發生了。如果不想獲得一個卡到爆的App的話,把記憶體的管理做好,即使這是Java。
- 故意阻塞UI執行緒。好吧,相信沒人會這麼幹吧。比如sleep()一下?
總結
- 抽出空餘時間寫文章分享需要動力,還請各位看官動動小手點個贊,鼓勵下嘍��
- 我一直在不定期的創作新的乾貨,想要上車只需進到我的個人主頁點個關注就好了哦。發車嘍~
整篇下來,相信童鞋對Android的繪製機制也有了一個比較全面的瞭解。現在回過頭來再寫程式碼時是不是有種知根知底的自信呢?��
參考連結
看到這裡的童鞋快獎勵自己一口辣條吧!