背景
對于Android開發(fā),在面試的時候,經常會被問到焕蹄,說一說View的繪制流程?我也經常問面試者阀溶,View的繪制流程.
對于3年以上的開發(fā)人員來說腻脏,就知道onMeasure/onLayout/onDraw基本鸦泳,知道他們呢是干些什么的,這樣就夠了嗎永品?
如果你來我們公司辽故,我是你的面試官,可能我會考察你這三年都干了什么,對于View你都知道些什么腐碱,會問一些更細節(jié)的問題誊垢,比如LinearLayout的onMeasure,onLayout過程?他們都是什么時候被發(fā)起的,執(zhí)行順序是什么?
如果以上問題你都知道,可能你進來我們公司就差不多了(如果需要內推症见,可以聯(lián)系我喂走,Android/IOS 崗位都需要),可能我會考察你draw的 canvas是哪里來的,他是怎么被創(chuàng)建顯示到屏幕上呢谋作?看看你的深度有多少芋肠?
對于現在的移動開發(fā)市場逐漸趨向成熟,趨向飽和遵蚜,很多不缺人的公司帖池,都需要高級程序員.在說大家也都知道,面試要造飛機大炮吭净,進去后擰螺絲,對于一個3年或者5年以上Android開發(fā)不稍微了解一些Android深一點的東西,不是很好混.扯了這么多沒用的東西睡汹,還是回到今天正題,Android的繪圖原理淺析.
本文介紹思路
從面試題中幾個比較容易問的問題寂殉,逐層深入囚巴,直至屏幕的繪圖原理.
在講Android的繪圖原理前,先介紹一下Android中View的基本工作原理友扰,本文暫不介紹事件的傳遞流程彤叉。
View 繪制工作原理
我們先理解幾個重要的類,也是在面試中經常問到的
Activity,Window(PhoneWindow),DecorView之間的關系
理解他們三者的關系村怪,我們直接看代碼吧秽浇,先從Activity開始的setContentView開始(注:代碼刪除了一些不是本次分析流程的代碼,以免篇幅過長)
//Activity
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
里面調用的getWindow的setContentView,這個接下來講,那么這個mWindow是何時被創(chuàng)建的呢?
//Activity
private Window mWindow;
final void attach(Context context, ActivityThread aThread,····) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}
在Activity的attach中創(chuàng)建了PhoneWindow,PhoneWindow是Window的實現類.
繼續(xù)剛才的setContentView
//PhoneWindow
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
}
在setContentView中甚负,如果mContentParent為空柬焕,會去調用installDecor,最后將布局infalte到mContentParent.在來看一下installDecor
//PhoneWindow
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;
private DecorView mDecor;
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
protected DecorView generateDecor(int featureId) {
return new DecorView(context, featureId, this, getAttributes());
}
在installDecor,創(chuàng)建了一個DecorView.看mContentParent的注釋我們可以知道,他本身就是mDecor或者是mDecor的contents部分.
綜上腊敲,我們大概知道了三者的關系击喂,
- Activity包含了一個PhoneWindow,
- PhoneWindow就是繼承于Window
- Activity通過setContentView將View設置到了PhoneWindow上
- PhoneWindow里面包含了DecorView碰辅,最終布局被添加到Decorview上.
理解ViewRootImpl,WindowManager,WindowManagerService(WMS)之間的關系
看了上述三者的關系后懂昂,我們知道布局最終被添加到了DecorView上.那么DecorView是怎么被添加到系統(tǒng)的Framework層.
當Activity準備好后,最終會調用到Activity中的makeVisible没宾,并通過WindowManager添加View,代碼如下
//Activity
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
那他們到底是什么關系呢凌彬? (下面提到到客戶端服務端是Binder通訊中的客戶端服務端概念. )
以下內容是重點需要理解的部分
- ViewRootImpl(客戶端):View中持有與WMS鏈接的mAttachInfo沸柔,mAttachInfo持有ViewRootImpl.ViewRootImpl是ViewRoot的的實現,WMS管理窗口時,需要通知客戶端進行某種操作铲敛,比如事件響應等.ViewRootImpl有個內部類W,W繼承IWindow.Stub褐澎,實則就是一個Binder,他用于和WMS IPC交互伐蒋。ViewRootHandler也是其內部類繼承Handler工三,用于與遠程IPC回來的數據進行異步調用.
- WindowManger(客戶端):客戶端需要創(chuàng)建一個窗口,而具體創(chuàng)建窗口的任務是由WMS完成,WindowManger就像一個部門經理先鱼,誰有什么需求就告訴它俭正,它和WMS交互,客戶端不能直接和WMS交互.
- WindowManagerService(WMS)(服務端):負責窗口的創(chuàng)建,顯示等.
View的重繪
從上述關系中焙畔,ViewRootImpl是用于接收WMS傳遞來的消息.那么我們來看一下ViewRootImpl里面的幾個關于View繪制的代碼.
在這里在強調一下,ViewRootImpl 兩個重要的內部類
- W類 繼承Binder 用于接收WMS 傳遞來的消息
- ViewRootHandler類繼承Handler 接收W類的異步消息
下面看一下ViewRootHandler類.(以View的setVisible為例.)
// ViewRootHandler(ViewRootImpl的內部類掸读,用于異步消息處理,和Acitivity的啟動很像)
//第一步 Handler接收W(Binder)傳遞來的消息
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
case MSG_INVALIDATE_RECT:
final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.recycle();
break;
case MSG_DISPATCH_APP_VISIBILITY://處理Visible
handleAppVisibility(msg.arg1 != 0);
break;
}
}
void handleAppVisibility(boolean visible) {
if (mAppVisible != visible) {
mAppVisible = visible;
scheduleTraversals();
if (!mAppVisible) {
WindowManagerGlobal.trimForeground();
}
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//開啟下次刷新宏多,就遍歷View樹
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
看一下mTraversalRunnable
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
}
}
在TraversalRunnable中儿惫,執(zhí)行doTraversal.并在doTraversal執(zhí)行performTraversals(),是不是看到了我們熟悉的performTraversals()了?是的,在這里才開始View的繪制工作.
在ViewRootImpl中的performTraversals()伸但,這個方法代碼很長(大約800行代碼)肾请,大致流程是
- 判斷是否需要重新計算視圖大小,如果需要就執(zhí)行performMeasure()
- 是否需要重新安置所在的位置,performLayout()
- 是否需要重新繪制performDraw()
那么是什么導致View的重繪呢?這里總結了3個主要原因
- 視圖本身內部狀態(tài)(enable砌烁,pressed等)變化,可能引起重繪
- View內部添加或者刪除了View
- View本身的大小和可見性發(fā)生了變化
View的繪制流程
在上一小節(jié)了,講述了performTraversals()的是被WMS IPC調用執(zhí)行的.View的繪制流程一般是
從performTraversals -> performMeasure() -> performLayout() -> performDraw().
下面看一下performMeasure()
//ViewRootImpl
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//在這里調用了onMeasure 方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
}
}
最終調用了View的measure方法筐喳,而View中的measure()方法被定義成final類型,保證整個流程的執(zhí)行.performLayout()和performDraw()也是類似的過程.
而對于程序員,自定義View只需要關注他提供出來幾個對應的方法,onMeasure/onLayout/onDraw. 關于這方面知識的網上介紹的資料很多函喉,也可以很容易的看到View及ViewGroup里面的代碼,推薦看LinerLayout的源碼理解這部分知識,在這里不詳細展開.
Android的繪圖原理淺析
Android屏幕繪制
關于繪制荣月,就要從performDraw()說起管呵,我們來看一下這個流程到底是怎么繪制的.
//ViewRootImpl
//1
private void performDraw() {
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
//2
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return;
}
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
//3
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
Canvas canvas = mSurface.lockCanvas(dirty);
}
看代碼執(zhí)行流程,1—>2->3, 最終拿到了Java層的canvas,然后進行一系列繪制操作.而canvas是通過Suface.lockCanvas()得到的.
那么Surface又是一個什么呢哺窄?在這里Surface只是一個抽象,在APP創(chuàng)建窗口時捐下,會調用WindowManager向WMS服務發(fā)起一個請求,攜帶上surface對象,只有他被分配完一段屏幕緩沖區(qū)才能真正對應屏幕上的一個窗口.
來看一下Framework中的繪圖架構.更好的理解Surface
Surface本質上僅僅代表了一個平面萌业,繪制不同圖案顯然是一種操作坷襟,而不是一段數據,Android使用了Skia繪圖驅動庫來進行平面上的繪制生年,在程序中使用canvas來表示這個功能.
雙緩沖技術的介紹
在ViewRootImpl中,我們看到接收到繪制消息后婴程,不是立刻繪制而是調用scheduleTraversals,在scheduleTraversals調用Choreographer.postCallback(),這又是因為什么呢抱婉?這其實涉及到屏幕繪制原理(除了Android其他平臺也是類似的).
我們都知道顯示器以固定的頻率刷新档叔,比如 iPhone的 60Hz桌粉、iPad Pro的 120Hz。當一幀圖像繪制完畢后準備繪制下一幀時衙四,顯示器會發(fā)出一個垂直同步信號(VSync)铃肯,所以 60Hz的屏幕就會一秒內發(fā)出 60次這樣的信號。
并且一般地來說传蹈,計算機系統(tǒng)中押逼,CPU、GPU和顯示器以一種特定的方式協(xié)作:CPU將計算好的顯示內容提交給 GPU惦界,GPU渲染后放入幀緩沖區(qū)宴胧,然后視頻控制器按照 VSync信號從幀緩沖區(qū)取幀數據傳遞給顯示器顯示.
但是如果屏幕的緩沖區(qū)只有一塊,那么這個VSync同步信號發(fā)出時表锻, 開始刷新屏幕恕齐,那么你看到的屏幕就是一條一條的數據在變化.為了讓屏幕看上去是一幀一幀的數據,一般都有兩塊緩沖區(qū)(也被成為雙緩沖區(qū)).當數據要刷新時瞬逊,直接替換另一個緩沖區(qū)的數據.
雙緩沖技術里面显歧,如果不能特定時間刷新完的話(如果60HZ的話,就是16ms內)把這個緩沖區(qū)數據刷新完成,屏幕發(fā)出VSync同步信號,無法完成兩個緩沖區(qū)的切換确镊,那么就會造成卡頓現象.
回到scheduleTraversals()上士骤,這個地方就是使用了雙緩沖技術(或者三緩沖技術),Choreographer接收VSync的同步信號,當屏幕刷新來時蕾域,開始屏幕的刷新操作拷肌。