Android View繪製原理:繪製流程排程、測算等

浪淘沙xud發表於2018-12-13

本文主要關注View的測量、佈局、繪製三個步驟,討論這三個步驟的執行流程。本文暫不涉及View和Window之間的互動以及Window的管理。再論述完這三個步驟之後,文末以自定義TagGroup為例,講述如何自定義ViewGroup。

Android View繪製原理:繪製流程排程、測算等

View 樹的繪圖流程

View樹的繪圖流程是由核心類:ViewRootImpl 來處理的,ViewRootImpl作為整個控制元件樹的根部,它是控制元件樹正常運作的動力所在,控制元件的測量、佈局、繪製以及輸入事件的派發處理都由ViewRootImpl觸發。

核心成員變數

這裡我主要講幾個Handler:

ViewRootHandler

這是ViewRootImpl排程的核心,其處理的訊息事件主要有: MSG_INVALIDATE、MSG_INVALIDATE_RECT、MSG_RESIZED、MSG_DISPATCH_INPUT_EVENT、MSG_CHECK_FOCUS、MSG_DISPATCH_DRAG_EVENT、MSG_CLOSE_SYSTEM_DIALOGS、MSG_UPDATE_CONFIGURATION等

主要有以下幾類:View繪製相關、輸入焦點等使用者互動相關、系統通知相關。

有經驗的同學肯定遇到過這樣的場景:動態建立一個View之後,想要直接獲取measureWidth 和 measureHeight往往取不到,這個時候我們會通過view.postDelayed()方法去獲取。那麼,問題來了,為什麼這樣就能取到呢?

答案就在ViewRootImpl中的ViewRootHandler,view.post--> attachInfo.mHandler.post --> ViewRootImpl ViewRootHandler. 這個Handler保證了當你post的runable被執行到時,view早就測量好了。

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}
複製程式碼

Choreographer.FrameHandler

Choreographer這個類來控制同步處理輸入(Input)、動畫(Animation)、繪製(Draw)三個UI操作,這裡不得不提一下Choreographer.FrameHandler目的就在於ViewRootImpl中涉及到到的View繪製流程,是通過Choreographer.FrameHandler來進行排程的。具體的排程過程如下:

1、 ViewRootImpl.scheduleTraversals

這個方法會往Choreographer註冊型別為Choreographer.CALLBACK_TRAVERSAL的Callback。

// ViewRootImpl.scheduleTraversals 註冊callback
mChoreographer.postCallback(
        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
複製程式碼

2、 Choreographer.FrameHandler

Choreographer.FrameHandler原始碼如下,主要處理三個訊號: MSG_DO_FRAME:開始渲染下一幀的操作 MSG_DO_SCHEDULE_VSYNC:請求Vsync訊號 MSG_DO_SCHEDULE_CALLBACK:請求執行callback

對於這三個訊號,Choreographer是有一個排程過程的,最終callback的回撥執行都是落實到doFrame()方法上面的。

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:
                doFrame(System.nanoTime(), 0);
                break;
            case MSG_DO_SCHEDULE_VSYNC:
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK:
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}
複製程式碼

doFrame執行回撥有一個順序的,順序依次如下: Choreographer.CALLBACK_INPUT Choreographer.CALLBACK_ANIMATION Choreographer.CALLBACK_TRAVERSAL Choreographer.CALLBACK_COMMIT

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); 
mFrameInfo.markInputHandlingStart(); 
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); 
mFrameInfo.markAnimationsStart(); 
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); 
mFrameInfo.markPerformTraversalsStart(); 
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); 
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
複製程式碼

關於Choreographer,讀者可以參考下這篇文章,講的非常詳細:Android Choreographer 原始碼分析

如何動態去檢測APP卡頓

這裡簡單說一個小竅門,通過Choreographer.getInstance().postFrameCallback() 註冊回撥,並計算前後兩幀的時間差,我們可以測算出APP的掉幀數,從而動態檢測APP 卡頓。

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {

            long lastFrameTimeNanos = 0;
            long currentFrameTimeNanos = 0;

            @Override
            public void doFrame(long frameTimeNanos) {
                if (lastFrameTimeNanos == 0) {
                    lastFrameTimeNanos = frameTimeNanos;
                }
                currentFrameTimeNanos = frameTimeNanos;
                long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos - lastFrameTimeNanos, TimeUnit.NANOSECONDS);
                long droppedCount = 0;
                if (diffMs > 100) {
                    droppedCount = (int) (diffMs / 16.6);
                    String anrLog = collectAnrLog(applicationContext);
                    DjLog.e("Block occur, droppedCount: " + droppedCount + ", anrLog: " + anrLog);
                }
                lastFrameTimeNanos = frameTimeNanos;
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
複製程式碼

View樹流程控制:performTraversals

整個 View 樹的繪圖流程在ViewRoot.java類的performTraversals()函式展開,該函式所做 的工作可簡單概況為是否需要重新計算檢視大小(measure)、是否需要重新安置檢視的位置(layout)、以及是否需要重繪(draw),流程圖如下:

Android View繪製原理:繪製流程排程、測算等

更詳細的圖示如下:

View樹繪製過程.png

performTraversals 方法非常龐大,整個原始碼在800行左右,看起來會讓人吐血。這個方法主要的過程有四個:

預測量階段 這是進入performTraversals()方法後的第一個階段,它會對控制元件樹進行第一次測量。測量結果可以通過mView. getMeasuredWidth()/Height()獲得。在此階段中將會計算出控制元件樹為顯示其內容所需的尺寸,即期望的視窗尺寸。在這個階段中,View及其子類的onMeasure()方法將會沿著控制元件樹依次得到回撥。

佈局視窗階段 根據預測量的結果,通過IWindowSession.relayout()方法向WMS請求調整視窗的尺寸等屬性,這將引發WMS對視窗進行重新佈局,並將佈局結果返回給ViewRootImpl。

最終測量階段 預測量的結果是控制元件樹所期望的視窗尺寸。然而由於在WMS中影響視窗布局的因素很多(參考第4章),WMS不一定會將視窗準確地佈局為控制元件樹所要求的尺寸,而迫於WMS作為系統服務的強勢地位,控制元件樹不得不接受WMS的佈局結果。因此在這一階段,performTraversals()將以視窗的實際尺寸對控制元件進行最終測量。在這個階段中,View及其子類的onMeasure()方法將會沿著控制元件樹依次被回撥。

佈局控制元件樹階段 完成最終測量之後便可以對控制元件樹進行佈局了。測量確定的是控制元件的尺寸,而佈局則是確定控制元件的位置。在這個階段中,View及其子類的onLayout()方法將會被回撥。

繪製階段 這是performTraversals()的最終階段。確定了控制元件的位置與尺寸後,便可以對控制元件樹進行繪製了。在這個階段中,View及其子類的onDraw()方法將會被回撥。

那問題來了,這個方法什麼時候會被觸發,或者說Android系統什麼時候會對整個View樹進行一次全量的操作呢?從原始碼中,我們可以看到以下幾個核心的方法會觸發:

  1. requestLayout: 注意在View中也有同樣的一個requestLayout方法,view中的requestLayout方法呼叫的就是ViewRootImpl中的requestLayout,最終觸發View樹的繪製流程,即 measure-layout-draw;
  2. invalidate:同樣的View中也有一個invalidate方法,View中該方法的呼叫最終呼叫的也是ViewRootImpl中的方法。有經驗的同學肯定知道,invalidate只會觸發draw,不會觸發measure和 layout。具體的ViewRootImpl會通過變數mLayoutRequested控制是否要進行measure和layout,invalidate操作時這個變數為false
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void invalidate() {

   	...

    if (!mWillDrawSoon) {
       scheduleTraversals();
    }
}
複製程式碼

View 繪製流程函式呼叫鏈

Android View繪製原理:繪製流程排程、測算等

有幾點注意: • invalidate/postInvalidate 只會觸發 draw; • requestLayout,會觸發 measure、layout 和 draw 的過程; • 它們都是走的 scheduleTraversals -> performTraversals,用不同的標記位來進行區分; • resume 會觸發 invalidate; • dispatchDraw 是用來繪製 child 的,發生在自己的 onDraw 之後,child 的 draw 之前 Measure 和 Layout 的具體過程

Measure 和 Layout 的具體過程

Android View繪製原理:繪製流程排程、測算等

關於Measure過程,不得不詳細提一下MeasureSpec。MeasureSpec是一個複合整型變數(32bit),用於指導控制元件對自身進行測量,它有兩個分量:前兩位表示SPEC_MODE,後30位表示SPEC_SIZE。SPEC_MODE的取值取決於此控制元件的LayoutParams.width/height的設定,SPEC_SIZE則是父檢視給定的指導大小。

SPEC_MODE有三種模式,具體的計算如下:

MeasureSpec.UNSPECIFIED: 表示控制元件在進行測量時,可以無視SPEC_SIZE的值。控制元件可以是它所期望的任意尺寸。

MeasureSpec.EXACTLY: 表示子控制元件必須為SPEC_SIZE所制定的尺寸。當控制元件的LayoutParams.width/height為一確定值,或者是MATCH_PARENT時,對應的MeasureSpec引數會使用這個SPEC_MODE。

MeasureSpec.AT_MOST: 表示子控制元件可以是它所期望的尺寸,但是不得大於SPEC_SIZE。當控制元件的LayoutParams.width/height為WRAP_CONTENT時,對應的MeasureSpec引數會使用這個SPEC_MODE。

自定義一個TagGroup

講了這麼多,下面我們來實操一下。

需求:自定義一個TagGroup,用來顯示一系列標籤元素。要求標籤樣式完全可以自定義,標籤間距可在xml中指定,要有最多顯示多少行的控制,顯示不全時要展示“更多 ...”

樣式協定

在attrs.xml中協定樣式:

<declare-styleable name="DjTagGroup">
    <attr name="tag_horizontalSpacing" format="dimension" />
    <attr name="tag_verticalSpacing" format="dimension" />
    <attr name="max_row" format="integer"/>
</declare-styleable>
複製程式碼

協定介面,用來提供具體的標籤元素:

public interface TagViewHolder {
    View getView();
}
複製程式碼

自定義Measure過程

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int width = 0;
    int height = 0;

    int row = 0; // The row counter.
    int rowWidth = 0; // Calc the current row width.
    int rowMaxHeight = 0; // Calc the max tag height, in current row.

    if (moreTagHolder != null) {
        moreTagMeasureWidth = moreTagHolder.getView().getMeasuredWidth();
        moreTagMeasureHeight = moreTagHolder.getView().getMeasuredHeight();
    }

    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        final int childWidth = child.getMeasuredWidth();
        final int childHeight = child.getMeasuredHeight();

        if (child.getVisibility() != GONE) {
            // judge the max_row
            if (row + 1 >= maxRow && rowWidth + childWidth  > widthSize) {
                break;
            }
            rowWidth += childWidth;
            if (rowWidth > widthSize) { // Next line.
                rowWidth = childWidth; // The next row width.
                height += rowMaxHeight + verticalSpacing;
                rowMaxHeight = childHeight; // The next row max height.
                row++;
            } else { // This line.
                rowMaxHeight = Math.max(rowMaxHeight, childHeight);
            }
            rowWidth += horizontalSpacing;
        }
    }

    // Account for the last row height.
    height += rowMaxHeight;

    // Account for the padding too.
    height += getPaddingTop() + getPaddingBottom();

    // If the tags grouped in one row, set the width to wrap the tags.
    if (row == 0) {
        width = rowWidth;
        width += getPaddingLeft() + getPaddingRight();
    } else {// If the tags grouped exceed one line, set the width to match the parent.
        width = widthSize;
    }

    setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
            heightMode == MeasureSpec.EXACTLY ? heightSize : height);
}
複製程式碼

自定義layout過程

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int parentLeft = getPaddingLeft();
    final int parentRight = r - l - getPaddingRight();
    final int parentTop = getPaddingTop();
    final int parentBottom = b - t - getPaddingBottom();

    int childLeft = parentLeft;
    int childTop = parentTop;

    int row = 0;
    int rowMaxHeight = 0;

    boolean showMoreTag = false;

    final int count = getChildCount();
    int unTagCount = count;
    if (moreTagHolder != null) {
        unTagCount--;
    }
    for (int i = 0; i < unTagCount; i++) {
        final View child = getChildAt(i);
        final int width = child.getMeasuredWidth();
        final int height = child.getMeasuredHeight();

        if (child.getVisibility() != GONE) {
            if (row + 1 >= maxRow && childLeft + width + (horizontalSpacing + moreTagMeasureWidth)  > parentRight) {
                // 預留一個空位放置moreTag
                showMoreTag = true;
                break;
            }
            if (childLeft + width > parentRight) { // Next line
                childLeft = parentLeft;
                childTop += rowMaxHeight + verticalSpacing;
                rowMaxHeight = height;
                row++;
            } else {
                rowMaxHeight = Math.max(rowMaxHeight, height);
            }

            // this is point
            child.layout(childLeft, childTop, childLeft + width, childTop + height);

            childLeft += width + horizontalSpacing;
        }
    }

    if (showMoreTag) {
        final View child = getChildAt(count - 1);
        final int width = child.getMeasuredWidth();
        final int height = child.getMeasuredHeight();
        child.layout(childLeft, childTop, childLeft + width, childTop + height);
    }
}
複製程式碼

使用

在xml中直接引用

<com.xud.tag.DjTagGroup
    android:id="@+id/dj_tag_group"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    app:tag_horizontalSpacing="8dp"
    app:tag_verticalSpacing="8dp"
    app:max_row="4"/>
複製程式碼

定義自己的TagViewHolder

public class DjTagViewHolder implements DjTagGroup.TagViewHolder {

    public String content;

    public View rootView;

    public TextView tagView;

    public DjTagViewHolder(View itemView, String content) {
        this.rootView = itemView;
        tagView = itemView.findViewById(R.id.tag);
        tagView.setText(content);

        tagView.setOnClickListener(v -> Toast.makeText(context, "點選了:" + content, Toast.LENGTH_SHORT).show());
    }

    @Override
    public View getView() {
        return rootView;
    }
}
複製程式碼

往DjTagGroup直接設定tags

private void initDjTags() {
    String[] tags = TagGenarator.generate(10, 6);
    List<DjTagGroup.TagViewHolder> viewHolders = new ArrayList<>();
    for (String tag: tags) {
        DjTagViewHolder viewHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false),
                tag);
        viewHolders.add(viewHolder);
    }
    DjTagViewHolder moreHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false),
            "更多 ...");
    djTagGroup.setTags(viewHolders, moreHolder);
}
複製程式碼

實際的效果

Android View繪製原理:繪製流程排程、測算等

原始碼地址: Github: 自定義View輯錄DjCustomView

參考文章

Hencoder: 自定義View相關

相關文章