Android UI繪製流程及原理

  • 2019 年 10 月 3 日
  • 筆記

一、繪製流程源碼路徑

1、Activity載入ViewRootImpl

ActivityThread.handleResumeActivity()  --> WindowManagerImpl.addView(decorView, layoutParams)  --> WindowManagerGlobal.addView()

2、ViewRootImpl啟動View樹的遍歷

ViewRootImpl.setView(decorView, layoutParams, parentView)  -->ViewRootImpl.requestLayout()  -->scheduleTraversals()  -->TraversalRunnable.run()  -->doTraversal()  -->performTraversals()(performMeasure、performLayout、performDraw)

二、View繪製流程

1、measure

(1)MeasureSpec是什麼?

重寫過onMeasure()方法都知道,測量需要用到MeasureSpec類獲取View的測量模式和大小,那麼這個類是怎樣存儲這兩個資訊呢?

留心觀察的話會發現,onMeasure方法的兩個參數實際是32位int類型數據,即:

00 000000 00000000 00000000 00000000

而其結構為 mode + size ,前2位為mode,而後30位為size。

==> getMode()方法(measureSpec --> mode):
private static final int MODE_SHIFT = 30;  // 0x3轉換為二進位即為:11  // 左移30位後:11000000 00000000 00000000 00000000  private static final int MODE_MASK  = 0x3 << MODE_SHIFT;    public static int getMode(int measureSpec) {      // 與MODE_MASK按位與運算後,即將低30位清零,結果為mode左移30位後的值      return (measureSpec & MODE_MASK);  }

getSize()方法同理。

==> makeMeasureSpec()方法(mode + size --> measureSpec):
public static int makeMeasureSpec(      @IntRange(from = 0,          to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,      @MeasureSpecMode int mode) {      if (sUseBrokenMakeMeasureSpec) {          return size + mode;      } else {          return (size & ~MODE_MASK) | (mode & MODE_MASK);      }  }

這裡解釋一下,按位或左側為size的高2位清零後的結果,右側為mode的低30位清零後的結果,兩者按位或運算的結果正好為高2位mode、低30位size,例:

01000000 00000000 00000000 00000000 |  00001000 00001011 11110101 10101101 =  01001000 00001011 11110101 10101101

二進位計算規則可參考:https://www.cnblogs.com/joahyau/p/6420619.html

==> 測量模式:
public static final int UNSPECIFIED = 0 << MODE_SHIFT;  public static final int EXACTLY     = 1 << MODE_SHIFT;  public static final int AT_MOST     = 2 << MODE_SHIFT;

UNSPECIFIED:父容器不對View作任何限制,系統內部使用。

EXACTLY:精確模式,父容器檢測出View大小,即為SpecSize;對應LayoutParams中的match_parent和指定大小的情況。

AT_MOST:最大模式,父容器指定可用大小,View的大小不能超出這個值;對應wrap_content。

(2)ViewGroup的測量流程

回到ViewRootImpl的performMeasure方法,這裡傳入的參數為頂層DecorView的測量規格,其測量方式為:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {      int measureSpec;      switch (rootDimension) {        case ViewGroup.LayoutParams.MATCH_PARENT:          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);          break;      case ViewGroup.LayoutParams.WRAP_CONTENT:          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);          break;      default:          measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);          break;      }      return measureSpec;  }

match_parent和具體數值大小為EXACTLY模式,wrap_content則為AT_MOST模式。

往下走,performMeasure方法中調用了DecorView的onMeasure方法,而DecorView繼承自FrameLayout,可以看到FL的onMeasure方法中調用了measureChildWithMargins方法,並傳入自身的測量規格:

protected void measureChildWithMargins(View child,          int parentWidthMeasureSpec, int widthUsed,          int parentHeightMeasureSpec, int heightUsed) {      final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,              mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin                      + widthUsed, lp.width);      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,              mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin                      + heightUsed, lp.height);        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  }

即測量子控制項的大小,測量規則詳情可看getChildMeasureSpec方法,總結如下:

childLayoutParamsparentSpecMode EXACTLY AT_MOST UNSPECIFIED
dp EXACTLY/childSize EXACTLY/childSize EXCATLY/childSize
match_parent EXACTLY/parentSize AT_MOST/parentSize UNSPECIFIED/0
wrap_content AT_MOST/parentSize AT_MOST/parentSize UNSPECIFIED/0

