Android的View繪製機制

引言

在以前的Android佈局窗口繪製分析一篇文章中,咱們介紹過如何將佈局加載到PhoneWindows窗口中並顯示。而在Android的inflate源詳解中,咱們則分析瞭如何將xml的佈局文件轉化爲View樹。可是View樹具體以何種位置、何種大小展示給咱們,沒有具體講解的。那麼這篇文章,咱們就在上兩章的基礎上繼續研究View是如何進行佈局和繪製的。java

還記得咱們在【Android佈局窗口繪製分析】一文中的最後的addView代碼塊中重點標註的requestLayout()方法麼?android

不記得了也不要緊,我把代碼貼出來就是了~git

//ViewRootImpl.java
   public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                ...
                // 這裏調用異步刷新請求,最終會調用performTraversals方法來完成View的繪製
                //重點方法  這裏面會進行view的 測量,layout,以及繪製工做
                requestLayout();
                ...
        }
    }
複製代碼

這句代碼就是View的繪製的入口,通過measure,layout,draw最終將咱們在【Android的inflate源詳解】中所造成的View樹繪製出來。當這篇文章完成以後,安卓如何從xml到view樹,而後將view樹進行繪製,而後將view添加到DecterView並顯示出來,這一整套流程就能夠結束了。github

基礎知識

Android View的繪製過程分爲3步: 測量、佈局、繪製canvas

源碼

//ViewRootImpl.java 
    public void requestLayout() {
    	//該boolean變量會在ViewRootImpl.performLayout()開始時置爲ture,結束置false
        //表示當前不處於Layout過程
        if (!mHandlingLayoutInLayoutRequest) {
			//檢測線程安全,只有建立這個view的線程才能操做這個線程(也就是主線程)。
            checkThread();
			//標記請求進行繪製
            mLayoutRequested = true;
            //進行調度繪製工做
            scheduleTraversals();
        }
    }
複製代碼

這段代碼主要就是一個檢測,若是當前正在進行layout,那麼就不處理。不然就進行調度繪製。數組

