Android View的工作流程分析学习

一、View工作的三个流程

View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制。

其中:

measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View或者ViewGroup的尺寸,并将这些尺寸保存起来。

layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。

draw:绘制。确定好位置后,就将这些控件绘制到屏幕上。

二、View的添加过程

在介绍View的添加过程之前我们应该了解一下Android视图层次结构以及DecorView,因为View的绘制流程的入口和DecorView有着密切的关系。

在这盗用别人一张图来展示Android视图层次结构:

我们平时看到的视图,其实存在着如上的嵌套关系。Android各个版本中的View视图层次略有出入,但整体没变。我们平时在Activity中setContentView(....)中对应的layout内容,对应的是上图中ViewGroup的树状结构,实际上添加到系统中时,会再裹上一层FrameLayout,就是上图中的浅蓝色部分。

了解完View视图的层次结构之后,我们开始介绍View是怎么添加的。

对Activity的启动过程有一定了解的同学,应该知道Activity启动过程会在ActivityThread.java类中完成的。在启动过程中会调用到此类的handleResumeActivity(......)方法,关于View视图的添加过程的起源就是从这个方法开始的。

1. handleResumeActivity()方法简单分析

@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
            

            ............      

            // 跟踪源码getWindow的返回值mWindow的初始赋值是在Activity类中
            // mWindow = PhoneWindow(this, window, activityConfigCallback);
            r.window = r.activity.getWindow();
            // 通过PhoneWindow获取DecoeView实例
            View decor = r.window.getDecorView();
            // 跟踪代码后发现,wm值为上述PhoneWindow实例中获取的WindowManager。
            ViewManager wm = a.getWindowManager();
            // View 视图添加过程的起源
            wm.addView(decor, l);
               
    }

该方法的大部分代码已经省略,仅保留了主要的流程代码。跟踪wm.addView(decoe,l)进入ViewManager,WindowManager是其子接口,WindowManagerImpl是WindowManager的实现类。所以,流程切换到了WindowManagerImpl类中的addView方法。代码如下:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

代码中调用了WindowManagerGlobal的addView方法,其中最后一个参数mParentWindow就是上面提到的Window,可以看出WindowManagerImpl虽然是WindowManager的实现类,但是没有实现什么功能,而是将功能实现委托给了WindowManagerGlobal,这里用到的是桥接模式。关于WindowManager相关的知识,之后我会重新写一篇文章专门介绍,在这儿不做过多陈述。

WindowManagerGlobal中addView方法代码如下:

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

        ........


        ViewRootImpl root;

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

            // do this last because it fires off messages to start doing things
            try {
                // 调用ViewRootImpl的setView方法
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

通过WindowManagerGlobal的addView方法,将流程切换到了ViewRootImpl的setView方法。

ViewRootImpl的setView源码如下:

/**
  * We have one child
  */
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            // 向上追溯源码,view是从ActivityThread的handleResumeActivity方法中来的
            // wm.addView(decor,l)其中的decor便是传递到此处的view
            mView = view;

            ......

            requestLayout();

            ......
                
        }
    }
}

注释中说明了参数View 的由来,这也就说明了View的添加是从DecorView开始的。此方法中有一个requestLayout()方法,从字面意思来看是“请求布局”。现在我们看它的方法体做了什么。源码如下:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

需要注意的就是scheduleTraversals()方法。去看一下它的实现。代码如下:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
     }
}

下面看一下mTraversalRunnable的实例化。这个可以自行去看源码,相当于重新启动了一个线程,它的run方法里是需要异步处理的任务。看源码可以知道它的run方法仅仅调用了一个doTraversal()方法。现在直接看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工作三大流程的一个起源,它实现了performMeasure()、performLayout()和performDraw()的调用。因此,至此View的添加过程分析完了。

该过程时序图如下:画的比较丑,大家见谅

分析完View的添加过程,现在我们来分析View的measure过程。

三、 View的measure过程分析

