View工做流程-相關學習

學習筆記android

1、Android的UI層級繪製體系

Android中的Activity是做爲應用程序的載體存在的,它表明一個完整的用戶界面並提供了窗口進行視圖繪製。canvas

  • 在這裏,咱們這裏所說的視圖繪製,實質上就是在對View及其子類進行操做。而View做爲視圖控件的頂層父類,在本文中會對其進行詳細分析。咱們以Android的UI層級繪製體系爲切入點對View進行探究。

圖1 View的層級結構
圖1 View的層級結構

Android的UI層級繪製體系如圖1所示bash

繪製體系中作了這些事情
①當調用 Activity 的setContentView 方法後會調用PhoneWindow 類的setContentView方法(PhoneWindow是抽象類Windiw的實現類,Window用來描述Activity視圖最頂端的窗口的顯示內容和行爲動做)。
②PhoneWindow類的setContentView方法中最終會生成一個DecorView對象(DectorView是是PhoneWindow的內部類,繼承自FrameLayout)。
③DecorView容器中包含根佈局,根佈局中包含一個id爲content的FrameLayout佈局,Activity加載佈局的xml最後經過LayoutInflater將xml文件中的內容解析成View層級體系,最後填加到id爲content的FrameLayout佈局中。

至此,View最終就會顯示到手機屏幕上。app

2、View的視圖繪製流程剖析

一、DecorView被加載到Window中less

DecorView被加載到Window的過程當中,WindowManager起到了關鍵性的做用,最後交給ViewRootImpl作詳細處理,經過以下的局部ActivityThread的源碼分析這一點能夠獲得印證(在這裏我只展現核心源碼,詳細源碼能夠在代碼中查看)。ide

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
       ...
       //在這裏執行performResumeActivity的方法中會執行Activity的onResume()方法
        r = performResumeActivity(token, clearHide, reason);
       ...
       if (r.window == null && !a.mFinished && willBeVisible) {
          //PhoneWindow在這裏獲取到
          r.window = r.activity.getWindow();
          //DecorView在這裏獲取到
          View decor = r.window.getDecorView();
          decor.setVisibility(View.INVISIBLE);
          //獲取ViewManager對象,在這裏getWindowManager()實質上獲取的是ViewManager的子類對象WindowManager
          ViewManager wm = a.getWindowManager();
          ...
          if (r.mPreserveWindow) {
          ...
          //獲取ViewRootImpl對象
          ViewRootImpl impl = decor.getViewRootImpl();
           ...
          }
          if (a.mVisibleFromClient) {
              if (!a.mWindowAdded) {
                   a.mWindowAdded = true;
                   //在這裏WindowManager將DecorView添加到PhoneWindow中
                   wm.addView(decor, l);
                   } 
                   ...
          }
          ...
    }
複製代碼

WindowManager將DecorView添加到PhoneWindow中,即addView()方法執行時將視圖添加的動做交給了ViewRoot,ViewRoot做爲接口,其實現類ViewRootImpl具體實現了addView()方法,最後,視圖的具體繪製在performTraversals()中展開,以下圖2.1所示:函數

圖2.1 View繪製的代碼層級分析
圖2.1 View繪製的代碼層級分析

二、ViewRootImpl的performTraversals()方法完成具體的視圖繪製流程oop

在源碼中ViewRootImpl中視圖具體繪製的流程以下:源碼分析

private void performTraversals() {
        // cache mView since it is used so much below...
        //mView就是DecorView根佈局
        final View host = mView;
        //在Step3 成員變量mAdded賦值爲true,所以條件不成立
        if (host == null || !mAdded)
            return;
        //是否正在遍歷
        mIsInTraversal = true;
        //是否立刻繪製View
        mWillDrawSoon = true;
         ...
        //頂層視圖DecorView所須要窗口的寬度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;

         ...
        //在構造方法中mFirst已經設置爲true,表示是不是第一次繪製DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            //若是窗口的類型是有狀態欄的,那麼頂層視圖DecorView所須要窗口的寬度和高度就是除了狀態欄
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else {//不然頂層視圖DecorView所須要窗口的寬度和高度就是整個屏幕的寬高 DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } } ... //得到view寬高的測量規格,mWidth和mHeight表示窗口的寬高,lp.width//he和lp.height表示DecorView根佈局寬和高 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // Ask host how big it wants to be //執行測量操做 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... //執行佈局操做 performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... //執行繪製操做 performDraw(); } 複製代碼

該方法主要流程就體現了View繪製渲染的三個主要步驟,分別是測量,擺放,繪製三個階段。流程圖以下圖2.2所示:佈局

圖2.2 View的繪製流程
圖2.2 View的繪製流程

接下來,咱們對於 performMeasure()、performLayout()、 performDraw()完成具體拆解分析。實質上最後就須要定位到View的onMeasure()、onLayout()、onDraw()方法中。

3、MeasureSpec在View體系中的做用

一、MeasureSpec的做用

首先咱們從performMeasure()入手分析,在上面的內容中,咱們經過源碼能夠看到 performMeasure()方法中傳入了childWidthMeasureSpec、childHeightMeasureSpec兩個int類型的值,performMeasure方法的源碼以下所示:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}

複製代碼

這兩個值又傳遞到mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法中,其中measure方法的核心源碼以下:

boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            //根據原有寬高計算獲取不一樣模式下的具體寬高值
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        ...
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //在該方法中子控件完成具體的測量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
         ...
    }

複製代碼

到這裏咱們應該明確,childWidthMeasureSpec, childHeightMeasureSpec是MeasureSpec根據原有寬高計算獲取不一樣模式下的具體寬高值。

二、MeasureSpec剖析

MeasureSpec是View的內部類,內部封裝了View的規格尺寸,以及View的寬高信息。在Measure的流程中,系統會將View的LayoutParams根據父容器是施加的規則轉換爲MeasureSpec,而後在onMeasure()方法中具體肯定控件的寬高信息。源碼及分析以下所示:

public static class MeasureSpec {
        //int類型佔4個字節,其中高2位表示尺寸測量模式,低30位表示具體的寬高信息
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}
        //以下所示是MeasureSpec中的三種模式:UNSPECIFIED、EXACTLY、AT_MOST                  

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //根據尺寸測量模式跟寬高具體肯定控件的具體寬高
        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);
            }
        }

        /**
         * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
         * will automatically get a size of 0. Older apps expect this.
         *
         * @hide internal use only for compatibility with system widgets and older apps
         */
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        //獲取尺寸模式
        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        //獲取寬高信息
        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        //將控件的尺寸模式、寬高信息進行拆解查看,並對不一樣模式下的寬高信息進行不一樣的處理
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
         * Returns a String representation of the specified measure
         * specification.
         *
         * @param measureSpec the measure specification to convert to a String
         * @return a String with the following format: "MeasureSpec: MODE SIZE"
         */
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

複製代碼

MeasureSpec的常量中指定了兩種內容,一種爲尺寸模式,一種爲具體的寬高信息。其中高2位表示尺寸測量模式,低30位表示具體的寬高信息。

尺寸測量模式有以下三種:

尺寸測量模式的3種類型
①UNSPECIFIED:未指定模式,父容器不限制View的大小,通常用於系統內部的測量
②AT_MOST:最大模式,對應於在xml文件中指定控件大小爲wrap_content屬性,子View的最終大小是父View指定的大小值,而且子View的大小不能大於這個值
③EXACTLY :精確模式,對應於在xml文件中指定控件爲match_parent屬性或者是具體的數值,父容器測量出View所需的具體大小

對於每個View,都持有一個MeasureSpec,MeasureSpec保存了該View的尺寸測量模式以及具體的寬高信息,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影響。

4、View的Measure流程分析

一、View樹的Measure測量流程邏輯圖

二、View的Measure流程分析

那麼在上文3.1的分析中,咱們可以明確在measure方法中最後調用onMeasure()方法完成子View的具體測量,onMeasure()方法的源碼以下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製代碼

setMeasuredDimension()方法在onMeasure()中被調用,被用於存儲測繪的寬度、高度,而不這樣作的話會觸發測繪時的異常。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

複製代碼

在setMeasuredDimension()方法中傳入的是getDefaultSize(),接着分析getDefaultSize()中作了哪些操做:

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;
    }
複製代碼

經過上文對MeasureSpec的分析,在這裏咱們就能明確,getDefaultSize實質上就是根據測繪模式肯定子View的具體大小,而對於自定義View而言,子View的寬高信息不只由自身決定,若是它被包裹在ViewGroup中就須要具體測量獲得其精確值。

三、View的Measure過程當中遇到的問題以及解決方案

View 的measure過程和Activity的生命週期方法不是同步執行的,所以沒法保證Activity執行了onCreate、onStart、onResume時某個View已經測量完畢了。若是View尚未測量完畢,那麼得到的寬和高都是0。下面是3種解決該問題的方法:

①Activity/View的onWindowsChanged()方法

onWindowFocusChanged()方法表示 View 已經初始化完畢了,寬高已經準備好了,這個時候去獲取是沒問題的。這個方法會被調用屢次,當Activity繼續執行或者暫停執行的時候,這個方法都會被調用,代碼以下:

public void onWindowFocusChanged(boolean hasWindowFocus) {
         super.onWindowFocusChanged(hasWindowFocus);
       if(hasWindowFocus){
       int width=view.getMeasuredWidth();
       int height=view.getMeasuredHeight();
      }      
  }
複製代碼

②View.post(runnable)方法

經過post將一個 Runnable投遞到消息隊列的尾部,而後等待Looper調用此runnable的時候View也已經初始化好了

@Override  
protected void onStart() {  
    super.onStart();  
    view.post(new Runnable() {  
        @Override  
        public void run() {  
            int width=view.getMeasuredWidth();  
            int height=view.getMeasuredHeight();  
        }  
    });  
}
複製代碼

③ViewTreeObsever 使用 ViewTreeObserver 的衆多回調方法能夠完成這個功能,好比使用onGlobalLayoutListener 接口,當 View樹的狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout 方法將被回調。伴隨着View樹的變化,這個方法也會被屢次調用。