//ViewRootImpl.java 
void scheduleTraversals() {
    if (!mTraversalScheduled) {
		///表示在排好此次繪製請求前,再也不排其它的繪製請求
        mTraversalScheduled = true;
		//Handler 的同步屏障,攔截 Looper 對同步消息的獲取和分發,只能處理異步消息
		//也就是說,對View的繪製渲染操做優先處理
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
		//mChoreographer可以接收系統的時間脈衝,統一動畫、輸入和繪製時機,實現了按幀進行繪製的機制
		//這裏增長了一個事件回調的類型。在繪製時,會調用mTraversalRunnable方法
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
複製代碼

這個函數裏面,將咱們要執行的繪製工做交給 mChoreographer 來進行調度處理。這個對象的主要工做就是根據系統的時間脈衝,將輸入、動畫、繪製等工做按照幀進行切割繪製。這裏的入參的回調對象 mTraversalRunnable 就是當對應的幀的週期到來時執行的對象。咱們跟蹤看一下里面執行了什麼操做。緩存

//ViewRootImpl.java
	final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
			//移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
			//重點方法 執行繪製工做
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
複製代碼

因此最後執行的就是 doTraversal() 這個方法。而後裏面調用了 performTraversals() 。這個方法就是咱們最終要剖析的函數對象了,能夠說這個函數屬於 View繪製的核心代碼 。全部的測量、佈局、繪製的工做都是在這個函數裏面來調用執行的。安全

對於一個將近千行的代碼,咱們只能逐一拆分來進行解析了。bash

//ViewRootImpl.java
		private void performTraversals() {
        //將mView緩存,用final修飾,避免運行過程當中修改
        final View host = mView;
		//若是沒有添加到DecorView,則直接返回
        if (host == null || !mAdded)
            return;
		//設置正在遍歷標誌位
        mIsInTraversal = true;
        //標記立刻就要進行View的繪製工做
        mWillDrawSoon = true;
        //視圖大小是否改變的標誌位
        boolean windowSizeMayChange = false;
        boolean surfaceChanged = false;
		//屬性
        WindowManager.LayoutParams lp = mWindowAttributes;
		//頂層Decor的寬高
        int desiredWindowWidth;
        int desiredWindowHeight;
		//頂層Decor是否可見
        final int viewVisibility = getHostVisibility();
		//視圖Decor的可見性改變了
        final boolean viewVisibilityChanged = !mFirst&& (mViewVisibility != viewVisibility || mNewSurfaceNeeded || mAppVisibilityChanged);
        mAppVisibilityChanged = false;
        final boolean viewUserVisibilityChanged = !mFirst &&((mViewVisibility == View.VISIBLE) != (viewVisibility == View.VISIBLE));
		...
        requestLayout = 0;
		//用來表示當前繪製的Activity窗口的寬高信息
        Rect frame = mWinFrame;
		//在構造方法中,設置爲了true,表示是不是第一次請求執行測量、佈局繪製工做
        if (mFirst) {
            //是否須要所有繪製
            mFullRedrawNeeded = true;
            //是否須要執行layout請求
            mLayoutRequested = true;
            final Configuration config = mContext.getResources().getConfiguration();
			//若是有狀態欄或者輸入框,那麼Activity窗口的寬度和高度刨除狀態欄
            if (shouldUseDisplaySize(lp)) {
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {
				//不然就是整個屏幕的寬高
                desiredWindowWidth = mWinFrame.width();
                desiredWindowHeight = mWinFrame.height();
            }
             /** * 由於第一次遍歷,View樹第一次顯示到窗口 * 而後對mAttachinfo進行一些賦值 * AttachInfo是View類中的靜態內部類AttachInfo類的對象 * 它主要儲存一組當View attach到它的父Window的時候視圖信息 */
            mAttachInfo.mUse32BitDrawingCache = true;
            mAttachInfo.mWindowVisibility = viewVisibility;
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mLastConfigurationFromResources.setTo(config);
            mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
            //設置佈局方向
            if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
                host.setLayoutDirection(config.getLayoutDirection());
            }
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
            dispatchApplyInsets(host);
        } else {//若是不是第一次請求執行測量、佈局的操做,直接使用frame的寬高信息便可,也就是上一次儲存的寬高
            desiredWindowWidth = frame.width();
            desiredWindowHeight = frame.height();
			//這個mWidth和mHeight也是用來描述Activity窗口當前寬度和高度的.它們的值是由應用程序進程上一次主動請求WindowManagerService服務計算獲得的,而且會一直保持不變到應用程序進程下一次再請求WindowManagerService服務來從新計算爲止
			//desiredWindowWidth和desiredWindowHeight表明着Activity窗口的當前寬度
            if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
				//mWidth此時表明的是上一次執行該方法的時候的frame.width()值,若是此時兩值不相等,那就說明視圖改變須要從新測量繪製了。
                if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
				//須要重新繪製標標誌位
                mFullRedrawNeeded = true;
				//須要執行layout標誌位
                mLayoutRequested = true;
				//標記窗口的寬高變化了
                windowSizeMayChange = true;
            }
        }
複製代碼

這段代碼的主要工做是爲了計算當前Activity的寬高。涉及的知識點相對來講仍是少一些的session

  1. 若是是第一次被請求繪製,會根據屏幕信息來進行設置。若是窗口是有狀態欄的,那麼Activity的寬高就會從Decor中剔除狀態欄的高度,不然的話,就設置爲整個屏幕的寬高
  2. 若是不是第一次執行,那麼Activity的寬高是上一次測量、佈局繪製時保存的值。也就是frame成員變量中的寬高信息。
  3. frame中的mWidth和mHeight是由WMS計算獲得的一個值,一直會保留到下一個WMS計算纔會改變。而 desiredWindowWidthdesiredWindowHeight 則是當前Activity的寬高。若是兩者不一樣,說明窗口發生了變化,這時候就須要將 mLayoutRequestedwindowSizeMayChange 進行設置,表示在後面的處理中須要進行佈局的工做。
  4. 這裏有一個 mAttachInfo 對象的相關賦值處理。它是 View.AttachInfo 類,主要負責將當前的View視圖附加到其父窗口時的一系列信息。

咱們繼續看第二段代碼。

//ViewRootImpl.java
		// 若是窗口不可見了,去掉可訪問性焦點
        if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
            host.clearAccessibilityFocus();
        }
        //執行HandlerActionQueue中HandlerAction數組保存的Runnable
        getRunQueue().executeActions(mAttachInfo.mHandler);
        boolean insetsChanged = false;
		//是否須要從新執行layout(執行了layoutRequest請求,而且當前頁面沒有中止)。
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        if (layoutRequested) {
			//獲取Resources
            final Resources res = mView.getContext().getResources();
            if (mFirst) {
                //指示View所處的Window是否處於觸摸模式
                mAttachInfo.mInTouchMode = !mAddedTouchMode;
				//確保這個Window的觸摸模式已經被設置
                ensureTouchModeLocally(mAddedTouchMode);
            } else {
				//判斷幾個insects是否發生了變化
                ...
				//若是當前窗口的根佈局是wrap,好比dialog,給它儘可能大的寬高,這裏會將屏幕的寬高賦值給它
                if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    windowSizeMayChange = true;
					//若是有狀態欄或者輸入框,那麼Activity窗口的寬度和高度刨除狀態欄
                    if (shouldUseDisplaySize(lp)) {
                        // NOTE -- system code, won't try to do compat mode.
                        Point size = new Point();
                        mDisplay.getRealSize(size);
                        desiredWindowWidth = size.x;
                        desiredWindowHeight = size.y;
                    } else {
						//獲取手機的配置信息
                        Configuration config = res.getConfiguration();
                        desiredWindowWidth = dipToPx(config.screenWidthDp);
                        desiredWindowHeight = dipToPx(config.screenHeightDp);
                    }
                }
            }
            //進行預測量窗口大小,以達到更好的顯示大小。好比dialog,若是設置了wrap,咱們會給出最大的寬高,可是若是隻是顯示一行字,顯示確定不會特別優雅,因此會使用
            //measureHierarchy來進行一下優化,儘可能展現出一個溫馨的UI效果出來
            windowSizeMayChange |= measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
        }
