Android 繪制原理淺析「干貨」

背景

對于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行代碼)肾请,大致流程是

  1. 判斷是否需要重新計算視圖大小,如果需要就執(zhí)行performMeasure()
  2. 是否需要重新安置所在的位置,performLayout()
  3. 是否需要重新繪制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其他平臺也是類似的).

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的同步信號,當屏幕刷新來時蕾域,開始屏幕的刷新操作拷肌。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市旨巷,隨后出現的幾起案子巨缘,更是在濱河造成了極大的恐慌,老刑警劉巖采呐,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件若锁,死亡現場離奇詭異,居然都是意外死亡斧吐,警方通過查閱死者的電腦和手機又固,發(fā)現死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來煤率,“玉大人仰冠,你說我怎么就攤上這事〉矗” “怎么了洋只?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我木张,道長众辨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任舷礼,我火速辦了婚禮鹃彻,結果婚禮上,老公的妹妹穿的比我還像新娘妻献。我一直安慰自己蛛株,他們只是感情好,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布育拨。 她就那樣靜靜地躺著谨履,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熬丧。 梳的紋絲不亂的頭發(fā)上笋粟,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機與錄音析蝴,去河邊找鬼害捕。 笑死,一個胖子當著我的面吹牛闷畸,可吹牛的內容都是我干的尝盼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼佑菩,長吁一口氣:“原來是場噩夢啊……” “哼盾沫!你這毒婦竟也來了?” 一聲冷哼從身側響起殿漠,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赴精,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后凸舵,有當地人在樹林里發(fā)現了一具尸體祖娘,經...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年啊奄,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掀潮。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡菇夸,死狀恐怖,靈堂內的尸體忽然破棺而出仪吧,到底是詐尸還是另有隱情庄新,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站择诈,受9級特大地震影響械蹋,放射性物質發(fā)生泄漏。R本人自食惡果不足惜羞芍,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一哗戈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧荷科,春花似錦唯咬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至刻获,卻和暖如春蜀涨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蝎毡。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工厚柳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人顶掉。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓草娜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親痒筒。 傳聞我的和親對象是個殘疾皇子宰闰,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內容