1. MeasureSpec介绍

MeasureSpec通过将SepcMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,而一个MeasureSpec也可以通过解包的形式来得出原始的SpecMode和SpecSize,注意这里提到的MeasureSpec是指MeasureSpec所代表的int值,而并非MeasureSpec本身。现在我们看源码中对它的解释。

/**
     * A MeasureSpec encapsulates the layout requirements passed from parent to child.
     * Each MeasureSpec represents a requirement for either the width or the height.
     * A MeasureSpec is comprised of a size and a mode. There are three possible
     * modes:
     * <dl>
     * <dt>UNSPECIFIED</dt>
     * <dd>
     * The parent has not imposed any constraint on the child. It can be whatever size
     * it wants.
     * </dd>
     *
     * <dt>EXACTLY</dt>
     * <dd>
     * 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.
     * </dd>
     *
     * <dt>AT_MOST</dt>
     * <dd>
     * The child can be as large as it wants up to the specified size.
     * </dd>
     * </dl>
     *
     * MeasureSpecs are implemented as ints to reduce object allocation. This class
     * is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
     */
    public static class MeasureSpec {
        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 {}

        /**
         * 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;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        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);
            }
        }

        /**
         * Extracts the mode from the supplied measure specification.
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

       .......

    }

主要看注释,源码中的注释写的很详细。从注释中我们可以得知:

1)MeasureSpec概括了从父布局传递给子View的布局要求。每一个MeasureSpec代表了宽度或者高度要求,它由Size(尺寸)和mode(模式)组成。

2)mode(模式)有三种取值:UNSPECIFIED、EXACTLY、AT_MOST。

3)UNSPECIFIED:未指定尺寸模式。父容器不对View做任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。

4)EXACTLY:精确模式。父容器已经检测出View所需要的精确大小,此时View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值(例如50dp)这两种模式。

5)AT_MOST:最大值模式。父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_conent。

6)makeMeasureSpec(int mode,int size)用于将mode和size打包成一个int型的MeasureSpec。

7)getSize(int measureSpec)方法用于从指定的measureSpec值中获取其size。

8)getMode(int measureSpec)方法用于从指定的measureSpec值中获取其mode。

2. ViewGroup.LayoutParams介绍

被用于子View告诉父布局它们想要怎样被布局。系统内部是通过MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。源码中对它的描述。源码如下:

 /**
     * LayoutParams are used by views to tell their parents how they want to be
     * laid out. See
     * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes}
     * for a list of all child view attributes that this class supports.
     *
     * <p>
     * The base LayoutParams class just describes how big the view wants to be
     * for both width and height. For each dimension, it can specify one of:
     * <ul>
     * <li>FILL_PARENT (renamed MATCH_PARENT in API Level 8 and higher), which
     * means that the view wants to be as big as its parent (minus padding)
     * <li> WRAP_CONTENT, which means that the view wants to be just big enough
     * to enclose its content (plus padding)
     * <li> an exact number
     * </ul>
     * There are subclasses of LayoutParams for different subclasses of
     * ViewGroup. For example, AbsoluteLayout has its own subclass of
     * LayoutParams which adds an X and Y value.</p>
     *
     * <div class="special reference">
     * <h3>Developer Guides</h3>
     * <p>For more information about creating user interface layouts, read the
     * <a href="{@docRoot}guide/topics/ui/declaring-layout.html">XML Layouts</a> developer
     * guide.</p></div>
     *
     * @attr ref android.R.styleable#ViewGroup_Layout_layout_height
     * @attr ref android.R.styleable#ViewGroup_Layout_layout_width
     */
    public static class LayoutParams {
        /**
         * Special value for the height or width requested by a View.
         * FILL_PARENT means that the view wants to be as big as its parent,
         * minus the parent's padding, if any. This value is deprecated
         * starting in API Level 8 and replaced by {@link #MATCH_PARENT}.
         */
        @SuppressWarnings({"UnusedDeclaration"})
        @Deprecated
        public static final int FILL_PARENT = -1;

        /**
         * Special value for the height or width requested by a View.
         * MATCH_PARENT means that the view wants to be as big as its parent,
         * minus the parent's padding, if any. Introduced in API Level 8.
         */
        public static final int MATCH_PARENT = -1;

        /**
         * Special value for the height or width requested by a View.
         * WRAP_CONTENT means that the view wants to be just large enough to fit
         * its own internal content, taking its own padding into account.
         */
        public static final int WRAP_CONTENT = -2;

        /**
         * Information about how wide the view wants to be. Can be one of the
         * constants FILL_PARENT (replaced by MATCH_PARENT
         * in API Level 8) or WRAP_CONTENT, or an exact size.
         */
        
        public int width;

        /**
         * Information about how tall the view wants to be. Can be one of the
         * constants FILL_PARENT (replaced by MATCH_PARENT
         * in API Level 8) or WRAP_CONTENT, or an exact size.
         */
        
        public int height;

        
    }

从源码中我们可以知道:

1)LayoutParams被View用于告诉它们的父布局他们想要怎么样被布局。

2)该LayoutParams基类仅仅描述了view希望宽高有多大。对于每一个宽或者高,可以指定为一下三种值中的一个:MATCH_PARENT,WARP_CONTENT,an exact number。

3)MATCH_PARENT:意味着该View希望和父布局尺寸一样大,如果父布局有padding,则要加上该padding值。

4)WARP_CONTENT:意味着该View希望其大小为能够包裹住自身内容即可,如果自己有padding,则要加上该padding值。

5)对ViewGroup不同的子类,也有相应的LayoutParams子类。

6)其width和height属性对应着xml布局文件中layout_width和layout_height属性。

3. View测量的流程

View体系的测量是从DecorView开始遍历测量的,至于原因上面在分析View的添加过程的时候提到过,此处不再赘述。通过上面的分析我们知道View工作的三大流程正是从ViewRootImpl的performTraversals方法开始的。下面我们就从此方法开始分析。

ViewRootImpl.performTraversals()方法源码 如下:

private void performTraversals() {   
     

    ........

        
    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, mWidth, mHeight);



    ........



    performDraw();

        
}

上述代码就是一个完整的绘制流程(包括测量布局和绘制)。

1)performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度。

2)performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域。

3)performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的区域,将所有View的当前需要显示的内容画到屏幕上。

4. 测量流程主要方法的分析

(1)ViewRootImpl.performMeasure方法分析,源码如下:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
            return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        // 关键代码
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

此方法代码较少,其中关键代码就是mView.measure(...,...),跟踪源码mView赋值的地方是:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            // 将参数中的view赋值给mView
            mView = view;


            .......

        }
    }
               
}

setView方法我们上面分析过,第一个参数View就是从ActivityThread类中handleResumeActivity方法中wm.addView传过来的,其实就是DecorView,所以,performMeasure方法中mView.measure(...,...)代码其实就是DecorView在执行measure()操作。DecorView存在如下继承关系DecorView extends FrameLayout extends ViewGroup extends View。View是所有控件的基类,所以View类型的mView指代DecorView是可以的。

(2)View.measure()方法

跟踪代码mView.measure()代码就会走到View.measure()方法处,关键代码如下:

 /**
     * <p>
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * </p>
     *
     * <p>
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * </p>
     *
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     *
     * @see #onMeasure(int, int)
     */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        

     .........

     // measure ourselves, this should set the measured dimension flag back
     onMeasure(widthMeasureSpec, heightMeasureSpec);


     .........
                
}

View.measure方法是被final关键字修饰的方法,这说明View不希望自己的子类对自身的measure方法做出修改。它的注释非常之多,可见它的重要性,现在我们看一下源码对此方法的解释。

a)此方法被调用是为了找出View应该为多大。父布局在width和height参数中提供了限制信息。

b)一个View的实际测量工作是在本方法调用的onMeasure方法中执行完成的。因此,只有onMeasure(int,int)方法可以并且必须被子类重写。(其实,我们知道,ViewGroup的子类必须重写onMeasure方法,才能绘制此容器内的子View。如果是一个extends View的自定义控件那么是可以不用重写onMeasure方法的。)

c)widthMeasureSpec:父容器加入的水平空间要求。

d)heightMeasureSpec:父容器加入的数值空间要求。

(3)View.onMeasure方法

View.measure方法中调用了onMeasure方法。现在我们看一下View类中onMeasure方法的源码实现。

/**
     * <p>
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * </p>
     *
     * <p>
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass'
     * {@link #onMeasure(int, int)} is a valid use.
     * </p>
     *
     * <p>
     * The base class implementation of measure defaults to the background size,
     * unless a larger size is allowed by the MeasureSpec. Subclasses should
     * override {@link #onMeasure(int, int)} to provide better measurements of
     * their content.
     * </p>
     *
     * <p>
     * If this method is overridden, it is the subclass's responsibility to make
     * sure the measured height and width are at least the view's minimum height
     * and width ({@link #getSuggestedMinimumHeight()} and
     * {@link #getSuggestedMinimumWidth()}).
     * </p>
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     *
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

面对如此多的注释肯定是一脸茫然,但是我们也只能硬着头皮看下去了,源码中对方法的注释还是比较准确的多去关注一下方法的注释对理解方法的作用有很大帮助。

1)测量该View以及它的内容来决定测量的宽度和高度。它被measure方法调用,它应该被子类重写来提供准确而且有效的对它们的内容的测量。

2)当重写该方法时,必须调用setMeasuredDimension(int,int)来存储该View测量出的宽和高。如果不这样做将会触发IllegalStateException,由measure(int,int)抛出。调用onMeasure(int,int)方法是一个行之有效的办法。

3)测量的基类实现默认为背景的尺寸,除非MeasureSpec允许更大的尺寸。子类应该重写onMeasure(int,int)方法来提供对内容更好的测量。

4)如果该方法被子类重写,子类负责确保测量的高度和宽度至少应该是该View的minimum高度值和minimum宽度值。

5)widthMeasureSpec:父布局加入的水平空间要求。

6)heightMeasureSpec:父布局加入的垂直空间要求。

注意:容器类控件都是ViewGroup的子类,都会重写onMeasure方法;如对于叶子节点View,即最里层的控件,如TextView也可能会重写onMeasure方法。所以当流程走到onMeasure方法时,流程可能就会切到那些重写onMeasure的方法去。最后通过从根View到叶子节点View的遍历和递归,最终的测量还是会在叶子View中调用的setMeasuredDimension()方法中完成。

(4)View.setMeasuredDimension方法。话不多说看源码:

/**
     * <p>This method must be called by {@link #onMeasure(int, int)} to store the
     * measured width and measured height. Failing to do so will trigger an
     * exception at measurement time.</p>
     *
     * @param measuredWidth The measured width of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     * @param measuredHeight The measured height of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     */
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {

        ......

        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

注释:

1)该方法必须被onMeasure方法调用,目的是为了保存测量的宽度和高度。

2)该方法的两个参数是该View被测量出的宽度和高度值。参数由widthMeasureSpec变成了measuredWidth,也就是由父布局对子View的要求变为了view的宽度,高度也是如此。这两个参数是getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec);和getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec)。所以接下来我们需要分析一下getSuggestedMinimumWidth这个方法。

(5)getSuggestedMinimumWidth()方法。源码如下:

/**
     * Returns the suggested minimum width that the view should use. This
     * returns the maximum of the view's minimum width
     * and the background's minimum width
     *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned width is within the requirements of the parent.
     *
     * @return The suggested minimum width of the view.
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

注释:

1)返回该View应该使用的最小建议宽度值。该方法将返回View的最小宽度值和背景的最小宽度值两者中较大的一个。

2)当在onMeasure(int,int)方法使用时,调用者应该仍然确保返回的宽度值符合父布局的要求。

3)返回值:View的建议最下宽度值。

从代码中可以看出,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此View的宽度即为android:minWidth属性所指定的值。这个属性如果不指定,那么mMinWidth则默认为0;如果View指定了背景,则View的宽度为max(mMinWidth,mBackground,getMinimumWidth())。

(6)getDefaultSize(int size,int measureSpec)方法。源码如下:

 /**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

注释解释:

1)返回测量后的默认大小。如果MeasureSpec没有施加限制,则使用提供的大小。如果MeasureSpec允许,尺寸会变大。

这个方法的逻辑很简单,对于我们来讲,我们只需要了解AT_MOST和EXACTLY这两种。其实getDefaultSize方法返回的大小就是MeasureSpec中的specSize,而这个specSize就是View测量后的大小。

注意:View的最终大小是在Layout阶段确定的,而在绝大多数情况下View的测量大小和View的最终大小是相等的。

此时,需要绘制的宽高值就确定下来了。在上面分析setMeasuredDimension(int ,int )方法时,其内部最后调用了setMeasuredDimensionRaw(int,int)方法。现在我们看一下这个方法的原码。

/**
     * Sets the measured dimension without extra processing for things like optical bounds.
     * Useful for reapplying consistent values that have already been cooked with adjustments
     * for optical bounds, etc. such as those from the measurement cache.
     *
     * @param measuredWidth The measured width of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     * @param measuredHeight The measured height of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     */
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

代码中对View的成员变量mMeasuredWidth和mMeasuredHeight赋值了,这也就意味着View的测量工作结束了。

(7)performTraversals方法中在调用performMeasure方法前有调用getRootMeasureSpec方法,它的作用是基于window的大小计算出root View的MeasureSpec。它的两个实际参数mWidth和lp.width可以在源码中找到,其中mWidth是frame.width也就是整个屏幕的宽度。而lp.width最终可以在ActivityThread类中的handleResumeActivity方法中找到。

至此,View的测量过程就分析完了。现在我们看一下ViewGroup中为测量子View而定义的方法。

5. ViewGroup中关于对子View测量的方法

ViewGroup中关于对子View测量的辅助方法有三个,它们分别是measureChild、measureChildWithMargins和getChildMeasureSpec。这三个方法中前两个的作用几乎是一样的,不同之处通过方法名也可以分析出来,后者是对有margins属性的子View的测量。这两个方法中都调用了第三个方法,可见getChildMeasureSpec方法是很重要的,现在我们来仔细分析一下getChildMeasureSpec方法。其源吗如下:

 /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

该方法的作用就是,将从父布局传递给子View的MeasureSpec和子View的LayoutParams整合成子View的MeasureSpec。

现在分析方法体内具体做了哪些事情。

● 当父布局传递给子View的测量模式为MeasureSpec.EXACTLY时,即父布局对子View的要求是一个精确值。这有两种情况:

a)child的布局参数为具体值,它是LayoutParams中的width属性,对应xml布局文件中的layout_width属性,它是一个具体指(不包括match_parent)。此时child的测量大小就能确定就是这个具体值childDimension, 测量模式也能确定就是这个MeasureSpec.EXACTLY。 这里有一个需要注意的地方,如果子View的laytout_width比父布局大,这个结论也是成立的,看到这里可能会有人觉得有问题,在多数人的认知里子View的宽度再怎样大也不能超过父布局的大小。事实上,我们平时看到的是最后布局绘制出来的结果,而当前过程是测量过程,它是在布局过程之前的,是有一定差别的。

b)child的布局参数为LayoutParams.MATCH_PARENT。它的尺寸和父布局一样也是一个精确值,所以resultSize为前面得到的父布局的size值,由父布局决定,resultMode为MeasureSpec.EXACTLY。

c)child的布局参数为LayoutPatams.WARP_CONTENT。当子View的layout_width的属性设置为warp_content,即使在屏幕上我们看到的控件很小但是在测量阶段它的大小也是跟父布局一样的大小,所以resultSize为父布局的size值,specMode为MeasureSpec.AT_MOST。

● 父布局传递给子View的测量模式为MeasureSpec.AT_MOST(即最大值)时,其对应于layout_width的属性值为warp_content,此时子View的布局参数对结果的决定性很大。

a)child的布局参数为精确值时(不包括match_parent),此时resultSize为自身设置的精确值也就是childDimension,specMode为MeasureSpec.EXACTLY。

b)child的布局参数为LayoutParams.MATCH_PARENT。此时resultSize的大小由父布局决定,为size;specMode和父布局一样为MeasureSpec.AT_MOST。

c)child的布局参数为LayoutPatams.WARP_CONTENT。此时resultSize的大小由父布局决定,为size;specMode为MeasureSpec.AT_MOST。

至于传递给子View的测量模式为MeasureSpec.UNSPECIFIED时,这种情况平时我们很少能遇到,一般都用在系统级的控件中。所以这里不做分析。

至此,View和ViewGroup的Measure过程我们就分析完了。接下来就是下一个工作过程的分析学习了。

四、View的Layout过程

当View的Measure过程完成后,接下来就会进入Layout阶段,也就是布局阶段。前面measure的作用是测量每个View的尺寸,而layout的作用是根据前面测量的尺寸以及设置的其他属性值,共同来确定View的位置。

1.  ViewRootImpl类中的performTraversals方法是View工作三大流程的起源。对应布局阶段的部分就是performLayout。现在我们分析一下此方法的具体代码。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
       
        ......

        final View host = mView;

        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());


        ......

            
}

performLayout方法代码比较多,这里只留下了关键的部分。mView上面提到过正是DecorView,布局流程也是从DecorView开始遍历和递归的。

2. layout方法正式启动布局流程

DecorView是一个容器,是FrameLayout的子类,而FrameLayout又是ViewGroup的子类。所以跟踪host.layout方法的时候,实际上是进入到ViewGroup类的layout方法中了。现在看一下ViewGroup.layout方法的源码:

@Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

这是一个final类型的方法,所以自定义的ViewGroup子类无法重写该方法,可见系统不希望自定义的ViewGroup子类破坏layout流程。继续追踪super.layout方法,又跳转到View中的layout方法了。现在看一下View.layout方法的源码:

/**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    @SuppressWarnings({"unchecked"})
    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);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            ......

            
        }
        
    }

看一下注释:

1)为View及其所有子视图分配大小和位置。

2)这是View工作流程的第二阶段(第一阶段是测量)。在这一阶段中,每一个父布局都会对它的子View进行布局来放置它们。一般来说,该过程会使用在Measure过程存储的child的测量值。

3)View的子类不应该重写该方法。有子View的派生类应该重写onLayout方法。在重写的onLayout方法中,它们应该为每一个子View调用layout方法进行布局。

4)参数依次为:left、top、right、bottom四个点相对父布局的位置。

3. setFrame方法代码分析

在上述代码中为changed赋值的代码中调用了setFrame方法,现在我们看一下setFrame方法做了什么事情?源码如下:

/**
     * Assign a size and position to this view.
     *
     * This is called from layout.
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the
     *         previous ones
     * {@hide}
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    protected boolean setFrame(int left, int top, int right, int bottom) {

        
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;


       }
    }

1)该方法用于给当前view分配尺寸和位置。实际的布局工作正是在这里完成的。

2)返回值:如果新的尺寸和位置与原来的不同则返回true。

在调用invalidate()方法之后,又将传递进来的参数赋值给了View的四个属性值。因此,View的布局工作是在此处完成的。

4. View.onLayout方法的代码分析

在View的layout方法中调用了setFrame方法之后又根据changed的值进而调用了onLayout方法,而View的onLayout方法是一个空方法。源码如下:

/**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

我们主要对它的注释做一下翻译

1)当该view要分配尺寸和位置给它的每一个子View时,该方法会在layout方法中被调用。

2)有子View的派生类(容器类)应该重写该方法并且为每一个子View调用layout方法。

layout过程是父容器布局子View的过程,onLayout方法叶子View没有意义,只有ViewGroup才有用。现在我们看一下ViewGroup类中onLayout方法的重写。代码如下:

@Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

它是一个抽象方法,所以由此就能知道只要是继承自ViewGroup的容器,都必须重写onLayout方法。所以,现在我们直接进入DecorView看一下它的onLayout方法。代码如下:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        ......
    }

此方法中调用了父类的onLayout方法,DecorView extends FrameLayout类,所以我们再看一下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();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                
                ......


                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

它的onLayout方法直接调用了layoutChildren方法,关于layoutChildren方法只贴出了关键代码。可以看到,该方法也是通过遍历的方式对每一个child进行布局,调用layout方法。如果当前是容器类View,会继续递归下去,如果是叶子View,则会走到View的onLayout方法。

至此,Layout流程就分析完了,这个相对Measure过程要简单的多。

五、View的Draw流程分析

当layout过程完成之后就进入到draw阶段,在这个阶段,会根据layout中确定的各个View的位置将它们绘制出来。这个过程相对来说也比较简单。首先,draw的源头也是ViewRootImpl.performTraversals方法,其中调用了performDraw方法。现在我们分析一下performDraw方法的源码。代码如下:

private void performDraw() {
        
        ......

        boolean canUseAsync = draw(fullRedrawNeeded);

        ......
           
}

private boolean draw(boolean fullRedrawNeeded) {        

    ......

    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
        scalingRequired, dirty, surfaceInsets)) {
        return false;
    }

    ......
          
}


/**
  * @return true if drawing was successful, false if an error occurred
  */
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

    ......

    mView.draw(canvas);

    ......

}

mView就是DecorView。DecorView的“画”的流程就是从这里开始的。现在看一下DecorView的draw方法。源码如下:

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);

    if (mMenuBackground != null) {
        mMenuBackground.draw(canvas);
    }
}

DecorView的draw方法里,调用了super.draw(canvas),并且还绘制了背景。我们主要是看super.draw方法做了什么。跟踪代码可知FrameLayout和ViewGroup都没有重写draw方法,所以super.draw就直接进入到了View.draw方法。现在看一下View.draw方法的源码,代码如下:

/**
     * Manually render this view (and all of its children) to the given Canvas.
     * The view must have already done a full layout before this function is
     * called.  When implementing a view, implement
     * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
     * If you do need to override this method, call the superclass version.
     *
     * @param canvas The Canvas to which the View is rendered.
     */
    @CallSuper
    public void draw(Canvas canvas) {
        

        /*
         * 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)
         */

        // Step 1, draw the background, if needed
        int saveCount;


        // skip step 2 & 5 if possible (common case)
       
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

           

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

            
        }

        
    }

这段代码描述了draw阶段完成的7个主要步骤,先看一下注释:

1)手动渲染该View(及其所有的子View)到给定的画布上。

2)在该方法调用之前,该View必须已经完成了全面的布局。当正在实现一个View时,实现onDraw方法而不是此方法。

3)参数canvas:将View渲染的画布。

从代码上看,这里做了很多工作,咱们简单说明一下,有助于理解这个“画”的过程。

1)步骤一:画背景。对应我们xml布局文件中设置的“android:background”属性,这是整个“画”过程的第一步。

2)步骤三:画内容。(第二步和第五步只有有需要的时候才用到,这里先跳过)。比如TextView的文字等。

3)步骤四:画子View。dispatchDraw方法用于帮助ViewGroup来递归画它的子View。

4)步骤六:画装饰。这里指画滚动条和前景。其实平时我们所见的每一个View都有滚动条,只是没有显示而已。

现在看一下步骤三中调用的onDraw方法。源码如下:

/**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }

它是一个空方法,实现这个方法用来做“画”的工作。也就是说,具体的View需要重写该方法,来画自己想展示的东西,如文字,线条等。DecorView中重写了该方法,所以流程会走到DecorView的onDraw方法。代码如下:

@Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
                mStatusColorViewState.view, mNavigationColorViewState.view);
    }

这里调用了父类的onDraw方法,并且还画了自己的东西。上面分析过FrameLayout和ViewGroup都没有重写该方法,所以super就直接到了View.onDraw,而View.onDraw方法又什么都没做。所以,DecorView的onDraw方法画完自己的东西,紧接着流程就又走到了dispatchDraw方法了。源码如下:

/**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }

注释:被draw方法调用用来画子View。该方法可能会被派生类重写来获取控制,这个过程在该View的子View被画之前(但在它自己被画完成之后)。也就是说当本view被画完之后,交就要开始画它的子View了。这个方法也是一个空方法,实际上对于叶子View来说,该方法没有什么意义,因为它已经没有子View需要画了,而对于ViewGroup来说,就需要重写该方法来画它的子View。

通过跟踪源码可以发现,平时用到的LinearLayout、FrameLayout、RelativeLayout等常用的布局控件,都没有重写该方法,DecorView中也一样,而是只在ViewGroup中实现了dispatchDraw方法的重写。所以当DecorView执行完onDraw方法后,流程就会切换到ViewGroup中的dispatchDraw方法中了。现在看一下ViewGroup的dispatchDraw方法的原码:

@Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        
        ......

        for (int i = 0; i < childrenCount; i++) {
            
            more |= drawChild(canvas, transientChild, drawingTime);
               

        }
    }

从代码中可以发现,这里其实就是对每一个child执行drawChild的操作。看一下drawChild方法的原码:

/**
     * Draw one child of this View Group. This method is responsible for getting
     * the canvas in the right state. This includes clipping, translating so
     * that the child's scrolled origin is at 0, 0, and applying any animation
     * transformations.
     *
     * @param canvas The canvas on which to draw the child
     * @param child Who to draw
     * @param drawingTime The time at which draw is occurring
     * @return True if an invalidate() was issued
     */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

看一下注释的内容:

1)画当前ViewGroup中的某一个子View。该方法负责在正确的状态下获取画布。这包括了裁剪,移动,以便子View的滚动原点为0、0,以及提供任何动画转换。

2)参数drawingTime:“画”动作发生的时间点。

继续追踪代码,进入到如下流程:

/**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     *
     * This is where the View specializes rendering behavior based on layer type,
     * and hardware acceleration.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
       
        draw(canvas);
                
    }

注释中提到:该方法被ViewGroup.drawChild()方法调用,来让每一个子View画它自己。

该方法中,又回到了draw(canvas)方法中了,然后再开始画其子View,这样不断递归下去,直到画完整个DecorView树。

至此,View工作的三大流程就分析完了。根节点是DecorView,整个View体系就是一棵以DecorView为根的View树,依次通过遍历来完成Measure、layout和draw过程。

事实来说,这篇文章我还是“借鉴”了Android开发艺术探索一书中的一些内容,还有其他写的不错的文章。相比较而言可能不如别人写的好,我主要是为了自己以后看起来更亲切,毕竟是经过自己手的,总比看别人的文章更加熟悉。对这篇文章的备战过程中我还真是发现对知识理解的这个深度还差得远。所以,本文也只能起到一个勉励自己的作用了。如果大家又发现其他一些更好的文章,希望大家能在评论中分享给我,在此先谢谢大家了。

好了,写到这里,也算结束了。谢谢大家。     

猜你喜欢

转载自blog.csdn.net/zhourui_1021/article/details/104460814