複製代碼

這段代碼主要是對一些視圖的可見性的處理等

  1. 設置焦點
  2. 設置觸摸模式
  3. 若是窗口的跟佈局使用了wrap,那麼會給儘可能大的寬高,而後使用 measureHierarchy() 方法進行從新處理。

這裏面的 measureHierarchy() 方法使咱們能夠研究的一個地方

//ViewRootImpl.java
	//測量層次結構
    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        //用於描述最終寬度的spec
        int childWidthMeasureSpec;
        //用於描述最終高度的spec
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
        boolean goodMeasure = false;
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //在大屏幕上,咱們不但願讓對話框僅僅拉伸來填充整個屏幕的寬度來顯示一行文本。首先嚐試在一個較小的尺寸佈局,看看它是否適合
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            //經過assertManager獲取config_prefDialogWidth設置的寬高,相關信息會保存在mTmpValue中
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                //獲取dim中設置的高度值
                baseSize = (int) mTmpValue.getDimension(packageMetrics);
            }
            if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize+ ", desiredWindowWidth=" + desiredWindowWidth);
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                //組合SPEC_MODE與SPEC_SIZE爲一個MeasureSpec
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //執行一次測量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight()
                        + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec)
                        + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
                //getMeasuredWidthAndState獲取繪製結果,若是繪製的結果不滿意會設置MEASURED_STATE_TOO_SMALL,
                if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
					//繪製滿意
                    goodMeasure = true;
                } else {
                	//繪製不滿意
                    //寬度從新設置一個平均值?
                    baseSize = (baseSize + desiredWindowWidth) / 2;
                    if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize="+ baseSize);
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
					//再次這行測量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                    if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
                        if (DEBUG_DIALOG) Log.v(mTag, "Good!");
                        goodMeasure = true;
                    }
                }
            }
        }

        if (!goodMeasure) {
			//若是通過兩次都不滿意,那麼就只能全屏顯示了
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
			//執行第三次測量
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }

        return windowSizeMayChange;
    }
複製代碼

能夠看到若是是Dialog設置的寬高都是wrap的狀況下,會首先使用系統的 com.android.internal.R.dimen.config_prefDialogWidth 的值來進行試探,若是合適就使用,若是不合適的話就是用這個值和視圖高度的平均值來進行試探,若是還不行,那就只能設置視圖的寬高了。能夠看到這裏面可能會屢次執行 performMeasure 方法,可是這三次的執行並不屬於View三大的繪製流程,僅僅只是爲了肯定Windows的大小而進行的輔助處理。

到如今爲止,一切的準備工做都作完了,那麼後面就是進入主題,進行頂層View樹的測量、佈局和繪製工做了。

測量

進行測量的條件:
//ViewRootImpl.java
//清除layoutRequested,這樣若是再有layoutRequested=true的狀況,咱們就能夠認爲是有了新的layout請求。
        if (layoutRequested) {
            mLayoutRequested = false;
        }
        //用來肯定窗口是否須要更改。
        //layoutRequested 爲true,說明view正在調用自身的requestLayout。
        // windowSizeMayChange:說明View樹所需大小與窗口大小不一致
        boolean windowShouldResize = layoutRequested && windowSizeMayChange
                //判斷上面測量後View樹的大小與窗口大小值是否相等
                && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
                //窗口的寬變化了。窗口設置的wrap。計算出來的窗口大小desiredWindowWidth 與上一次測量保存的frame.width()大,同時與WindowManagerService服務計算的寬度mWidth和高度mHeight也不一致,說明窗口大小變化了
                || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() != mWidth)
                || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() != mHeight));//窗口的高變化了。
        windowShouldResize |= mDragResizing && mResizeMode == RESIZE_MODE_FREEFORM;

        //若是activity進行了從新啓動,那麼經過wms強制進行resize
        windowShouldResize |= mActivityRelaunched;

        final boolean computesInternalInsets =mAttachInfo.mTreeObserver.hasComputeInternalInsetsListeners()|| mAttachInfo.mHasNonEmptyGivenInternalInsets;

        boolean insetsPending = false;
        int relayoutResult = 0;
        boolean updatedConfiguration = false;

        final int surfaceGenerationId = mSurface.getGenerationId();

        final boolean isViewVisible = viewVisibility == View.VISIBLE;
        final boolean windowRelayoutWasForced = mForceNextWindowRelayout;
        boolean surfaceSizeChanged = false;
		//這裏若是窗口個各類信息發生了變化,就須要進行測量工做
        if (mFirst || windowShouldResize || insetsChanged ||viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
複製代碼

有時候咱們的界面沒有必要進行測量工做,畢竟測量屬於一個比較耗時而又繁瑣的工做。因此對於測量工做的進行,是有必定的執行條件的,而上面的代碼就可以告訴咱們什麼狀況下才會進行整個頁面的測量工做。

  • mFirst爲true。表示窗口是第一次執行測量、佈局和繪製操做。
  • windowShouldResize標誌位爲true。而這個標誌位主要就是判斷窗口大小是否發生了變化。
  • insetsChanged爲true。這個表示這次窗口overscan等一些邊襯區域發生了改變
  • viewVisibilityChanged爲true。這個標誌位是View的可見性發生了變化
  • params說明窗口的屬性發生了變化。
  • mForceNextWindowRelayout爲true。表示設置了要強制了layout操做

當咱們肯定須要進行測量的話,下一步就是進行具體的測量工做了。

測量的執行:
//ViewRootImpl.java
				...
				//請求WMS計算Activity窗口大小及邊襯區域大小
                relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);

                if (!mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) {
                    if (DEBUG_CONFIGURATION) Log.v(mTag, "Visible with new config: "
                            + mPendingMergedConfiguration.getMergedConfiguration());
                    performConfigurationChange(mPendingMergedConfiguration, !mFirst,INVALID_DISPLAY /* same display */);
                    updatedConfiguration = true;
                }
            	//進行一些邊界的處理
                final boolean overscanInsetsChanged = !mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets);
                contentInsetsChanged = !mPendingContentInsets.equals(mAttachInfo.mContentInsets);
                final boolean visibleInsetsChanged = !mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets);
                final boolean stableInsetsChanged = !mPendingStableInsets.equals(mAttachInfo.mStableInsets);
                final boolean cutoutChanged = !mPendingDisplayCutout.equals(mAttachInfo.mDisplayCutout);
                final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
                surfaceSizeChanged = (relayoutResult& WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0;
                surfaceChanged |= surfaceSizeChanged;
                final boolean alwaysConsumeSystemBarsChanged =mPendingAlwaysConsumeSystemBars != mAttachInfo.mAlwaysConsumeSystemBars;
                final boolean colorModeChanged = hasColorModeChanged(lp.getColorMode());
                ...
            //從window session獲取最大size做爲當前窗口大小
            if (mWidth != frame.width() || mHeight != frame.height()) {
                mWidth = frame.width();
                mHeight = frame.height();
            }
			....
			//當前頁面處於非暫停狀態,或者接收到了繪製的請求
            if (!mStopped || mReportNextDraw) {
				//獲取焦點
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
				//寬高有變化了
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||updatedConfiguration) {
                    //得到view寬高的測量規格,lp.width和lp.height表示DecorView根佈局寬和高
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                    //執行測量工做
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    //須要從新測量標記
                    boolean measureAgain = false;
                    //lp.horizontalWeight表示將多少額外空間水平地(在水平方向上)分配給與這些LayoutParam關聯的視圖。若是
                    //視圖不該被拉伸,請指定0。不然,將在全部權重大於0的視圖中分配額外的像素。
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    //有變化了,就再次執行測量
                    if (measureAgain) {
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }
                    //設置請求layout標誌位
                    layoutRequested = true;
                }
            }
        } else {
            maybeHandleWindowMove(frame);
        }

複製代碼

在執行測量操做以前作了一系列的邊界處理。而後若是頁面是可見的,那麼就調用 performMeasure() 方法進行測量,當測量完成之後再根據是否設置了 weight 來肯定是否須要執行二次測量。這裏咱們去看一下 performMeasure() 函數執行。這裏的兩個參數是根據屏幕的寬度以及高度生成的 MeasureSpec。

//執行測量工做
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        try {
            //調用measure方法
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
複製代碼

這裏的mView是DecorView根佈局,記錄ViewRootImpl管理的View樹的根節點,也就是一個 ViewGroup 。而後調用了 measure() 方法。

//View.java
	//測量view使用的具體的寬高,參數是父類的寬高信息
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        //觀察spec是否發生了變化
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec|| heightMeasureSpec != mOldHeightMeasureSpec;
        //是不是固定寬高
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        //上次測量的高度和如今的最大高度相同
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        //是否須要layout
        final boolean needsLayout = specChanged&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        //若是須要繪製或者設置了強制layout
        if (forceLayout || needsLayout) {
            // 先清除測量尺寸標記
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //***重點方法 測量咱們本身
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            //若是開發者本身設置了onMeasure,可是裏面沒有調用setMeasuredDimension()方法,這時候就會報錯
            //setMeasuredDimension()方法會將PFLAG_MEASURED_DIMENSION_SET設置進mPrivateFlags
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "+ getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling"+ " setMeasuredDimension()");
            }
            //設置請求layout標誌位,爲了進行下一步的layout工做
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        //記錄父控件給予的MeasureSpec值,便於之後判斷父控件是否變化了
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        //緩存起來
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
複製代碼

能夠看到這個方法裏面並無進行任何的測量工做,真正的測量操做是交給了 onMeasure() 來進行處理。而這個函數的做用只是對於 onMeasure 方法的正確性進行檢測

  1. 若是上次傳過來的父類的寬高信息等各類狀況都沒發生變化,就不進行測量工做。
  2. 由於 onMeasure 方法是能夠被子類覆寫的,子類在進行覆寫的時候,必須調用 setMeasuredDimension() 方法,不然就會報錯
  3. 這裏有個緩存機制,若是不是強制執行測量工做,那麼能夠從緩存來獲取以前的測量信息。
//View.java
	//onMeasure方法的具體的實現應該是由子類去重寫的,提供更加合理、高效的實現
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //保存測量結果。
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製代碼

在View的實現中,onMeasure內部只是調用了 setMeasuredDimension 方法。

//View.java
	//onMeasue方法必須調用這個方法來進行測量數據的保存
    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);
    }
    
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        //保存測量的寬高信息
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        //向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此證實onMeasure()保存了測量結果
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
複製代碼

反過來咱們看一下 setMeasuredDimension 的入參是怎麼獲取的。也就是 getDefaultSizegetSuggestedMinimumWidth

//ViewRootImpl.java 
	//返回建議的視圖應該使用的最小寬度。
    protected int getSuggestedMinimumWidth() {
        //若是沒有背景,直接返回最小寬度,若是有背景,那麼使用mMinWidth和背景的最小寬度,兩者的最大值
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    //根據建議的size和當前控件的模式返回最終肯定的寬高信息
    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://未指明(wrap_content)的狀況下,使用建議的size值
            result = size;
            break;
        case MeasureSpec.AT_MOST://設置使用最大值(match_parent)
        case MeasureSpec.EXACTLY://設置了肯定的寬高信息(width="20dp")的狀況下,使用父類傳入的大小值
            result = specSize;
            break;
        }
        return result;
    }
複製代碼

因此這裏都計算一次控件的最小寬高值,而後根據父類傳入的measureSpec信息來進行不一樣的取值。

這裏面只是對View的測量工做進行了解析,其實在實際使用中,更多的是對ViewGroup的子類的測量,其實現更加複雜一些,這些留在之後進行處理,咱們這裏只是跟蹤View的繪製流程。

到目前爲止,整個的的測量工做完成了,咱們繼續回到主線,看一下當測量完成之後又作了哪些工做。

佈局

//ViewRootImpl.java
		//非暫停狀態或者請求繪製,並且設置了請求layout標誌位.
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout || mAttachInfo.mRecomputeGlobalAttributes;
        //須要進行layout佈局操做
        if (didLayout) {
            //重點方法****執行layout,內部會調用View的layout方法,從而調用onLayout方法來實現佈局
            performLayout(lp, mWidth, mHeight);
            //到如今爲止,全部的view已經進行過了測量和定位。能夠計算透明區域了
            if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
                ...
        }
        //觸發全局的layout監聽器,也就是咱們設置的mTreeObserver
        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }

複製代碼

對於控件的佈局操做,代碼量仍是比較少的。主要就是判斷是否須要進行layout操做,而後調用 performLayout 方法來進行佈局。這裏的 performLayout 會調用View的 layout() 方法,而後調用其 onLayout() 方法,具體分析與measure相似。因此這裏再也不進行分析了,有興趣的朋友能夠本身看一下。或者關注個人github中的源碼解析項目,裏面會不按期的更新對於源碼的註釋。

繪製

//ViewRootImpl.java
        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
        //沒有取消繪製
        if (!cancelDraw) {
            //存在動畫則執行動畫效果
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
			//重點方法 ***執行繪製工做
            performDraw();
        } else {//取消了繪製工做。
            if (isViewVisible) {
                // Try again
                //若是當前頁面是可見的,那麼從新進行調度
                scheduleTraversals();
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                //存在動畫效果則取消動畫
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }
        //清除正在遍歷標誌位
        mIsInTraversal = false;
    }
複製代碼

上面這些代碼實現了對於View的繪製工做。裏面的重點方法就是 performDraw()

對於繪製工做,不像測量和佈局那麼簡單,是須要交給 ThreadedRenderer 這個線程渲染器來進行渲染工做的,咱們跟蹤一下主要的流程

//ViewRootViewImpl 類
 private void performDraw() {
    ....
    draw(fullRedrawNeeded);
    ....
 }
-------------------------------------------------------------------------
//ViewRootViewImpl 類
private void draw(boolean fullRedrawNeeded) {
    ....
    mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
    ....
}
 
-------------------------------------------------------------------------
//ThreadedRenderer 類
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
    ....
    updateRootDisplayList(view, callbacks);
    ....
}
-------------------------------------------------------------------------
//ThreadedRenderer 類
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
    ....
    updateViewTreeDisplayList(view);
    ....
}
-------------------------------------------------------------------------
//ThreadedRenderer 類
private void updateViewTreeDisplayList(View view) {
    view.mPrivateFlags |= View.PFLAG_DRAWN;
    view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
            == View.PFLAG_INVALIDATED;
    view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
    //這裏調用了 View 的 updateDisplayListIfDirty 方法 
    //這個 View 其實就是 DecorView
    view.updateDisplayListIfDirty();
    view.mRecreateDisplayList = false;
}
複製代碼

能夠看到,最後會調用其參數view(也就是DecorView)的 updateDisplayListIfDirty 方法。

//View.java
 public RenderNode updateDisplayListIfDirty() {
    	...
        draw(canvas);
        ...
    }
複製代碼

在這個方法裏會調用View的draw(canvas)繪製方法,因爲DecorView方法重寫了draw方法,因此先執行DecorView的draw方法。

//DecorView
	@Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (mMenuBackground != null) {
            mMenubackground.draw(canvas);
        }
    }
複製代碼

因此最終仍是調用View類中的draw方法。

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        int saveCount;
        //步驟1  繪製背景
        drawBackground(canvas);
        //通常狀況下跳過步驟2和步驟5
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            //步驟3 繪製內容
            onDraw(canvas);
            //步驟4 繪製children
            dispatchDraw(canvas);

            //步驟6 繪製裝飾(前景,滾動條)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            //步驟7,繪製默認的焦點突出顯示
            drawDefaultFocusHighlight(canvas);


            // we're done... return; } 複製代碼

在繪製方法中,將繪製過程分爲了7個步驟,其中步驟2和步驟5通常狀況下是跳過的

咱們看一下里面的執行的步驟。具體的繪製流程再也不逐一分析了。

  1. 繪製背景。
  2. 若是有必要,保存畫布的圖層以備淡入
  3. 繪製視圖的內容
  4. 畫Children
  5. 若有必要,繪製淡入邊緣並恢復圖層
  6. 繪製裝飾(例如,滾動條)
  7. 繪製默認的焦點突出顯示

到這裏爲止,咱們的整個View的繪製流程就所有完成了。裏面具體的細節還有不少挖掘的地方。等之後有機會慢慢再分析吧。

總結

  1. handler是有一種同步屏障機制的,可以屏蔽同步消息(有什麼用圖之後再開發)。
  2. 對於屏幕的幀繪製是經過choreographer來進行的,它來進行屏幕的刷新,幀的丟棄等工做。
  3. 若是Dialog的寬高設置的wrap,會先用默認的高度試試是否可行,不可行就(屏幕高+默認高)/2來進行試驗,再不行就直接給屏幕高了。
  4. 對於測量工做,在整個過程當中會發生不少次。
  5. 在整個View的繪製過程當中,都有對於mTreeObserver的回調。這裏咱們能夠根據咱們的須要進行各類監聽工做。
  6. 自定義控件繼承View必須覆寫onMeasure方法。由於View默認的onMeasure中,若是使用了wrap,那麼會MeasureSpec爲AT+MOST。並且最大值爲父類的高度,也就是至關於match_parent。因此必須重寫onMeasure方法。這一點在TextView,Button等控件裏面都能看到。咱們只須要給自定義的View一個默認的內部寬高,當使用wrap_context的時候設置寬高便可。
  7. 只有onMeasure以後才能獲取到控件的寬高值,可是在實際中會存在屢次測量,onMeasure又是在onResume中調用。因此若是獲取寬高須要經過其餘途徑。這裏提供四種
    • onWindowFocusChanged中獲取
    • view.post中獲取
    • ViewTreeObserver監聽
    • 手動調用view.measure

本文由 開了肯 發佈!

同步公衆號[開了肯]

image-20200404120045271
相關文章
相關標籤/搜索