源碼分析:View的工作流程

簡介

View的工作流程主要是指measure婿斥、layout民宿、draw這三大流程。其中measure確定View的測量寬高活鹰,layout確定View的最終寬高和四個頂點的位置志群,draw則將View繪制到屏幕上。

View的工作流程入口

在開始三大流程之前荠医,還有一些其他工作,例如將DecorView加載到Window中豫喧。并且三大流程的開始是通過ViewRootImpl來調(diào)用的幢泼。

DecorView被加載到Window中

當(dāng)在Activity的onCreate中調(diào)用setContentView方法時,將會創(chuàng)建DecorView孵班。當(dāng)DecorView創(chuàng)建完畢后招驴,要加載到Window中别厘。這一過程需要從Activity的創(chuàng)建過程說起,先看ActivityThread的handleLaunchActivity方法:

ActivityThread#handleLaunchActivity

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        //...

        Activity a = performLaunchActivity(r, customIntent);   //該方法會調(diào)用onCreate   

        if (a != null) {
            //...
            
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason); 

        } else {
            //...
        }
    }

(1)處的performLaunchActivity將會調(diào)用onCreate方法氮发,從而完成DecorView的創(chuàng)建∪吲常現(xiàn)在看handleResumeActivity方法:

ActivityThread#handleResumeActivity

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        //...
        
        r = performResumeActivity(token, clearHide, reason);    //調(diào)用onResume

        if (r != null) {
            final Activity a = r.activity;

            //...
            
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();       //得到DecorView
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();

                //...
        
                if (a.mVisibleFromClient && !a.mWindowAdded) {
                    //由于WindowManager的實現(xiàn)類是WindowManagerImpl披蕉,所以實際
                    //調(diào)用的是WindowManagerImpl的addView方法
                    wm.addView(decor, l);   
                }

            } 
            
            //...
        }
    }

繼續(xù)看WindowManagerImpl的addView方法:

WindowManagerImpl#addView

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        //mGlobal是一個WindowManagerGlobal對象
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

繼續(xù)看WindowManagerGlobal的addView方法:

WindowManagerGlobal#addView

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        //...

        ViewRootImpl root;

        synchronized (mLock) {
            //...

            root = new ViewRootImpl(view.getContext(), display);    

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            try {
                root.setView(view, wparams, panelParentView);
            } 
            //...
        }
    }

通過ViewRootImpl的setView方法没讲,ViewRootImpl和DecorView建立聯(lián)系爬凑,并將DecorView加載到Window中。

小結(jié)

在Activity創(chuàng)建過程中贰谣,在onCreate中通過setContentView方法可以完成DecorView的創(chuàng)建吱抚。之后在調(diào)用完onResume后考廉,需要將DecorView加載到Window中。這個過程需要ViewRootImpl的幫助既绕,ViewRootImpl是連接WindowManager和DecorView的橋梁。通過ViewRootImpl的setView方法誓军,ViewRootImpl和DecorView建立聯(lián)系疲扎,并將DecorView加載到Window中椒丧。

開始View的工作流程

通過ViewRootImpl的performTraversals方法開始View的工作流程

ViewRootImpl#performTraversals

    private void performTraversals() {

        //...
    
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {

                    //調(diào)用DecorView的measure方法
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        measureAgain = true;
                    }

                    //需要重新measure
                    if (measureAgain) {
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                }                    

        //...
    
        if (didLayout) {
            //調(diào)用DecorView的layout方法
            performLayout(lp, mWidth, mHeight);

            //...
        }

        //...

        if (!cancelDraw && !newSurface) {
            //...

            //調(diào)用View的draw方法
            performDraw();
        } 
        
        //...
    }

可以看到壶熏,在performTraversals方法中,ViewRootImpl先后執(zhí)行了performMeasure溯职、performLayout和performDraw方法帽哑,這三個方法分別調(diào)用了頂級View的measure、layout和draw方法甚带。

measure過程

measure過程要分情況來看佳头,如果是一個View,那么通過measure方法就完成了測量碉输。如果是一個ViewGroup亭珍,那么除了完成自己的測量外肄梨,還要遍歷所有子元素并調(diào)用其measure方法。

View的measure過程

View#measure

View的measure過程由其measure方法開始:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //...

        if (forceLayout || needsLayout) {
            //...
            
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                onMeasure(widthMeasureSpec, heightMeasureSpec);
            }

            //...
        }

        //...
    }

可以看到侨赡,measure是一個final方法,意味著子類不能重寫該方法蓖宦。measure方法繼續(xù)調(diào)用onMeasure方法:

View#onMeasure

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

其中油猫,setMeasuredDimension方法設(shè)置View寬高的測量值情妖,getDefaultSize方法得到寬高的測量值。來看下getDefaultSize方法:

View#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;
    }

該方法根據(jù)傳入的MeasureSpec的SpecMode來確定共螺,AT_MOST和EXACTLY模式返回的值都是MeasureSpec的SpecSize情竹。所以一般來說SpecSize就是測量后的大小。

