天天看點

Android 繪制原理淺析【幹貨】背景View 繪制工作原理Android的繪圖原理淺析文末

背景

對于Android開發,在面試的時候,經常會被問到,說一說View的繪制流程?我也經常問面試者,View的繪制流程.

對于3年以上的開發人員來說,就知道onMeasure/onLayout/onDraw基本,知道他們呢是幹些什麼的,這樣就夠了嗎?

如果你來我們公司,我是你的面試官,可能我會考察你這三年都幹了什麼,對于View你都知道些什麼,會問一些更細節的問題,比如LinearLayout的onMeasure,onLayout過程?他們都是什麼時候被發起的,執行順序是什麼?

如果以上問題你都知道,可能你進來我們公司就差不多了(如果需要内推,可以聯系我,Android/IOS 崗位都需要),可能我會考察你draw的 canvas是哪裡來的,他是怎麼被建立顯示到螢幕上呢?看看你的深度有多少?

對于現在的移動開發市場逐漸趨向成熟,趨向飽和,很多不缺人的公司,都需要進階程式員.在說大家也都知道,面試要造飛機大炮,進去後擰螺絲,對于一個3年或者5年以上Android開發不稍微了解一些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是何時被建立的呢?

//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中建立了PhoneWindow,PhoneWindow是Window的實作類.

繼續剛才的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,建立了一個DecorView.看mContentParent的注釋我們可以知道,他本身就是mDecor或者是mDecor的contents部分.

綜上,我們大概知道了三者的關系,

  • Activity包含了一個PhoneWindow,
  • PhoneWindow就是繼承于Window
  • Activity通過setContentView将View設定到了PhoneWindow上
  • PhoneWindow裡面包含了DecorView,最終布局被添加到Decorview上.

了解ViewRootImpl,WindowManager,WindowManagerService(WMS)之間的關系

看了上述三者的關系後,我們知道布局最終被添加到了DecorView上.那麼DecorView是怎麼被添加到系統的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(用戶端):用戶端需要建立一個視窗,而具體建立視窗的任務是由WMS完成,WindowManger就像一個部門經理,誰有什麼需求就告訴它,它和WMS互動,用戶端不能直接和WMS互動.
  • WindowManagerService(WMS)(服務端):負責視窗的建立,顯示等.

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中,執行doTraversal.并在doTraversal執行performTraversals(),是不是看到了我們熟悉的performTraversals()了?是的,在這裡才開始View的繪制工作.

在ViewRootImpl中的performTraversals(),這個方法代碼很長(大約800行代碼),大緻流程是

  1. 判斷是否需要重新計算視圖大小,如果需要就執行performMeasure()
  2. 是否需要重新安置所在的位置,performLayout()
  3. 是否需要重新繪制performDraw()

那麼是什麼導緻View的重繪呢?這裡總結了3個主要原因

  • 視圖本身内部狀态(enable,pressed等)變化,可能引起重繪
  • View内部添加或者删除了View
  • View本身的大小和可見性發生了變化

View的繪制流程

在上一小節了,講述了performTraversals()的是被WMS IPC調用執行的.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類型,保證整個流程的執行.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);
 }        
 
           

看代碼執行流程,1—>2->3, 最終拿到了Java層的canvas,然後進行一系列繪制操作.而canvas是通過Suface.lockCanvas()得到的.

那麼Surface又是一個什麼呢?在這裡Surface隻是一個抽象,在APP建立視窗時,會調用WindowManager向WMS服務發起一個請求,攜帶上surface對象,隻有他被配置設定完一段螢幕緩沖區才能真正對應螢幕上的一個視窗.

來看一下Framework中的繪圖架構.更好的了解Surface

Android 繪制原理淺析【幹貨】背景View 繪制工作原理Android的繪圖原理淺析文末

Surface本質上僅僅代表了一個平面,繪制不同圖案顯然是一種操作,而不是一段資料,Android使用了Skia繪圖驅動庫來進行平面上的繪制,在程式中使用canvas來表示這個功能.

雙緩沖技術的介紹

在ViewRootImpl中,我們看到接收到繪制消息後,不是立刻繪制而是調用scheduleTraversals,在scheduleTraversals調用Choreographer.postCallback(),這又是因為什麼呢?這其實涉及到螢幕繪制原理(除了Android其他平台也是類似的).

Android 繪制原理淺析【幹貨】背景View 繪制工作原理Android的繪圖原理淺析文末

我們都知道顯示器以固定的頻率重新整理,比如 iPhone的 60Hz、iPad Pro的 120Hz。當一幀圖像繪制完畢後準備繪制下一幀時,顯示器會發出一個垂直同步信号(VSync),是以 60Hz的螢幕就會一秒内發出 60次這樣的信号。

并且一般地來說,計算機系統中,CPU、GPU和顯示器以一種特定的方式協作:CPU将計算好的顯示内容送出給 GPU,GPU渲染後放入幀緩沖區,然後視訊控制器按照 VSync信号從幀緩沖區取幀資料傳遞給顯示器顯示.

但是如果螢幕的緩沖區隻有一塊,那麼這個VSync同步信号發出時,

開始重新整理螢幕,那麼你看到的螢幕就是一條一條的資料在變化.為了讓螢幕看上去是一幀一幀的資料,一般都有兩塊緩沖區(也被成為雙緩沖區).當資料要重新整理時,直接替換另一個緩沖區的資料.

雙緩沖技術裡面,如果不能特定時間重新整理完的話(如果60HZ的話,就是16ms内)把這個緩沖區資料重新整理完成,螢幕發出VSync同步信号,無法完成兩個緩沖區的切換,那麼就會造成卡頓現象.

回到scheduleTraversals()上,這個地方就是使用了雙緩沖技術(或者三緩沖技術),Choreographer接收VSync的同步信号,當螢幕重新整理來時,開始螢幕的重新整理操作.

文末

Android的繪制原理淺析,介紹完了,以上内容可能有不對的地方,希望各路大神指教.

(如果你正在看Flutter)推薦閱讀

Flutter高性能原理

其他相關文章

  • Flutter PIP(畫中畫)效果的實作
  • Flutter 側滑欄及城市選擇UI的實作
  • 如何優雅的處理Android 的OnActivityResult,RequestPermissions

參考資料

《Android 核心剖析》