@Override  
  protected void onStart() {  
    super.onStart();  
    ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();  
    viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {  
        @Override  
        public void onGlobalLayout() {  
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);  
            int width=view.getMeasuredWidth();  
            int height=view.getMeasuredHeight();  
        }  
    });  
}
複製代碼

固然,在這裏你能夠經過setMeasuredDimension()方法對子View的具體寬高以及測量模式進行指定。

5、View的layout流程分析

一、View樹的layout擺放流程邏輯圖

二、View的layout流程分析

layout 的做用是ViewGroup來肯定子元素的位置,當 ViewGroup 的位置被肯定後,在layout中會調用onLayout ,在onLayout中會遍歷全部的子元素並調用子元素的 layout 方法。

在代碼中設置View的成員變量 mLeft,mTop,mRight,mBottom 的值,這幾個值是在屏幕上構成矩形區域的四個座標點,就是該View顯示的位置,不過這裏的具體位置都是相對與父視圖的位置而言,而 onLayout 方法則會肯定全部子元素位置,ViewGroup在onLayout函數中經過調用其children的layout函數來設置子視圖相對與父視圖中的位置,具體位置由函數 layout 的參數決定。下面咱們先看View的layout 方法(只展現關鍵性代碼)以下:

/*  
 *@param l view 左邊緣相對於父佈局左邊緣距離 
 *@param t view 上邊緣相對於父佈局上邊緣位置 
 *@param r view 右邊緣相對於父佈局左邊緣距離 
 *@param b view 下邊緣相對於父佈局上邊緣距離 
 */  
    public void layout(int l, int t, int r, int b) {
        ...
        //記錄 view 原始位置  
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //調用 setFrame 方法 設置新的 mLeft、mTop、mBottom、mRight 值,  
        //設置 View 自己四個頂點位置  
        //並返回 changed 用於判斷 view 佈局是否改變  
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //第二步,若是 view 位置改變那麼調用 onLayout 方法設置子 view 位置  
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //調用 onLayout  
            onLayout(changed, l, t, r, b);
            ...
        }
        ...
    }

複製代碼

6、View的draw流程分析

一、View樹的draw繪製流程邏輯圖

二、View的draw流程分析

在View的draw()方法的註釋中,說明了繪製流程中具體每一步的做用,源碼中對於draw()方法的註釋以下,咱們在這裏重點分析註釋中除第二、第5步外的其餘步驟。

/*
         * 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(繪製View的內容)
         *      4. Draw children(繪製子View)
         *      5. If necessary, draw the fading edges and restore layers(若是須要的話,繪製漸變邊緣並恢復畫布圖層。)
         *      6. Draw decorations (scrollbars for instance)(繪製裝飾(例如滾動條scrollbar))
         */

複製代碼

①View中的drawBackground()繪製背景

核心源碼以下:

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        ...
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

複製代碼

若是背景有偏移,實質上對畫布首先作偏移處理,而後在其上進行繪製。

②View內容的繪製

View內容的繪製源碼以下所示:

protected void onDraw(Canvas canvas) {
    }
複製代碼

該方法是空實現,就根據不一樣的內容進行不一樣的設置,自定義View中就須要重寫該方法加入咱們本身的業務邏輯。

③子View的繪製

子View的繪製源碼以下所示:

protected void dispatchDraw(Canvas canvas) {

    }
複製代碼

該方法一樣爲空實現,而對於ViewGroup而言對子View進行遍歷,並最終調用子View的onDraw方法進行繪製。

④裝飾繪製

裝飾繪製的源碼以下所示(只展現核心源碼):

public void onDrawForeground(Canvas canvas) {
        //繪製前景裝飾
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);
       ...
            foreground.draw(canvas);
    }

複製代碼

很明顯,在這裏onDrawForeground()方法用於繪製例如ScrollBar等其餘裝飾,並將它們顯示在視圖的最上層。

7、視圖重繪

一、requestLayout從新繪製視圖

子View調用requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會調用三大流程,從measure開始,對於每個含有標記位的view及其子View都會進行測量、佈局、繪製。

二、invalidate在UI線程中從新繪製視圖

當子View調用了invalidate方法後,會爲該View添加一個標記位,同時不斷向父容器請求刷新,父容器經過計算得出自身須要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程(只繪製須要重繪的視圖)。

三、postInvalidate在非UI線程中從新繪製視圖

這個方法與invalidate方法的做用是同樣的,都是使View樹重繪,但二者的使用條件不一樣,postInvalidate是在非UI線程中調用,invalidate則是在UI線程中調用。

  • 總結一下 通常來講,若是View肯定自身再也不適合當前區域,好比說它的LayoutParams發生了改變,須要父佈局對其進行從新測量、擺放、繪製這三個流程,每每使用requestLayout。而invalidate則是刷新當前View,使當前View進行重繪,不會進行測量、佈局流程,所以若是View只須要重繪而不須要測量,佈局的時候,使用invalidate方法每每比requestLayout方法更高效。

參考文章:連接:https://www.jianshu.com/p/af266ff378c6

相關文章
相關標籤/搜索