Android自定义View绘制流程
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绘制流程相关推荐
- android自定义弧度按钮,Android 自定义View 绘制六边形设置按钮
今天逛酷安的时候,发现酷安的设置按钮(截图的右上角),是一个六边形 + 中心圆的图标,所以又是一个自定义View练习对象了.画圆很简单,知道半径即可,而重点就在画出六边形. 酷安截图.png 最终效果 ...
- Android自定义View绘制闹钟
Android自定义View绘制闹钟 本文简单实现了一个闹钟,扩展View,Canvas绘制 效果如下: 代码如下: package com.gaofeng.mobile.clock_demo;imp ...
- android字符显示流程图,Android应用层View绘制流程与源码分析
1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...
- Android应用层View绘制流程与源码分析
前言 Activity中界面加载显示的基本流程原理,最终分析结果就是下面的关系: 看见没有,如上图中id为content的内容就是整个View树的结构,所以对每个具体View对象的操作,其实就是个递归 ...
- android画a4矩形,Android自定义View绘制原理:画多大?画在哪?画什么?(三)
View绘制就好比画画,抛开Android概念,如果要画一张图,首先会想到哪几个基本问题: 画多大? 画在哪? 怎么画? Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分 ...
- Android 自定义View绘制电池图标
/*** @anthor GrainRain* @funcation 自定义View绘制电池* @date 2019/8/27*/ public class DrawBatteryView exten ...
- android 自定义View绘制电池电量(电池内带数字显示)
最新公司需要一个电池内带数字的显示电池电量需求,百度了一下.参考下面这篇文章写的Android自定义View之电池电量显示. 增加了里面电池电量数字显示,还有就是一个屏幕适配.不管屏幕分辨率基本都能适 ...
- Android 自定义View绘制的基本开发流程 Android自定义View(二)
1 View绘制的过程 View的测量--onMeasure() View的位置确定--onLayout() View的绘制--onDraw() 2 View的测量--onMeasure() Andr ...
- Android中View绘制流程以及invalidate()等相关方法分析
...
最新文章
- NFS挂载失败(Kernel Panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0))
- 极速写作2017彻底卸载_如何将 Flash 从 Mac 和 Windows 系统中彻底卸载?
- 工业交换机和工控交换机有什么区别?
- 2008r2服务器频繁自动重启,解决windows server 2008 更新后不断重启现象
- 【2015年第4期】面向科技情报的互联网信息源自动发现技术
- js,在字符串中,查找某个字符的位置
- MongoDB安装及结合mongobooster可视化工具使用
- linux7 开启端口,常用CentOS7系统防火墙开启设置和开放端口方法
- [转载]MySQL优化之索引的运用(2)
- 13 Django组件- cookie与session
- linux qemu的使用教程,详解QEMU网络配置的方法
- Windows安全机制---数据执行保护:DEP机制
- Atcoder ABC162 D - RGB Triplets
- mysql密码expired_mysql密码过期的修改方法(your password has expired)
- CMMI五个成熟度级别和对应22个过程域(PA)
- 智力问答选择题_智力问答题题库
- 机动车号牌查询, 在线查询, api 查询
- SFM方向开源软件COLMAP代码分析
- 欧姆龙气压传感器 2SMPB-02E程序编写
- 仿163邮箱上传多附件,有点酷