标签:
Android应用程序的界面是View组成的,这些View以树形结构组织,它们存在着父子关系,其中,子view位于父view里面,因此,在绘制一个Android应用程序窗口的View之前,我们首先要确定它里面的各个子view在父view里面的大小以及位置。确定各个子view在父view里面的大小以及位置的过程又称为测量过程和布局过程。测量和布局完后,就可以绘制view了.
参考上一篇文章Android应用程序窗口View的创建过程,可以知道Android应用程序窗口的根View是DecorView,而测量,布局,绘制这三个过程都是从DecorView开始的,然后递归到各个子view.
从Activity的onCreate(),到DecorView开始measure(),中间还经过了一些步骤.
下面先简单说一下中间的过程,然后重点分析measure的过程.
首先Activity的onCreate()函数里,我们创建应用程序的界面,也就是DecorView在这里创建好了.
接着Activity的onStart(),OnResume()相继被调用,我们就从OnResume()被调用之后往下看:
1.
在frameworks/base/core/java/android/app/ActivityThread.java中
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { .. ActivityClientRecord r = performResumeActivity(token, clearHide);// .... wm.addView(decor, l); }
WindowManager是用来管理应用程序窗口的.
2.
在frameworks/base/core/java/android/view/WindowManagerImpl.java中
public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); }
3.
在frameworks/base/core/java/android/view/WindowManagerGlobal.java中
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... root = new ViewRootImpl(view.getContext(), display); .. root.setView(view, wparams, panelParentView); ... }
4.
在frameworks/base/core/java/android/view/ViewRootImpl.java中
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { ... requestLayout(); ... res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel); .... }
mWindowSession.addToDisplay()就是把界面显示出来.
上面过程涉及了几个重要的类ViewRootImpl,WindowManagerImpl,WindowManagerGlobal,这里没详细说了,这里先了解大致流程.以后有时间再细说.
好了,现在就开始进入正题,进入requestLayout()里面.
requestLayout()里面会执行到performTraversals()
performTraversals()里面又会执行到 performMeasure(childWidthMeasureSpec,
childHeightMeasureSpec),而这个方法,就是开始测量了(Ask host how big it wants to be)
第一步:performTraversals()
在frameworks/base/core/java/android/view/ViewRootImpl.java中
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // Ask host how big it wants to be performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
childWidthMeasureSpec和childHeightMeasureSpec.分别表示宽度和高度的测量标准,这两个参数是传给DecorView测量用的.看获取该值的函数名字也很明显:getRootMeasureSpec().因为不单单是DecorView需要用测量标准去测量,它的子view,以及它子view的子view都需要自己的一个测量标准去测量,而它们的测量标准获取方法都是一样的,后面会说。
首先来看一下这两个值是怎么得到的:
int
childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int
childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
上面有三个变量mWidth,mHeight,lp.说明一下
mWidth和mHeight这两个值就是当前窗口(手机屏幕)的宽度和高度(减去状态栏的高度).其实测量过程不止一次,在performTraversals()函数一开始就会去测量一次,
DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; ... // Ask host how big it wants to be windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);
这个时候desiredWindowWidth和desiredWindowHeight是手机屏幕的宽度和高度了。
然后由于是第一次启动该画面,所以会请求WindowManagerService服务重新布局系统中的所有窗口.mWidth和mHeight就是WindowManagerService服务分配给该应用程序窗口的宽度和高度.而这时高度就是手机屏幕的高度减去状态栏的高度了。
这个lp 是WindowManager.LayoutParams类型,相当是DecorView的布局参数,每一个view都有自己的布局参数。
在frameworks/base/core/java/android/view/Window.java中
是Window的成员变量,一路传来到这里的
</pre><pre name="code" class="java" style="font-family: Arial; line-height: 26px;">
// The current window attributes. private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
看一下它的构造函数
frameworks/base/core/java/android/view/WindowManager.javapublic LayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = TYPE_APPLICATION; format = PixelFormat.OPAQUE; }
它的父类ViewGroup.LayoutParams
frameworks/base/core/java/android/view/ViewGroup.javapublic LayoutParams(int width, int height) { this.width = width; this.height = height; }
第二步:getRootMeasureSpec()
在frameworks/base/core/java/android/view/ViewRootImpl.java中
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; }
这里简单说一下MeasureSpec:
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }
SpecMode有三类,每一类都表示特殊的含义,如下表示。
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中match_parent和具体的数值这两种模式。
AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现。它对应于LayoutParams中的wrap_contetn.
继续来看getRootMeasureSpec()函数的代码
上面函数不管是获取DecorView宽度的测量标准,还是高度的测量标准,windowSize的是一个常量值
rootDimension的值根据上面LayoutParams的构造函数,知道都是MATCH_PARENT,因此都会执行:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
这么一看DecorView的测量标准是由两个变量决定的,一个是屏幕的宽度(相当于它的父视图),一个是它自己的期望(MATCH_PARENT)。那么其他View的测量标准是由什么决定呢,后面会看到
好了,DecorView的宽度和高度的测量标准都获得了,继续往下看:
第三步:performMeasure()
在frameworks/base/core/java/android/view/ViewRootImpl.java中
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { .. mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); ... }
第四步:measure()
在frameworks/base/core/java/android/view/View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { .. // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); .... }
第五步:onMeasure()
在frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); final boolean isPortrait = metrics.widthPixels < metrics.heightPixels; final int widthMode = getMode(widthMeasureSpec); final int heightMode = getMode(heightMeasureSpec); boolean fixedWidth = false; if (widthMode == AT_MOST) { ... } if (heightMode == AT_MOST) { ... } super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getMeasuredWidth(); boolean measure = false; widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY); if (!fixedWidth && widthMode == AT_MOST) { ... } // TODO: Support height? if (measure) { .. } }
final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);根据第二步知道,这两个值都是EXACTLY
接着super.onMeasure(widthMeasureSpec, heightMeasureSpec);调用它父类的onMeasure()函数,也就是FrameLayout的onMeasure()函数
第六步:onMeasure()
在frameworks/base/core/java/android/widget/FrameLayout.java中
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount();//获取decorView有多少个子view final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear(); 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); 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); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } // Account for padding too maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); count = mMatchParentChildren.size(); if (count > 1) { for (int i = 0; i < count; i++) { final View child = mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidthMeasureSpec; int childHeightMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } if (lp.height == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
这个函数需要注意一下,不同类型的ViewGroup子类,这个函数的实现不尽相同.
但是函数的作用都是一样的:
1.遍历自己的子view,然后测量每一个子view,如果子view也是ViewGroup,则递归下去.
2.设置自己的测量宽度和测量高度.
首先来看一下测量子view的过程.看measureChildWithMargins()函数
第七步:measureChildWithMargins()
在frameworks/base/core/java/android/view/ViewGroup.java中
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();//首先获取子View的布局参数 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); }在这个情景,这个函数根据DecorView的测量标准和它的子view的布局参数来确定它的子view的测量标准,然后子view开始自己的测量过程。其他情况也就是普通的ViewGroup的测量标准和它的子view的布局参数来确定它的子view的测量标准.
第八步:getChildMeasureSpec()
在frameworks/base/core/java/android/view/ViewGroup.java中
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec);//这个获取的是decorview的测量标准里面的mode int specSize = MeasureSpec.getSize(spec);//这个获取的是decorview的测量标准里面的值 int size = Math.max(0, specSize - padding);//这个padding包括decorview的padding值 和它子view的margin值 int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY://现在decorview的是这种mode if (childDimension >= 0) {//如果decorview的子view设置了固定值,那么它的测量标准也就定了 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 = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
android:layout_width=""
android:layout_height=""
设置成固定值或者MATCH_PARENT,WRAP_CONTENT,FILL_PARENT.
FILL_PARENT和MATCH_PARENT值一样.
总的来说,DecorView的子view测量标准也有两个变量决定的,一个是DecorView(父view)的测量标准,一个是DecorView的子view它自己的布局参数.
也就是说一般View的测量标准是由它父容器的测量标准和它本身的LayoutParams决定。
所以根View DecorView的测量标准获得,与其他普通View的测量标准获得有一定区别。
总结一下上面函数:
一:如果父View的specMode是MeasureSpec.EXACTLY:
①如果子view的布局宽度或高度是常量值,那么子view的SpecSize就是子view的布局宽度或高度的常量值,specMode为MeasureSpec.EXACTLY.
②如果子view的布局宽度或高度是MATCH_PARENT,那么子view的SpecSize就是父view的SpecSize,specMode为MeasureSpec.EXACTLY.。
③如果子view的布局宽度或高度是WRAP_CONTENT,那么子view的SpecSize就是父view的SpecSize,specMode为MeasureSpec.AT_MOST。
二:如果父view的specMode是MeasureSpec.AT_MOST:
①如果子view的布局宽度或高度是常量值,那么子view的SpecSize就是子view的布局宽度或高度的常量值,specMode为MeasureSpec.EXACTLY.
②如果子view的布局宽度或高度是MATCH_PARENT,那么子view的SpecSize就是父view的SpecSize,specMode为MeasureSpec.AT_MOST.。
③如果子view的布局宽度或高度是WRAP_CONTENT,那么子view的SpecSize就是父view的SpecSize,specMode为MeasureSpec.AT_MOST。
三:如果父view的specMode是MeasureSpec.UNSPECIFIED:
①如果子view的布局宽度或高度是常量值,那么子view的SpecSize就是子view的布局宽度或高度的常量值,specMode为MeasureSpec.EXACTLY.
②如果子view的布局宽度或高度是MATCH_PARENT,那么子view的SpecSize就是0,specMode为MeasureSpec.UNSPECIFIED.。
③如果子view的布局宽度或高度是WRAP_CONTENT,那么子view的SpecSize就是0,specMode为MeasureSpec.UNSPECIFIED。
第三情况可以忽略。获取子view的测量标准后回到第七步,子view要根据它的测量标准去测量自己宽度和高度了
<span style="font-size:14px;"> child.measure(childWidthMeasureSpec, childHeightMeasureSpec);</span>
这样就递归下去了,如果该child是ViewGroup的话,则又会调用它的onMeasure()函数,然后一个个去获取它子view的测量标准,函数也是getChildMeasureSpec(),接着它的子view调用自己的measure()函数去测量,这样一直递归到一个具体的view,在该view的onMeasure()函数里调用setMeasuredDimension()来设置自己测量宽度和高度了,
第九步:setMeasuredDimension()
在frameworks/base/core/java/android/view/view.java中
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
onMeasure()的两个参数也就是该View的宽度和高度的测量标准了.
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }view的测量宽度和高度就是getDefaultSize()函数的返回值了
首先来看一下getSuggestedMinimumWidth()函数
第十步:getSuggestedMinimumWidth()
在frameworks/base/core/java/android/view/view.java中
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }如果该View没有设置背景,那么返回mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,这个属性如果不指定,那么mMinWidth为0,如果view指定了背景,则返回mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());那么mBackground.getMinimumWidth()是什么呢?我们看一下Drawable的getMinimumWidth()函数
public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0.那么Drawable在什么情况下有原始高度呢,这里举个例子说明一下,ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高(图片的尺寸),以后有机会再讲。因为获取宽度和高度的原理一样,就没看了
第十一步:getDefaultSize()
在frameworks/base/core/java/android/view/view.java中
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; }
具体view在基类View中onMeasure()函数里都有了默认的实现。就是上面第九步至第十一步
而像DecorView这样的ViewGroup重写该onMeasure()函数,第五步第六步,这是因为ViewGroup还需要去遍历它的子view,让它子view又去测量它们自己,而且不同类型的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如LinerLayout和FrameLayout这两者的布局特性显然不同。
但是viewgroup递归测量完自己的子view后,也要通过setMeasuredDimension()的设置自己测量宽度和高度.回到第六步DecorView在递归测量完自己的子view后,要设置自己测量宽度和高度
在frameworks/base/core/java/android/view/view.java中
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
在frameworks/base/core/java/android/view/ViewGroup.java中
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { if (Build.VERSION.SDK_INT >= 11) { return View.resolveSizeAndState(size, measureSpec, childMeasuredState); } 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: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; } return result | (childMeasuredState&MEASURED_STATE_MASK); }
至于它的onMeasure()其他细节不再分析了,大家有兴趣可以去分析源码。
测量过程可能要重复几次.
上面就是测量的过程了.
标签:
原文地址:http://blog.csdn.net/hehe26/article/details/51910845