至于UNSPECIFIED模式雏蛮,一般是用于系統(tǒng)內(nèi)部的測量過程阱州。這種情況下寬高的測量值是由getSuggestedMinimumWidth和getSuggestedMinimumHeigh方法決定的苔货。兩個方法同理,這里只分析下getSuggestedMinimumWidth方法:

View#getSuggestedMinimumWidth

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

其中mMinWidth對應(yīng)于android:minWidth屬性指定的值姻灶,如果沒有指定該屬性诈茧,mMinWidth默認(rèn)為0敢会。mBackground.getMinimumWidth()返回的是背景Drawable的原始寬度。

所以該方法的邏輯是:如果沒有設(shè)置背景鸥昏,則返回minWidth屬性對應(yīng)的值互广。否則返回minWidth屬性對應(yīng)的值和背景Drawable的原始寬度中的較大值。

小結(jié)

View的measure過程從measure方法開始像樊,調(diào)用onMeasure方法確定測量寬高旅敷。測量寬高的確定取決于寬高的MeasureSpec。如果SpecMode為AT_MOST或EXACTIY涂滴,測量大小為SpecSize晴音。如果SpecMode為UNSPECIFIED锤躁,需要判斷有無背景,如果沒有設(shè)置背景郭计,測量大小為minWidth屬性對應(yīng)的值椒振;否則測量大小為minWidth屬性對應(yīng)的值和背景Drawable的原始寬度中的較大值。但是UNSPECIFIED模式一般是用于系統(tǒng)內(nèi)部的測量過程庐杨,所以我們平常使用的View的測量大小就是SpecSize夹供。

注意

直接繼承View的自定義控件需要重寫onMeasure()方法罩引,并設(shè)置wrap_content時的自身大小,否則在布局中使用wrap_content就相當(dāng)于使用match_parent揭蜒。

這是因為在當(dāng)View使用wrap_content是剔桨,他的specMode是AT_MOST,而且View的specSize是parentSize,既父容器的當(dāng)前剩余空間大小瑰谜,這與match_parent一致。

如何解決這個問題隐轩?需要重寫onMeasure方法职车,代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    }
}

我們只需要給View指定一個寬高(mWidth, mHeight)鹊杖,并在wrap_content時設(shè)置此寬高即可骂蓖。至于這個寬高如何確定,需要根據(jù)View的類型靈活確定登下。對于非wrap_content的情形庐船,我們?nèi)允褂孟到y(tǒng)的測量值。

ViewGroup的measure過程

ViewGroup#measureChildren

ViewGroup是一個抽象類揩瞪,它并沒有重寫onMeasure方法篓冲,而是交由各個實現(xiàn)類來重寫。但是它提供了一個measureChildren方法來測量每個子View嗤攻,實現(xiàn)如下:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

該方法遍歷各子View妇菱,并調(diào)用measureChild方法來測量單個子View

ViewGroup#measureChild

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

該方法先得到子View寬高的MeasureSpec暴区,然后調(diào)用子View的measure方法進行測量

小結(jié)

ViewGrop本身是一個抽象類仙粱,其內(nèi)部并沒有重寫View的onMeasure方法房交,但提供了一個measureChildren()方法來對每一個子元素進行measure。之所以沒有重寫onMeasure方法伐割,是因為ViewGroup的子類具有各種不同的布局特性候味,所以測量方式不同刃唤,這需要子類自己重寫onMeasure方法來定義測量規(guī)則。

注意

View的measure完成之后白群,通過getMeasuredWidth尚胞、getMeasuredHeight方法就可以獲得View的測量寬高。需要注意的是川抡,在某些極端情況下须尚,系統(tǒng)可能要多次measure才能獲得最終的測量寬高崖堤,這時在onMeasure方法獲得的測量寬高可能是不準(zhǔn)確的。一個比較好的習(xí)慣是在onLayout方法中去獲取View的測量寬高耐床。

layout過程

View#layout

由于ViewGroup也是調(diào)用父類View的layout方法密幔,所有先從View的layout方法看起:

    public void layout(int l, int t, int r, int b) {
        //...

        //初始化四個頂點的值
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        //如果視圖的大小和位置發(fā)生變化,調(diào)用onLayout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            //...
        }

        //...
    }

該方法的步驟是:先初始化四個頂點的值撩轰,確定View在其父容器中的位置胯甩。如果發(fā)現(xiàn)View的大小和位置發(fā)生了變化,會繼續(xù)調(diào)用onLayout方法確定子元素的位置堪嫂。

View和ViewGroup都沒有實現(xiàn)onLayout偎箫,而是交由具體的ViewGroup來實現(xiàn)。下面看下LinearLayout的onLayout方法:

LinearLayout#onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

只看豎向排列的情況皆串,調(diào)用layoutVertical方法

LinearLayout#layoutVertical

    void layoutVertical(int left, int top, int right, int bottom) {
        //...

        final int count = getVirtualChildCount();

        //...

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                //...
                
                //確定子元素的位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                        
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

該方法遍歷子元素淹办,并通過setChildFrame方法確定子元素的位置

LinearLayout#setChildFrame

    private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

可以看出,setChildFrame方法又是調(diào)用子元素的layout方法來確定子元素的位置

小結(jié)

View只需確定自己四個頂點的位置即可確定自己的位置恶复,而ViewGroup除了要確定自己的位置怜森,如果發(fā)現(xiàn)自己的大小和位置發(fā)生了變化,還要調(diào)用onLayout重新確定子元素的位置谤牡。而在確定子元素位置的時候副硅,又會調(diào)用其layout方法,直到所有的View都確定位置翅萤。

注意

在View的默認(rèn)實現(xiàn)中恐疲,View的測量寬高和最終寬高是相等的,只不過測量寬高形成于View的measure過程套么,而最終寬高形成于View的layout過程流纹。因此,一般情況下违诗,我們可以認(rèn)為View的測量寬高等于最終寬高漱凝,但是在某些特殊情況會導(dǎo)致兩者不一樣:

第一種情況如下:

    @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r + 100, b + 100);
    }

上面重寫了View的layout方法,將導(dǎo)致會View的最終寬高比測量寬高大100px诸迟,雖然這樣做會導(dǎo)致View顯示不正常并且也沒有實際意義茸炒。

另一種情況是在某些情況愕乎,View需要多次measure才能確定自己的測量寬高,那么可能前幾次得出的測量寬高和最終寬高不一致壁公,但最終的測量寬高還是和最終寬高相同感论。

draw過程

View#draw

由于ViewGroup并沒有重寫draw方法,所以只需看View的draw方法即可:

   public void draw(Canvas canvas) {
        //...

        //繪制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        //...
        if (!verticalEdges && !horizontalEdges) {
          
            //繪制自己
            if (!dirtyOpaque) onDraw(canvas);   //View的onDraw方法是一個空方法紊册,需要子類自己實現(xiàn)

            //繪制子元素
            dispatchDraw(canvas);   //在View中是一個空方法比肄,ViewGroup重寫了該方法

            //繪制裝飾(foreground, scrollbars)
            onDrawForeground(canvas);

            //...
        }

        //...
    }

可以看出,View的繪制過程步驟如下:

  1. 繪制背景:調(diào)用背景Drawable的draw方法
  2. 繪制自己:調(diào)用onDraw方法囊陡,這是一個空方法芳绩,需要子類自己實現(xiàn)
  3. 繪制子元素:調(diào)用dispatchDraw方法,該方法在View中是一個空方法撞反,ViewGroup重寫了該方法
  4. 繪制裝飾:調(diào)用onDrawForeground方法妥色,繪制foreground, scrollbars等

注意

View有一個特殊的方法setWillNotDraw:

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

從注釋可以看出,如果一個View不需要繪制任何內(nèi)容遏片,那么設(shè)置這個標(biāo)志位為true后嘹害,系統(tǒng)會進行相應(yīng)的優(yōu)化。默認(rèn)情況下吮便,View沒有啟用這個標(biāo)志位笔呀,但是ViewGroup會默認(rèn)啟用這個優(yōu)化標(biāo)志位

這個標(biāo)志位的意義是:當(dāng)我們自定義的控件繼承與ViewGroup并且自身不具備繪制功能(沒有重寫onDraw)時髓需,就可以開啟這個標(biāo)志位從而便于系統(tǒng)進行后續(xù)的優(yōu)化许师。相反,當(dāng)我們需要重寫ViewGroup的onDraw方法來繪制內(nèi)容時授账,就需要顯示地關(guān)閉這個標(biāo)志位枯跑。

參考

  • 《Android 開發(fā)藝術(shù)探索》
  • 《Android 進階之光》
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市白热,隨后出現(xiàn)的幾起案子敛助,更是在濱河造成了極大的恐慌,老刑警劉巖屋确,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纳击,死亡現(xiàn)場離奇詭異,居然都是意外死亡攻臀,警方通過查閱死者的電腦和手機焕数,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刨啸,“玉大人堡赔,你說我怎么就攤上這事∩枇” “怎么了善已?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵灼捂,是天一觀的道長。 經(jīng)常有香客問我换团,道長悉稠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任艘包,我火速辦了婚禮的猛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘想虎。我一直安慰自己卦尊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布磷醋。 她就那樣靜靜地躺著猫牡,像睡著了一般胡诗。 火紅的嫁衣襯著肌膚如雪邓线。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天煌恢,我揣著相機與錄音骇陈,去河邊找鬼。 笑死瑰抵,一個胖子當(dāng)著我的面吹牛你雌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播二汛,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼婿崭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肴颊?” 一聲冷哼從身側(cè)響起氓栈,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎婿着,沒想到半個月后授瘦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡竟宋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年提完,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丘侠。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡徒欣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜗字,到底是詐尸還是另有隱情打肝,我是刑警寧澤官研,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站闯睹,受9級特大地震影響戏羽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜楼吃,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一始花、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧孩锡,春花似錦酷宵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荣挨,卻和暖如春男韧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背默垄。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工此虑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人口锭。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓朦前,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鹃操。 傳聞我的和親對象是個殘疾皇子韭寸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容