回到onMeasure方法,測完子控制項之後,ViewGroup會經過一些計算,得出自身大小:

// 加上padding  maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();  maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();    // 檢查是否小於最小寬度、最小高度  maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());  maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());    // 檢查Drawable的最小高度和寬度  final Drawable drawable = getForeground();  if (drawable != null) {      maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());      maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());  }    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),          resolveSizeAndState(maxHeight, heightMeasureSpec,                  childState << MEASURED_HEIGHT_STATE_SHIFT));

綜上,ViewGroup的測量需要先測量子View的大小,而後結合padding等屬性計算得出自身大小。

(3)View的測量流程
View.performMeasure()  -->onMeasure(int widthMeasureSpec, int heightMeasureSpec)  -->setMeasuredDimension(int measuredWidth, int measuredHeight)  -->setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)

可以看到setMeasuredDimensionRaw()方法:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {      // 存儲測量結果      mMeasuredWidth = measuredWidth;      mMeasuredHeight = measuredHeight;        // 設置測量完成的標誌位      mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  }

View不需要考慮子View的大小,根據內容測量得出自身大小即可。

另外,View中的onMeasure方法中調用到getDefaultSize方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {      setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),              getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  }    public static int getDefaultSize(int size, int measureSpec) {      int result = size;      int specMode = MeasureSpec.getMode(measureSpec);      int specSize = MeasureSpec.getSize(measureSpec);        switch (specMode) {      case MeasureSpec.UNSPECIFIED:          result = size;          break;      case MeasureSpec.AT_MOST:      case MeasureSpec.EXACTLY:          // 最終測量的結果都是父容器的大小          result = specSize;          break;      }      return result;  }

這裡看到精確模式和最大模式,最終測量的結果都是父容器的大小,即布局中的wrap_content、match_parent以及數值大小效果都一樣,這也就是自定義View一定要重寫onMeasure方法的原因。

2、layout

布局相對測量而言要簡單許多,從ViewRootImpl的performLayout方法出發,可以看到其中調用了DecorView的layout方法:

// 實則為DecorView的left, top, right, bottom四個資訊  host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

進入layout方法,發現l、t、r、b被傳遞到了setFrame方法中,並設置給了成員變數:

mLeft = left;  mTop = top;  mRight = right;  mBottom = bottom;

所以,布局實際為調用View的layout方法,設置自身的l、t、r、b值。另外,layout方法中往下走,可以看到調用了onLayout方法,進入後發現為空方法。因而查看FrameLayout的onLayout方法:

@Override  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {      layoutChildren(left, top, right, bottom, false /* no force left gravity */);  }    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {      final int count = getChildCount();        // 省略        for (int i = 0; i < count; i++) {          final View child = getChildAt(i);          if (child.getVisibility() != GONE) {              final LayoutParams lp = (LayoutParams) child.getLayoutParams();                // 省略                child.layout(childLeft, childTop, childLeft + width, childTop + height);          }      }  }

可以看到,進行一系列計算後,調用了child的layout方法,對子控制項進行布局,同時子控制項又會繼續往下對自己的子控制項布局,從而實現遍歷。

綜上,布局實際為調用layout方法設置View位置,ViewGroup則需要另外實現onLayout方法擺放子控制項。

3、draw

(1)繪製過程入口
ViewRootImpl.performDraw()  -->ViewRootImpl.draw()  -->ViewRootImpl.drawSoftware()  -->View.draw()
(2)繪製步驟

進入到View的draw方法中,可以看到以下一段注釋:

/*   * Draw traversal performs several drawing steps which must be executed   * in the appropriate order:   *   *      1. Draw the background   *      2. If necessary, save the canvas' layers to prepare for fading   *      3. Draw view's content   *      4. Draw children   *      5. If necessary, draw the fading edges and restore layers   *      6. Draw decorations (scrollbars for instance)   */

結合draw方法的源碼,繪製過程的關鍵步驟如下:

==> 繪製背景:drawBackground(canvas)

==> 繪製自己:onDraw(canvas)

==> 繪製子view:dispatchDraw(canvas)

==> 繪製滾動條、前景等裝飾:onDrawForeground(canvas)

Tags: