Android视图层次结构简介

在介绍View绘制流程之前,咱们先简单介绍一下Android视图层次结构以及DecorView,因为View的绘制流程的入口和DecorView有着密切的联系。

我们平时在Activity中setContentView()中设置的layout,对应的是上图中的ViewGrop。

从Activity启动开始的视图绘制调用过程

ActivityThread.java类是Android应用的入口类,在启动Activity的过程中,会调用ActivityThread的handleResumeActivity()方法,关于视图的绘制过程最初就是从这个方法开始的。

1.从Activity启动到视图绘制的UML时序图

2.相关关键的类简介

相关类的信息可查看Android窗口机制

3.上述过程源码分析

ActivityThread.handleResumeActivity()方法 :

  @Overridepublic void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {...//其他代码if (a.mVisibleFromClient) {if (!a.mWindowAdded) {a.mWindowAdded = true;wm.addView(decor, l);} else {// The activity will get a callback for this {@link LayoutParams} change// earlier. However, at that time the decor will not be set (this is set// in this method), so no action will be taken. This call ensures the// callback occurs with the decor set.a.onWindowAttributesChanged(l);}}...//其他代码}

调用WindowManagerImpl.addView() :

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

调用WindowManagerGlobal.addView():

    public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {...//其他代码root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);// do this last because it fires off messages to start doing thingstry {root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {// BadTokenException or InvalidDisplayException, clean up.if (index >= 0) {removeViewLocked(index, true);}throw e;}}}

调用ViewRootImpl.setView()

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {synchronized (this) {if (mView == null) {//将顶层视图DecorView赋值给全局的mViewmView = view;..//其他代码//标记已添加DecorViewmAdded = true;.............//请求布局requestLayout();..//其他代码}}

调用了requestLayout()方法:

    @Overridepublic 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();}}

这个方法关键的就是调用mChoreographer.postCallback()向mChoreographer提交了一个mTraversalRunnable,等待Choreographer执行这个TraversalRunnable(Choreographer的机制可以查看Android 编舞者Choreographer),看下TraversalRunnable :

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

TraversalRunnable里面就执行了doTraversal()方法:

  void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);if (mProfile) {Debug.startMethodTracing("ViewAncestor");}performTraversals();if (mProfile) {Debug.stopMethodTracing();mProfile = false;}}}

里面执行了关键的performTraversals()方法:

private void performTraversals() {// cache mView since it is used so much below...final View host = mView;        //mView就是DecorView根布局//是否正在遍历mIsInTraversal = true;//是否马上绘制ViewmWillDrawSoon = true;..//其他代码//希望的窗口的宽高int desiredWindowWidth;int desiredWindowHeight;//Window的参数WindowManager.LayoutParams lp = mWindowAttributes;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.widthhe和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(); //执行绘制操作}

终于到了performMeasure(),performLayout(),performDraw()这三个关键的方法。
上述代码就是一个完整的绘制流程,包括三个步骤:

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

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

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

接下来就对这三个过程展开分析。

measure过程分析

measure过程关键类

MeasureSpec

测量规格或者测量参数,MeasureSpec是View的静态内部类,封装的是父容器传递给子容器的布局要求。 MeasureSpec中的值是一个整型(32位),其中高两位是mode,后面30位存的是size,是为了减少对象的分配开支。其中mode有三种模式:

  • UNSPECIFIED
    The parent has not imposed any constraint on the child. It can be whatever size it wants.
    父容器对于子容器没有任何限制,子容器想要多大就多大

  • EXACTLY
    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.
    父容器已经为子容器设置了精确的尺寸,无论子容器想要多大的空间,都应当遵循这些边界。

  • AT_MOST
    The child can be as large as it wants up to the specified size.
    子容器可以按其需要大到指定大小。即给出限定的最大值,子View最大只能是这个值。

如果从代码上来看,view.measure(int widthMeasureSpec, int heightMeasureSpec) 的两个MeasureSpec是父View传递过来的,但子View并不是只按照MeasureSpec,而是由·MeasureSpec和子View自己的LayoutParams共同决定的,子View的LayoutParams就是我们在xml写的时候设置的layout_width和layout_height 转化而来的。

即父容器的MeasureSpec和子View的LayoutParams共同决定了子View的MeasureSpec。
在measure阶段,View的宽和高由其measureSpec中的specSize决定。

普通View的MeasureSpec的创建规则:

ViewGroup.LayoutParams

LayoutParams被view用于告诉它们的父布局它们想要怎样被布局。
该LayoutParams基类仅仅描述了view希望宽和高有多大。对于每一个宽或者高,可以指定为以下三种值中的一个:MATCH_PARENT,WRAP_CONTENT,an exact number。
对ViewGroup不同的子类,也有相应的LayoutParams子类。

measure过程源码分析

ViewRootImpl.performMeasure()方法:

  //=============ViewRootImpl.java==============private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {......mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);......}

这个mView就是ViewRootImpl的setView方法()传递进来的DecorView,看下View.measure()方法:

    //===========================View.java===============================/*** <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 backonMeasure(widthMeasureSpec, heightMeasureSpec);......}

实际测量工作是在onMeasure(int,int)方法中实现的,ViewGroup的实现类必须重写onMeasure()方法,才能绘制该容器内的子View。

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));}

里面就一句代码,调用setMeasuredDimension()方法存储测量出的宽度和高度。看一下onMeasure()方法的注释:

/**
测量该view以及它的内容来决定测量的宽度和高度。该方法被measure()方法调用,并且应该被子类重写来对它们的内容进行准确而且有效的测量。当重写该方法时,您必须调用setMeasuredDimension(int,int)来存储该view测量出的宽和高。如果不这样做将会触发一个IllegalStateException异常,由measure()方法抛出。调用基类的onMeasure(int,int)方法是一个有效的方法。测量过程在基类中的实现是默认为背景的尺寸,除非更大的尺寸被MeasureSpec所允许。子类应该重写onMeasure(int,int)方法来提供对内容更好的测量。如果该方法被重写,子类负责确保测量出的高和宽至少是该view的mininum高度值和mininum宽度值(getSuggestedMininumHeight()和getSuggestedMininumWidth());*/

也就是说,容器类控件(ViewGroup的子类)如FrameLayout、LinearLayout、RelativeLayout等,都会重写onMeasure方法,根据自己的特性来进行测量,最后会调用setMeasuredDimension()方法来存储测量后的宽度和高度;

ViewGroup的measureChildWithMargins()

父View的measure的过程会先测量子View,等子View测量结果出来后再来测量自己,measureChildWithMargins()方法就是用来测量子View的:

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { // 子View的LayoutParams,你在xml的layout_width和layout_height,// layout_xxx的值最后都会封装到这个个LayoutParams。final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();   //根据父View的测量规格和父View自己的Padding,//还有子View的Margin和已经用掉的空间大小(widthUsed),就能算出子View的MeasureSpec,具体计算过程看getChildMeasureSpec方法。final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,           mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  + heightUsed, lp.height);  //通过父View的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,然后父容器传递给子容器的// 然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)去测量自己,如果子View是ViewGroup 那还会递归往下测量。child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}// spec参数   表示父View的MeasureSpec
// padding参数    父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出
//               子View的MeasureSpec的size
// childDimension参数  表示该子View内部LayoutParams属性的值(lp.width或者lp.height)
//                    可以是wrap_content、match_parent、一个精确指(an exactly size),
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  int specMode = MeasureSpec.getMode(spec);  //获得父View的mode  int specSize = MeasureSpec.getSize(spec);  //获得父View的大小  //父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。int size = Math.max(0, specSize - padding);   int resultSize = 0;    //初始化值,最后通过这个两个值生成子View的MeasureSpecint resultMode = 0;    //初始化值,最后通过这个两个值生成子View的MeasureSpecswitch (specMode) {  // Parent has imposed an exact size on us  //1、父View是EXACTLY的 !  case MeasureSpec.EXACTLY:   //1.1、子View的width或height是个精确值 (an exactly size)  if (childDimension >= 0) {            resultSize = childDimension;         //size为精确值  resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。  }   //1.2、子View的width或height为 MATCH_PARENT/FILL_PARENT   else if (childDimension == LayoutParams.MATCH_PARENT) {  // Child wants to be our size. So be it.  resultSize = size;                   //size为父视图大小  resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。  }   //1.3、子View的width或height为 WRAP_CONTENT  else if (childDimension == LayoutParams.WRAP_CONTENT) {  // Child wants to determine its own size. It can't be  // bigger than us.  resultSize = size;                   //size为父视图大小  resultMode = MeasureSpec.AT_MOST;    //mode为AT_MOST 。  }  break;  // Parent has imposed a maximum size on us  //2、父View是AT_MOST的 !      case MeasureSpec.AT_MOST:  //2.1、子View的width或height是个精确值 (an exactly size)  if (childDimension >= 0) {  // Child wants a specific size... so be it  resultSize = childDimension;        //size为精确值  resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY 。  }  //2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT  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;                  //size为父视图大小  resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST  }  //2.3、子View的width或height为 WRAP_CONTENT  else if (childDimension == LayoutParams.WRAP_CONTENT) {  // Child wants to determine its own size. It can't be  // bigger than us.  resultSize = size;                  //size为父视图大小  resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST  }  break;  // Parent asked to see how big we want to be  //3、父View是UNSPECIFIED的 !  case MeasureSpec.UNSPECIFIED:  //3.1、子View的width或height是个精确值 (an exactly size)  if (childDimension >= 0) {  // Child wants a specific size... let him have it  resultSize = childDimension;        //size为精确值  resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY  }  //3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT  else if (childDimension == LayoutParams.MATCH_PARENT) {  // Child wants to be our size... find out how big it should  // be  resultSize = 0;                        //size为0! ,其值未定  resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED  }   //3.3、子View的width或height为 WRAP_CONTENT  else if (childDimension == LayoutParams.WRAP_CONTENT) {  // Child wants to determine its own size.... find out how  // big it should be  resultSize = 0;                        //size为0! ,其值未定  resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED  }  break;  }  //根据上面逻辑条件获取的mode和size构建MeasureSpec对象。  return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec()方法非常关键,它根据View自己的MeasureSpec和子View的LayoutParams计算出要传递给子View的MeasureSpec。
上述代码为什么会有这么复杂的判断逻辑:
如果我们在xml 的layout_width或者layout_height 把值都写死,那么上述的测量完全就不需要了,之所以要上面的这步测量,是因为 match_parent 就是充满父容器,wrap_content 就是自己的内容多大自己就多大,我们写代码的时候特别爽,我们编码方便的时候,google就要帮我们计算你match_parent的时候是多大,wrap_content的时候是多大,这个计算过程,就是计算出来的父View的MeasureSpec不断往子View传递,结合子View的LayoutParams一起再算出子View的MeasureSpec,然后继续传给子View,不断计算每个View的MeasureSpec,子View有了MeasureSpec才能测量自己和自己的子View。

上述代码这么理解就比较简单了:

  • 如果父View的MeasureSpec是EXACTLY,说明父View的大小是确切的,(确切的意思很好理解,如果一个View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大)。

1)、如果子View 的layout_xxxx是MATCH_PARENT(充满整个父View),父View的大小是确切的,那么子View的大小肯定是确切的,而且大小值就是父View的size。所以这种情况下子View的MeasureSpec的mode=EXACTLY,size=父View的size。

2)、如果子View 的layout_xxxx是WRAP_CONTENT,也就是子View的大小是根据自己的content 来决定的,但是子View的毕竟是子View,大小不能超过父View的大小,但是子View的是WRAP_CONTENT,我们还不知道具体子View的大小是多少,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 调用的时候才去真正测量子View自己content的大小(比如TextView wrap_content 的时候你要测量TextView content 的大小,也就是字符占用的大小,这个测量就是在child.measure(childWidthMeasureSpec, childHeightMeasureSpec)的时候才能测出字符的大小,假设你字符100px,但是MeasureSpec要求最大的只能50px,这时候就要截掉了)。通过上述描述,子View的MeasureSpec mode的应该是AT_MOST,而size 暂定父View的 size,表示的意思就是子View的大小没有不确切的值,子View的大小最大为父View的大小,不能超过父View的大小(这就是AT_MOST的意思),然后这个MeasureSpec做为子View measure()方法的参数,做为子View的大小的约束,子View再实现自己的测量。所以这种情况下子View的MeasureSpec的mode = AT_MOST,size=父View的size。

3)、如果如果子View 的layout_xxxx是确定的值(比如200dp),那么就更简单了,不管你父View的mode和size是什么,子View写死了就是200dp,那么控件最后展示的就是200dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是这么大。所以这种情况下子View的MeasureSpec 的mode = EXACTLY, size=子View在layout_xxxx 填的那个值。

  • 如果父View的MeasureSpec 是AT_MOST,说明父View的大小是不确定的,只知道最大的大小是MeasureSpec的size值,不能超过这个值。

1)、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是不确定(只知道最大只能是MeasureSpec的size值),子View的大小MATCH_PARENT(充满整个父View),那么子View你即使充满父容器,你的大小也是不确定的,因为父View自己都确定不了自己的大小。所以这种情况下子View的mode=AT_MOST,size=父View的size。

2)、如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不确定(只知道最大只能是MeasureSpec的size值),子View又是WRAP_CONTENT,那么在子View的Content没算出大小之前,子View的大小最大就是父View的大小。所以这种情况下子View MeasureSpec的mode=AT_MOST,size=父View的 size。

3)、如果如果子View 的layout_xxxx是确定的值(比如200dp),同上,写多少就是多少。所以这种情况下子View的MeasureSpec 的mode = EXACTLY, size=子View在layout_xxxx 填的那个值。

  • 如果父View的MeasureSpec是UNSPECIFIED(未指定),表示没有任何约束,不像AT_MOST表示最大只能多大,不也像EXACTLY表示父View确定的大小,子View可以得到任意想要的大小,不受约束。

1)、如果子View 的layout_xxxx是MATCH_PARENT,因为父View的MeasureSpec是UNSPECIFIED,父View自己的大小并没有任何约束和要求,那么对于子View来说无论是WRAP_CONTENT还是MATCH_PARENT,子View也是没有任何束缚的,想多大就多大,没有不能超过多少的要求,一旦没有任何要求和约束,size的值就没有任何意义了,所以一般都直接设置成0。所以这种情况下子View的MeasureSpec的mode = UNSPECIFIED,size=0。

2)、同 1)。 所以这种情况下子View的MeasureSpec的mode = UNSPECIFIED,size=0。

3)、如果如果子View 的layout_xxxx是确定的值(比如200dp),同上,写多少就是多少。(只要设置确切的值,那么无论怎么测量,大小都是不变的,都是layout_xxxx写的那个值)。所以这种情况下子View的MeasureSpec 的mode = EXACTLY, size=子View在layout_xxxx 填的那个值。

ViewGroup的onMeasure()方法

ViewGroup这个抽象类本身并没有实现onMeasure()方法,onMeasure()方法是由ViewGroup的实现类(如FrameLayout、LinearLayout、RelativeLayou)来实现的,来看下FrameLayout的onMeasure()方法:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  ...//其他代码//步骤1:先测量子Viewint maxHeight = 0;int maxWidth = 0;int childState = 0;for (int i = 0; i < count; i++) {    final View child = getChildAt(i);    if (mMeasureAllChildren || child.getVisibility() != GONE) {   // 遍历自己的子View,只要不是GONE的都会参与测量,measureChildWithMargins方法在上面// 的源码已经讲过了,基本思想就是父View把自己的MeasureSpec传给子View,结合子View自己的LayoutParams算出子View的MeasureSpec,然后继续往下传,// 最会传递到叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);       final LayoutParams lp = (LayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() +  lp.leftMargin + lp.rightMargin);        maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);  ...//其他代码}}...//其他代码//步骤2:再测量自己//所有的子View测量之后,经过一系列的计算之后通过setMeasuredDimension()设置自己的宽和高,//对于FrameLayout,根据最大的子View的大小,对于LinearLayout,根据宽度或者高度的累加,具体测量的原理去看看源码。//总的来说,父View是等所有的子View测量结束之后,再来测量自己的宽和高,最后调用setMeasuredDimension()方法存储测量出的宽和高。setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));...//其他代码}

一个具体例子分析measure过程

到目前为止,基本把measure主要原理都过了一遍,接下来我们结合实例来讲解整个measure的过程,定义一个 R.layout.activity_main 的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/linear"android:background="@android:color/holo_blue_dark"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:layout_marginTop="50dp"android:paddingBottom="70dp"    android:orientation="vertical">    <TextView        android:id="@+id/text"       android:background="@color/material_blue_grey_800"       android:layout_width="match_parent"     android:layout_height="wrap_content"  android:text="TextView"        android:textColor="@android:color/white"        android:textSize="20sp" />    <View       android:id="@+id/view"       android:background="@android:color/holo_green_dark"android:layout_width="match_parent" android:layout_height="150dp"/>
</LinearLayout>

上面的代码对于出来的布局是下面的一张图:

上面的图做些说明:

整个图是一个DecorView,DecorView可以理解成整个页面的根View,DecorView是一个FrameLayout,包含两个子View,一个id=statusBarBackground的View和一个是LineaLayout,id=statusBarBackground的View先不管,而这个LinearLayout比较重要,它包含一个title和一个content,title很好理解其实就是TitleBar或者ActionBar,content是一个FrameLayout,id是android.R.id.content,写的页面布局通过setContentView()加进来就成了content的直接子View。

整个View的层级图如下:

注: 1、header的是个ViewStub,用来惰性加载ActionBar,为了便于分析整个测量过程,我把Theme设成NoActionBar,这样分析时就可以忽略ActionBar的measure过程。
2、包含Header(ActionBar)和id/content的那个父View,我们就把他叫做ViewRoot,它是垂直的LinearLayout,放着整个页面除statusBar的之外所有的东西。

我们来看下ViewRootImpl的performTraversals()方法

private void performTraversals() {...//其他代码int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="+ mWidth + " measuredWidth=" + host.getMeasuredWidth()+ " mHeight=" + mHeight+ " measuredHeight=" + host.getMeasuredHeight()+ " coveredInsetsChanged=" + contentInsetsChanged);// Ask host how big it wants to beperformMeasure(childWidthMeasureSpec, childHeightMeasureSpec);...//其他代码}

先调用了getRootMeasureSpec()计算出MeasureSpec

    /*** Figures out the measure spec for the root view in a window based on it's* layout params.** @param windowSize*            The available width or height of the window** @param rootDimension*            The layout params for one dimension (width or height) of the*            window.** @return The measure spec to use to measure the root view.*/private static int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:// Window can't resize. Force root view to be windowSize.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:// Window can resize. Set max size for root view.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:// Window wants to be an exact size. Force root view to be that size.measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec;}

然后调用performMeasure()对mView进行测量

    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其实就是DecorView,整个View的绘制从DecorView开始的。

getRootMeasureSpec(mWidth, lp.width) 和 getRootMeasureSpec(mHeight, lp.height) 的参数mWidth和mHeight是屏幕的宽度和高度,lp是WindowManager.LayoutParams,lp.width和lp.height的默认值是MATCH_PARENT,所以通过getRootMeasureSpec()生成的测量规格MeasureSpec的mode是EXACTLY ,size是屏幕的宽和高。

通过getRootMeasureSpec()生成的两个MeasureSpec传递给了DecorView的measure()方法,DecorView的测量就开始了,我们画出传递给DecorView的MeasureSpec图:

1、-1 代表的是EXACTLY,-2 是AT_MOST
2、由于屏幕的像素是1440x2560,所以DecorView的MeasureSpec的size对应这两个值

DecorView是一个FrameLayout,接下来在FrameLayout 的onMeasure()方法中分析DecorView的测量子View的过程,DecorView测量完所有的子View再来测量自己。

//FrameLayout 的测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  ...//其他代码int maxHeight = 0;int maxWidth = 0;int childState = 0;for (int i = 0; i < count; i++) {    final View child = getChildAt(i);    if (mMeasureAllChildren || child.getVisibility() != GONE) {   measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);      ...//其他代码}}...//其他代码}

先测量的是ViewRoot的大小,调用measureChildWithMargins():

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

ViewRoot 是系统的View,它的LayoutParams默认都是match_parent,根据MeasureSpec的计算规则,计算出ViewRoot的MeasureSpec的mode=EXACTLY,size=DecorView的size。(DecorView的MeasureSpec的mode是EXACTLY,ViewRoot的layoutparams是match_parent)
所以ViewRoot的MeasureSpec图如下:

算出ViewRoot的MeasureSpec 之后,开始调用ViewRoot.measure 方法去测量ViewRoot的大小,然而ViewRoot是一个LinearLayout ,ViewRoot.measure最终会执行的LinearLayout 的onMeasure 方法,LinearLayout 的onMeasure()方法又开始逐个测量它的子View,上面的measureChildWithMargins()方法又会被调用,那么根据View的层级图,接下来先测量的是header(ViewStub),由于header的Gone,所以直接跳过不做测量工作,所以接下来测量ViewRoot的第二个child content(android.R.id.content),我们要算出content的MeasureSpec,所以又要拿ViewRoot 的MeasureSpec 和 android.R.id.content的LayoutParams 做计算了,计算过程就是调用getChildMeasureSpec()的方法,

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { .....//其他代码final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  + heightUsed, lp.height);  .....//其他代码
}public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  int specMode = MeasureSpec.getMode(spec);  //获得父View的mode  int specSize = MeasureSpec.getSize(spec);  //获得父View的大小  //父View的大小-自己的Padding+子View的Margin,得到值才是子View可能的最大值。  int size = Math.max(0, specSize - padding); .....//其他代码
}

由上面的代码
int size = Math.max(0, specSize - padding);
其中padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed
算出android.R.id.content 的MeasureSpec 的size。
由于ViewRoot 的mPaddingTop=100px(这个可能和状态栏的高度有关,我们测量的最后会发现id/statusBarBackground的View的高度刚好等于100px,ViewRoot 是系统的View,它的Padding我们没法改变),所以计算出来content 的MeasureSpec 的高度少了100px ,它的宽高的mode也是EXACTLY(ViewRoot 是EXACTLY,android.R.id.content 是match_parent)。所以content 的MeasureSpec 如下(高度少了100px):

content是FrameLayout,递归调用开始准备计算id/linear的MeasureSpec,我们先给出结果:

图中有两个要注意的地方:
1、id/linear的heightMeasureSpec 的mode=AT_MOST,因为id/linear 的LayoutParams 的layout_height=“wrap_content”
2、id/linear的heightMeasureSpec 的size 少了200px, 由上面的代码
padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;
int size = Math.max(0, specSize - padding);
由于id/linear 的 android:layout_marginTop=“50dp” 使得lp.topMargin=200px (本设备的density=4,px=4*pd),在计算后id/linear的heightMeasureSpec的size 少了200px。(布局代码前面已给出,可自行查看id/linear控件xml中设置的属性)

linear.measure()接着往下算linear的子View的的MeasureSpec,看下View层级图,往下走应该是id/text,接下来计算id/text的MeasureSpec,直接看图,mode=AT_MOST ,size 少了280px(父布局linear设置了paddingBottom=“70dp”,根据specSize - padding,所以少了280px),

算出id/text 的MeasureSpec后,接下来执行text.measure(childWidthMeasureSpec, childHeightMeasureSpec);准备测量id/text 的高宽,这时候已经到底了,id/text是TextView,已经没有子View了,这时候就执行TextView的onMeasure()方法了。
TextView 拿着刚才计算出来的heightMeasureSpec(mode=AT_MOST,size=1980),这个就是对TextView的高度和宽度的约束,进到TextView 的onMeasure(widthMeasureSpec,heightMeasureSpec) 方法,在onMeasure 方法执行调试过程中,我们发现下面的代码:

         ...//其他代码int desired = getDesiredHeight();  //desired=107pxheight = desired;mDesiredHeightAtMeasure = desired;if (heightMode == MeasureSpec.AT_MOST) {height = Math.min(desired, heightSize);  //heightSize=1980px}...//其他代码

TextView中字符的高度(也就是TextView的content高度[wrap_content])测出来=107px,107px 并没有超过1980px(允许的最大高度),所以实际测量出来TextView的高度是107px。
最终算出id/text 的mMeasureWidth=1440px,mMeasureHeight=107px。

TextView的高度已经测量出来了,接下来测量id/linear的第二个child(id/view),同样的原理测出id/view的MeasureSpec.

id/view的MeasureSpec 计算出来后,调用view.measure(childWidthMeasureSpec, childHeightMeasureSpec)的测量id/view的高宽,之前已经说过View measure的默认实现是:

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

最终算出id/view的mMeasureWidth=1440px,mMeasureHeight=600px。

id/linear 的子View的高度都计算完毕了,接下来id/linear就通过所有子View的测量结果计算自己的高宽,id/linear是LinearLayout,所以它的高度计算简单理解就是子View的高度的累积+自己的Padding.

最终算出id/linear的mMeasureWidth=1440px,mMeasureHeight=987px。

算出id/linear出来后,id/content 就要根据它唯一的子View id/linear 的测量结果和自己的MeasureSpec一起来测量自己的大小,具体计算的逻辑去看FrameLayout的onMeasure()方法的计算过程。以此类推,接下来测量ViewRoot,然后再测量id/statusBarBackground,最后测量DecorView 的高宽,最终整个测量过程结束。所有的View的大小测量完毕,所有View的getMeasuredWidth()和 getMeasuredHeight()都已经有值了。

Measure 分析到此为止。

layout过程分析

performTraversals ()方法调用performMeasure()计算出mMeasuredWidth和mMeasuredHeight后开始调用performLayout()来确定View具体放在哪个位置。
layout过程的主要作用 :根据子视图的大小以及布局参数将View放到合适的位置上。

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {mLayoutRequested = false;mScrollMayChange = true;mInLayout = true;final View host = mView;if (host == null) {return;}if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {Log.v(mTag, "Laying out " + host + " to (" +host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");}Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");try {host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());...//其他代码}

看下ViewGroup 的layout()方法:

    @Overridepublic 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 finishesmLayoutCalledWhileSuppressed = true;}}

layout 的具体实现是在super.layout(l, t, r, b)里面做的,那么我接下来看一下View类的layout函数

 public final void layout(int l, int t, int r, int b) {.....//设置View位于父视图的坐标轴boolean changed = setFrame(l, t, r, b); //判断View的位置是否发生过变化,看有必要进行重新layout吗if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {if (ViewDebug.TRACE_HIERARCHY) {ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);}//调用onLayout(changed, l, t, r, b); 函数onLayout(changed, l, t, r, b);mPrivateFlags &= ~LAYOUT_REQUIRED;}mPrivateFlags &= ~FORCE_LAYOUT;.....}

1、setFrame(l, t, r, b) 可以理解为给mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了,这几个值构成的矩形区域就是该View显示的位置,这里的具体位置都是相对与父视图的位置。
2、回调onLayout,对于View来说,onLayout只是一个空实现,一般情况下我们也不需要重载该函数:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  }

对于ViewGroup 来说,唯一的差别就是ViewGroup中多了关键字abstract的修饰,要求其子类必须实现onLayout函数:

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

而实现onLayout()的目的就是安排其children在父视图的具体位置,那么如何安排子View的具体位置呢?
看下FrameLayout的onLayout():

    //FrameLayout.java@Overrideprotected 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) {int childCount = getChildCount() ; ...//其他代码for(int i=0 ;i<childCount ;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();int childLeft;int childTop;int gravity = lp.gravity;if (gravity == -1) {gravity = DEFAULT_CHILD_GRAVITY;}...//其他代码//整个layout()过程就是个递归过程child.layout(l, t, r, b) ;}}        }

代码很简单,就是遍历自己的孩子,然后根据gravity,padding,margin等布局文件里的参数,结合measure过程测量出的mMeasuredWidth和mMeasuredHeight,计算各个child的l, t, r, b值,最后调用 child.layout(l, t, r, b) 递归继续给child布局。

layout 过程相对简单些,分析就到此为止。

draw过程分析

performTraversals ()方法调用performLayout()计算出View具体放在哪个位置之后调用performDraw()进行绘制:

 private void performDraw() {...//其他代码try {boolean canUseAsync = draw(fullRedrawNeeded);if (usingAsyncReport && !canUseAsync) {mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);usingAsyncReport = false;}} ...//其他代码}

调用了draw()方法:

 private boolean draw(boolean fullRedrawNeeded) {...//其他代码mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);...//其他代码}

调用绘制线程ThreadedRenderer的draw()方法进行绘制,参数mView就是DecorView,之后调用View.java的draw()方法:

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...background.draw(canvas);...// skip step 2 & 5 if possible (common case)...// Step 2, save the canvas' layers...if (solidColor == 0) {final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;if (drawTop) {canvas.saveLayer(left, top, right, top + length, null, flags);}...// Step 3, draw the contentif (!dirtyOpaque) onDraw(canvas);// Step 4, draw the childrendispatchDraw(canvas);// Step 5, draw the fade effect and restore layersif (drawTop) {matrix.setScale(1, fadeHeight * topFadeStrength);matrix.postTranslate(left, top);fade.setLocalMatrix(matrix);canvas.drawRect(left, top, right, top + length, p);}...// Step 6, draw decorations (scrollbars)onDrawScrollBars(canvas);}

注释写得比较清楚,一共分成6步,看注释:

 // skip step 2 & 5 if possible (common case)

除了2 和 5之外我们一步一步来看:
第1步:绘制View的背景
看注释即可,不是重点

private void drawBackground(Canvas canvas) { Drawable final Drawable background = mBackground; ...... //mRight - mLeft, mBottom - mTop layout确定的四个点来设置背景的绘制区域 if (mBackgroundSizeChanged) { background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);   mBackgroundSizeChanged = false; rebuildOutline(); } ...... //调用Drawable的draw() 把背景图片画到画布上background.draw(canvas); ......
}

第3步,绘制View的内容。
onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。

第4步, 对View的所有子View进行绘制
dispatchDraw(canvas) 方法是用来绘制子View的。
View.java 的dispatchDraw()方法是一个空方法,因为View本身就是叶子节点,不需要实现dispatchDraw ()方法,ViewGroup就不一样了,它实现了dispatchDraw ()方法:

@Overrideprotected void dispatchDraw(Canvas canvas) {...if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {for (int i = 0; i < count; i++) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {more |= drawChild(canvas, child, drawingTime);}}} else {for (int i = 0; i < count; i++) {final View child = children[getChildDrawingOrder(count, i)];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {more |= drawChild(canvas, child, drawingTime);}}}......}

dispatchDraw()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用drawChild()进行具体的绘制了,drawChild()方法实际调用的是子视图的draw()方法。

第6步 对View的滚动条进行绘制
看注释即可,不是重点

一张图看下整个draw的递归流程:

到此整个绘制过程基本讲述完毕了。

参考:

【朝花夕拾】Android自定义View篇之(一)View绘制流程
Android自定义view之measure、layout、draw三大流程
Android View的绘制流程

Android系统Choreographer机制实现过程

2019年百度Android面试题-公共技术点之 View 绘制流程

https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/app/ActivityThread.java
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/com/android/internal/policy/DecorView.java

https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/view/ViewRootImpl.java
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/view/Choreographer.java
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/view/WindowManagerImpl.java
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/view/WindowManagerGlobal.java
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/base/core/java/android/view/Window.java

Android自定义View绘制流程相关推荐

  1. android自定义弧度按钮,Android 自定义View 绘制六边形设置按钮

    今天逛酷安的时候,发现酷安的设置按钮(截图的右上角),是一个六边形 + 中心圆的图标,所以又是一个自定义View练习对象了.画圆很简单,知道半径即可,而重点就在画出六边形. 酷安截图.png 最终效果 ...

  2. Android自定义View绘制闹钟

    Android自定义View绘制闹钟 本文简单实现了一个闹钟,扩展View,Canvas绘制 效果如下: 代码如下: package com.gaofeng.mobile.clock_demo;imp ...

  3. android字符显示流程图,Android应用层View绘制流程与源码分析

    1  背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...

  4. Android应用层View绘制流程与源码分析

    前言 Activity中界面加载显示的基本流程原理,最终分析结果就是下面的关系: 看见没有,如上图中id为content的内容就是整个View树的结构,所以对每个具体View对象的操作,其实就是个递归 ...

  5. android画a4矩形,Android自定义View绘制原理:画多大?画在哪?画什么?(三)

    View绘制就好比画画,抛开Android概念,如果要画一张图,首先会想到哪几个基本问题: 画多大? 画在哪? 怎么画? Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分 ...

  6. Android 自定义View绘制电池图标

    /*** @anthor GrainRain* @funcation 自定义View绘制电池* @date 2019/8/27*/ public class DrawBatteryView exten ...

  7. android 自定义View绘制电池电量(电池内带数字显示)

    最新公司需要一个电池内带数字的显示电池电量需求,百度了一下.参考下面这篇文章写的Android自定义View之电池电量显示. 增加了里面电池电量数字显示,还有就是一个屏幕适配.不管屏幕分辨率基本都能适 ...

  8. Android 自定义View绘制的基本开发流程 Android自定义View(二)

    1 View绘制的过程 View的测量--onMeasure() View的位置确定--onLayout() View的绘制--onDraw() 2 View的测量--onMeasure() Andr ...

  9. Android中View绘制流程以及invalidate()等相关方法分析

                                                                                                        ...

最新文章

  1. NFS挂载失败(Kernel Panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0))
  2. 极速写作2017彻底卸载_如何将 Flash 从 Mac 和 Windows 系统中彻底卸载?
  3. 工业交换机和工控交换机有什么区别?
  4. 2008r2服务器频繁自动重启,解决windows server 2008 更新后不断重启现象
  5. 【2015年第4期】面向科技情报的互联网信息源自动发现技术
  6. js,在字符串中,查找某个字符的位置
  7. MongoDB安装及结合mongobooster可视化工具使用
  8. linux7 开启端口,常用CentOS7系统防火墙开启设置和开放端口方法
  9. [转载]MySQL优化之索引的运用(2)
  10. 13 Django组件- cookie与session
  11. linux qemu的使用教程,详解QEMU网络配置的方法
  12. Windows安全机制---数据执行保护:DEP机制
  13. Atcoder ABC162 D - RGB Triplets
  14. mysql密码expired_mysql密码过期的修改方法(your password has expired)
  15. CMMI五个成熟度级别和对应22个过程域(PA)
  16. 智力问答选择题_智力问答题题库
  17. 机动车号牌查询, 在线查询, api 查询
  18. SFM方向开源软件COLMAP代码分析
  19. 欧姆龙气压传感器 2SMPB-02E程序编写
  20. 仿163邮箱上传多附件,有点酷

热门文章

  1. 梅花香自苦寒来——从字节被裁员后,奋战5个月终于上岸阿里
  2. python培训班出来能找到工作吗-参加Python人工智能培训班能找到工作吗?
  3. 把一切看淡,心就不累了
  4. CAD中如何将图形对象快速转换成三维曲面?
  5. 【学习笔记】二元Logistic回归预测
  6. docker19.03最新版本安装并配置加速器
  7. 郑州史丹利家居经销商这颗老鼠屎,如何一步步毁了百年品牌?
  8. 高校最低录取分数查询C语言,高校往年录取分数查询系统
  9. C-CF29A Spit Problem
  10. 如何当好一枚交际花,且看汪涵的人际关系学