1. ViewRoot
ViewRoot是連接WindowManager與DecorView的紐帶,View的整個繪制流程的三大步(measure、layout、draw)都是通過ViewRoot完成的。當Activity對象被創建完畢后,會將DecorView添加到Window中(Window是對窗口的抽象,DecorView是一個窗口的頂級容器View,其本質是一個FrameLayout),同時會創建ViewRootImpl(ViewRoot的實現類)對象,並將ViewRootImpl與DecorView建立關聯。關於ViewRoot,我們只需要知道它是聯系GUI管理系統和GUI呈現系統的紐帶。View的繪制流程從ViewRoot的performTraversals方法開始,經過measure、layout、draw三大過程完成對一個View的繪制工作。peformTraversal方法內部會調用measure、layout、draw這三個方法,這三個方法內部又分別調用onMeasure、onLayout、onDraw方法。
在onMeasure方法中View會對其所有的子元素執行measure過程,此時measure過程就從父容器"傳遞"到了子元素中,接着子元素會遞歸的對其子元素進行measure過程,如此反復完成對整個View樹的遍歷。onLayout與onDraw過程的執行流程與此類似。
measure過程決定了View的測量寬高,這個過程結束后,就可以通過getMeasuredHeight和getMeasuredWidth獲得View的測量寬高了;
layout過程決定了View在父容器中的位置和View的最終顯示寬高,getTop等方法可獲取View的top等四個位置參數(View的左上角頂點的坐標為(left, top), 右下角頂點坐標為(right, bottom)),getWidth和getHeight可獲得View的最終顯示寬高(width = right - left;height = bottom - top)。
draw過程決定了View最終顯示出來的樣子,此過程完成后,View才會在屏幕上顯示出來。
2. MeasureSpec
MeasureSpec為一個32位的int值,高2位代表SpecMode,低30位代表SpecSize,前者指測量模式,后者指某種測量模式下的規格大小。在一個View的measure過程中,系統會將該View的LayoutParams結合父容器的“要求”生成一個MeasureSpec,這個MeasureSpec說明了應該怎樣測量這個View。
(1)三種 SpecMode:
UNSPECIFIED:父容器不對View作任何要求,通常用於系統內部,表示一種測量的狀態。
EXACTLY:父容器已經檢測出View所需要的精確大小,這種測量模式下View的測量值就是SpecSize的值。這個SpecMode對應於LayoutParams中的match_parent和給出具體大小這兩種模式。
AT_MOST:父容器指定了一個可用大小即SpecSize,View的大小不能大於此值,可用大小取決於不同View的具體實現。這個SpecMode對應於LayoutParams中的wrap_content。
(2)對於DecorView,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同確定;對於普通View,他的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同確定。
3. View的具體繪制流程
(1)measure過程
a. DecorView的measure過程
前面我們提到過,DecorView是一個應用窗口的根容器,它本質上是一個FrameLayout。DecorView有唯一一個子View,它是一個垂直LinearLayout,這個垂直線性布局管理器包含兩個子元素,一個是TitleView(ActionBar的容器),另一個是ContentView(窗口內容的容器)。關於ContentView,它是一個FrameLayout(android.R.id.content),我們平常用的setContentView就是設置它的子View。如下圖中,我們為TilteView中添加了一個ActionBar,為ContentView中添加了一個RelativeLayout(通過setContentView方法)。
前面提到,DecorView的MeasureSpec由窗口的尺寸和自身的LayoutParams共同決定。在ViewRootImpl的measureHierarchy方法中,完成了創建DecorView的MeasureSpec的過程,相應的代碼片段如下:
1 childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); 2 childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 3 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
以上代碼片段中的childXxxMeasureSpec即為DecorView的MeasureSpec,lp.width和lp.height被系統賦值為MATCH_PARENT。getRootMeasureSpec的代碼如下:
1 private int getRootMeasureSpec(int windowSize, int rootDimension) { 2 int measureSpec; 3 switch (rootDimension) { 4 case ViewGroup.LayoutParams.MATCH_PARENT: 5 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); 6 break; 7 case ViewGroup.LayoutParams.WRAP_CONTENT: 8 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); 9 break; 10 default: 11 measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); 12 break; 13 } 14 return measureSpec; 15 }
上述代碼中調用了makeMeasureSpec方法來獲取measureSpec,而傳入的rootDimension參數即為lp.width或lp.height,值為MATCH_PARENT,由此可得DecorView的MeasureSpec,其中SpecMode為EXACTLY,SpecSize為windowSize。
b. 普通View(非ViewGroup)的measure過程:
非ViewGroup的View的特點是不能有子元素,因此只需測量好自身就行。普通View的measure通過measure方法來完成:
1 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 2 //.... 3 4 //回調onMeasure()方法 5 onMeasure(widthMeasureSpec, heightMeasureSpec); 6 7 //more 8 }
普通View的measure方法是由ViewGroup在measureChild方法中調用的(即完成了measure過程從ViewGroup到子View的傳遞),ViewGroup調用其子View的measure時即傳入了該子View的widthMeasureSpec和heightMeasureSpec。注意到measure是一個final方法,因此要實現自定義的measure過程,需要重寫onMeasure方法,onMeasure方法源碼如下:
1 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 2 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 3 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 4 }
setMeasureDimension方法用於設置View的測量寬高,如果不重寫此方法,默認是直接調用getDefaultSize方法獲取尺寸的:
1 public static int getDefaultSize(int size, int measureSpec) { 2 int result = size; 3 int specMode = MeasureSpec.getMode(measureSpec); 4 int specSize = MeasureSpec.getSize(measureSpec); 5 switch (specMode) { 6 case MeasureSpec.UNSPECIFIED: 7 result = size; 8 break; 9 case MeasureSpec.AT_MOST: 10 case MeasureSpec.EXACTLY: 11 result = specSize; 12 break; 13 } 14 return result; 15 }
由以上代碼可知,正常情況下(SpecMode為AT_MOST或EXACTLY),getDefaultSize獲取的尺寸大小即為specSize。由以上代碼還可知道,直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當於使用match_parent的效果。示例如下:
1 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 2 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 3 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 4 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 5 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 6 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 7 if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { 8 setMeasuredDimension(mWidth, mHeight); 9 } else if (widthSpecMode == MeasureSpec.AT_MOST) { 10 setMeasuredDimension(mWidth, heightSpecSize); 11 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 12 setMeasuredDimension(widthSpecSize, mHeight); 13 } 14 }
上述示例代碼中的mWidth,mHeight是為wrap_content時設定的默認寬高。這個默認寬高可根據實際需要自行設置,比如TextView在wrap_content時的默認寬高是根據其中的所有文字的寬度來設定的,從而實現正好“包裹”文字內容的效果。
c. ViewGroup的measure過程:
ViewGroup需要先完成子View的measure過程,才能完成自身的measure過程,ViewGroup的onMeasure方法根據不同的布局管理器類(LinearLayout、RelativeLayout等等)有不同的實現,比如LinearLayout的onMeasure方法代碼如下:
1 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 2 if (mOriention == VERTICAL) { 3 measureVertical(widthMeasureSpec, heightMeasureSpec); 4 } else { 5 measureHorizontal(widthMeasureSpec, heightMeasureSpec); 6 } 7 }
measureVertical中測量子元素的主要代碼如下:
1 //See how tall everyone is. Also remember max width. 2 for (int i = 0; i < count; ++i) { 3 final View child = getVirtualChildAt(i); 4 . . . 5 //Determine how big this child would like to be. If this or previous children have given a weight, //then we allow it to use all available space (and we will shrink things later if needed). 6 measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, totalHeight == 0 ? mTotalLength : 0); 7 8 if (oldHeight != Integer.MIN_VALUE) { 9 lp.height = oldHeight; 10 } 11 12 final int childLength = child.getMeasuredHeight(); 13 final int totalLength = mTotalLength; 14 mTotalLength = Math.max(totalLength, totalLength+childHeight+lp.topMargin+lp.bottomMargin+getNextLocationOffset(child)); 15 }
由上述代碼可以知道,在measureVertical方法中會對每個LinearLayout中的子元素進行遍歷並通過measureChildBeforeLayout方法對每個子元素執行measure過程。在measureChildBeforeLayout方法內部會調用子元素的measure方法,這樣會依次讓每個子元素進入measure過程。mTotalLength表示LinearLayout在豎直方向上的尺寸,每完成一個子元素的measure過程,它的值也會相應增加。測量完子元素后,LinearLayout會測量自身的大小。measureVertical中測量LinearLayout自身的主要代碼如下:
1 //Add in our padding. 2 mTotalLength += mPaddingTop + mPaddingBottom; 3 int heightSize = mTotalLength; 4 //Check against our minimum height 5 heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); 6 //Reconcile our calculated size with the heightMeasureSpec 7 int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); 8 heightSize = heightSizeAndState & MEASURED_SIZE_MASK; 9 . . . 10 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
對垂直的LinearLayout來說,它在水平方向的測量過程與普通View的measure過程一樣,在豎直方向的measure過程如下:若該垂直LinearLayout的layout_height為match_parent或具體數值,它的measure過程與普通View一樣;若該垂直LinearLayout的layout_height為wrap_content,則它豎直方向的高度為所有子元素占用的高度之和,但不能超過父容器的可用空間大小,最終高度還要考慮到其豎直方向的padding,相關的代碼如下:
1 public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { 2 int result = size; 3 int specMode = MeasureSpec.getMode(measureSpec); 4 int specSize = MeasureSpec.getSize(measureSpec); 5 switch (speczMode) { 6 case MeasureSpec.UNSPECIFIED: 7 result = size; 8 break; 9 case MeasureSpec.AT_MOST: 10 if (specSize < size) { 11 result = specSize | MEASURED_STATE_TOO_SMALL; 12 } else { 13 result = size; 14 } 15 break; 16 case MeasureSpec.EXACTLY: 17 result = specSize; 18 break; 19 } 20 return result | (childMeasuredState & MEASURED_STATE_MASK); 21 }
ViewGroup主要通過其measureChildren方法完成其子View的measure過程,上面垂直LinearLayout中調用的measureChildBeforeLayout可以看做是measureChildren的一個“變種”,measureChildren方法代碼如下:
1 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { 2 final int size = mChildrenCount; 3 final View[] children = mChildren; 4 for (int i = 0; i < size; ++i) { 5 final View child = children[i]; 6 if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { 7 measureChild(child, widthMeasureSpec, heightMeasureSpec); 8 } 9 } 10 }
其中,measureChild方法完成對子View的measure過程:
1 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { 2 final LayoutParams lp = child.getLayoutParams(); 3 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 4 mPaddingLeft + mPaddingRight, lp.width); 5 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 6 mPaddingTop + mPaddingBottom, lp.height); 7 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 8 }
注意在這里,在執行child.measure方法前,就已經通過getChildMeasureSpec獲取了子View的MeasureSpec。getChildMeasureSpec根據子View的LayoutParams和父容器的MeasureSpec來決定子View的MeasureSpec,getChildMeasureSpec的代碼如下:
1 public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 2 //這里傳入的spec為ViewGroup的MeasureSpec 3 //specMode和specSize即為父容器的MeasureSpec 4 int specMode = MeasureSpec.getMode(spec); 5 int specSize = MeasureSpec.getSize(spec); 6 //padding為父容器中已使用的空間大小,size為父容器可用空間大小 7 int size = Math.max(0, specSize - padding); 8 int resultSize = 0; 9 int resultMode = 0; 10 11 switch (specMode) { 12 case MeasureSpec.EXACTLY: 13 if (childDimension >= 0) { 14 resultSize = childDimension; 15 resultMode = MeasureSpec.EXACTLY; 16 } else if (childDimension == LayoutParams.MATCH_PARENT) { 17 //子View想要和父容器一樣大 18 resultSize = size; 19 resultMode = MeasureSpec.EXACTLY; 20 } else if (childDimension == LayoutParams.WRAP_CONTENT) { 21 //子View想自己決定它的大小,但不能比父容器大 22 resultSize = size; 23 resultMode = MeasureSpec.AT_MOST; 24 } 25 break; 26 27 case MeasureSpec.AT_MOST: 28 if (childDimension >= 0) { 29 resultSize = childDimension; 30 resultMode = MeasureSpec.EXACTLY; 31 } else if (childDimension == LayoutParams.MATCH_PARENT) { 32 resultSize = size; 33 resultMode = MeasureSpec.AT_MOST; 34 } else if (childDimension == LayoutParams.WRAP_CONTENT) { 35 resultSize = size; 36 resultMode = MeasureSpec.AT_MOST; 37 } 38 break; 39 40 //Parent asked to see how big we want to be 41 case MeasureSpec.UNSPECIFIED: 42 if (childDimension >= 0) { 43 resultSize = childDimension; 44 resultMode = MeasureSpec.EXACTLY; 45 } else (childDimension == LayoutParams.MATCH_PARENT) { 46 resultSize = 0; 47 resultMode = MeasureSpec.UNSPECIFIED; 48 } else (childDimension == LayoutParams.WRAP_CONTENT) { 49 resultSize = 0; 50 resultMode = MeasureSpec.UNSPECIFIED; 51 } 52 break; 53 } 54 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 55 }
以上函數的工作過程可總結如下:
a. 當childLayoutParams指定為為具體的大小時:若parentSpecMode為EXACTLY,則childSpecMode為EXACTLY,childSpecSize為childSize(layout_width和layout_height中指定的具體大小);若parentSpecMode為AT_MOST,則childSpecMode和childSpecSize分別為EXACTLY和childSize。
b. 當childLayoutParams為match_parent時:若parentSpecMode為EXACTLY,則childSpecMode和childSpecSize分別為EXACTLY和parentSize(父容器中可用的大小);若parentSpecMode為AT_MOST,則childSpecMode和childSpecSize分別為AT_MOST和parentSize。
c. 當childLayoutParams為wrap_content時:若parentSpecMode為EXACTLY,則childSpecMode和childSpecSize分別為AT_MOST和parentSize;若parentSpecMode為AT_MOST,則childSpecMode和childSpecSize分別為AT_MOST和parentSize。
還有一點需要注意的是,View的measure過程和Activity生命周期的回調方法不是同步的,也就是不能保證在某個生命周期的回調方法中measure過程已經執行完畢。
(2)layout過程
layout過程用來確定View在父容器中的位置,因而是由父容器獲取子View的位置參數后,調用child.layout方法並傳入已獲取的位置參數,從而完成對子View的layout。當ViewGroup的位置被確定后,它在onLayout中會遍歷所有子元素並調用其layout方法,在layout方法中子元素的onLayout又會被調用。layout方法確定先View本身的位置,再調用onLayout方法確定所有子元素的位置。layout方法如下:
1 public void layout(int l, int t, int r, int b) {
2 …… 3 int oldL = mLeft; 4 int oldT = mTop; 5 int oldB = mBottom; 6 int oldR = mRight; 7 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : set(l, t, r, b); 8 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { 9 onLayout(changed, l, t, r, b); 10 mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; 11 ListenerInfo li = mListenerInfo; 12 if (li != null && li.mOnLayoutChangeListeners != null) { 13 ArrayList<OnLayoutChangeListener> listenersCopy = 14 (ArrayList<OnLayoutChangeListener>) li.mOnLayoutChangeListeners.clone(); 15 int numListeners = listenersCopy.size(); 16 for (int i = 0; i < numListeners; ++i) { 17 listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); 18 } 19 } 20 } 21 mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; 22 mPrivateFlags3 |= PFLAGS3_IS_LAID_OUT; 23 }
layout方法的大致流程:首先通過setFrame方法設定View的四個位置參數,即用傳來的l、t、r、b四個參數初始化mLeft、mTop、mRight、mBottom這四個值,從而確定了該View在父容器中的位置。若位置發生改變就調用onLayout方法,onLayout方法在View類中為空,因為對子元素布局的工作只有容器View才需要做。在ViewGroup中,onLayout是一個抽象方法,因為對於不同的布局管理器類,對子元素的布局方式是不同的。比如,LinearLayout的onLayout方法如下:
1 protected void onLayout(boolean changed, int l, int t, int r, int b) {
2 if (mOriention == VERTIVAL) { 3 layoutVertical(l, t, r, b); 4 } else { 5 layoutHorizontal(l, t, r, b); 6 } 7 }
以上代碼會根據LinearLayout的orientation為水平或垂直調用相應的函數來完成布局過程,這里以layoutVertical為例分析一下垂直線性布局管理器的布局過程,layoutVertical的主要代碼如下:
1 void layoutVertical(int left, int top, int right, int bottom) {
2 . . . 3 final int count = getVirtualChildCount(); 4 for (int i = 0; i < count; i++) { 5 final View child = getVirtualChildAt(i); 6 if (child == null) { 7 childTop += measureNullChild(i); 8 } else if (child.getVisibility() != GONE) { 9 final int childWidth = child.getMeasuredWidth(); 10 final int childHeight = child.getMeasuredHeight(); 11 12 final int LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); 13 . . . 14 if (hasDividerBeforeChildAt(i)) { 15 childTop += mDividerHeight; 16 } 17 18 childTop += lp.topMargin; 19 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); 20 childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); 21 22 i += getChildrenSkipCount(child, i); 23 } 24 } 25 }
以上代碼中,LinearLayout會遍歷它的所有子View,並調用setChildFrame方法設置子View的位置,代碼中的childTop代表當前子View的top位置參數。setChildFrame方法的代碼如下:
1 private void setChildFrame(View child, int left, int top, int width, int height) {
2 child.layout(left, top, left + width, top + height); 3 }
也就是說,在父容器(這里為LinearLayout)完成了對子元素位置參數(top、left、right、bottom)的獲取后,會調用子元素的layout方法,並把獲取到的子元素位置參數傳入,從而完成對子元素的layout過程。子元素在自己的layout方法中,也會先完成對自己的布局(確定四個位置參數),再調用onLayout方法完成對其子View的布局,這樣layout過程就沿着View樹一層層傳了下去。
layout過程完成后,便可以通過getWidth和getHeight方法獲取View的最終顯示寬高,這倆方法源碼如下:
1 public final int getWidth() {
2 return mRight – mLeft; 3 }
1 public final int getHeight() {
2 return mBottom – mTop; 3 }
由此便可以知道,通過getMeasuredWidth/getMeasuredHeight方法獲取的測量寬高與通過getWidth/getHeight方法獲取的最終顯示寬高的區別:即最終顯示寬高是通過View的位置參數相減得到的,正常情況下應該與測量寬高相等。但如果我們重寫View的layout方法如下:
1 public void layout(int l, int t, int r, int b) {
2 super.layout(l, t, r, b, r + 100, b + 100); 3 }
這樣就會導致最終顯示寬高比測量寬高大100。(除非你很明確的知道自己想要干啥,否則不應該這樣做)
(3)draw過程
主要分為以下六步:
a. 繪制背景;
b. 如果要視圖顯示漸變框,這里會做一些准備工作;
c. 繪制視圖本身,即調用onDraw()函數。在View中onDraw()是個空函數,也就是說具體的視圖都要override該函數來實現自己的顯示,而對於ViewGroup則不需要實現該函數,因為作為容器是“沒有內容“的,其包含了多個子view,而子view已經實現了自己的繪制方法,因此只需要告訴子view繪制自己就可以了,也就是下面的dispatchDraw()方法;
d. 繪制子視圖,即dispatchDraw()函數。在View中這是個空函數,具體的視圖不需要實現該方法,它是專門為容器類准備的,也就是容器類必須實現該方法;
e. 如果需要, 開始繪制漸變框;
f. 繪制滾動條;
draw方法的代碼如下:
1 public void draw(Canvas canvas) {
2 final int privateFlags = mPrivateFlags; 3 final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && 4 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); 5 mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; 6 // Step 1, draw the background, if needed 7 int saveCount; 8 if (!dirtyOpaque) { 9 drawBackground(canvas); 10 } 11 //step 2 & 5 12 final int viewFlags = mViewFlags; 13 boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; 14 boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; 15 if (!verticalEdges && !horizontalEdges) { 16 // Step 3, draw the content 17 if (!dirtyOpaque) onDraw(canvas); 18 // Step 4, draw the children 19 dispatchDraw(canvas); 20 // Step 6, draw decorations (scrollbars) 21 onDrawScrollBars(canvas); 22 if (mOverlay != null && !mOverlay.isEmpty()) { 23 mOverlay.getOverlayView().dispatchDraw(canvas); 24 // we're done... 25 return; 26 } 27 } 28 }
View的draw過程的傳遞通過diapatchDraw來實現,dispatchDraw會遍歷調用所有子View的draw方法,這樣draw事件就一層層傳了下去。重寫View的onDraw方法可以定制View繪制出來的樣子,例如實現一些特殊的圖形和動畫。
View有個名為setWillNotDraw的方法,若一個View不需要繪制任何內容,可通過這個方法將相應標記設為true,系統會進行相應優化。ViewGroup默認開啟這個標記,View默認不開啟。
以上是我學習View的繪制流程后的簡單總結,很多地方敘述的還不夠清晰准確,如有問題歡迎大家在評論區一起討論 :)