android事件分發機制原理源碼分析詳解

  我們都知道,在android裏當點擊一個控件時,系統能準確地將事件傳遞給真正需要這個事件的控件,那麼當android系統捕獲到用戶的各種輸入事件之後,是如何傳遞分發的呢?其實android系統我們提供了一整套完善的事件傳遞、分發、處理機制,來幫助開發者完成準確的事件分發與處理。
 要了解觸摸事件的攔截機制,首先要了解什麼是觸摸事件?顧名思義,觸摸事件就是捕獲觸摸屏幕後產生的事件。通常的當點擊屏幕時,會產生兩個或三個事件——按下、滑動、擡起。android爲觸摸事件封裝了一個類——MotionEvent。如果重寫onMotionEvent()方法,你就會發現該方法的參數就是MotionEvent。
 在MotionEvent裏面封裝了不少好東西,比如觸摸點的座標,可以通過event.getX()方法和event.getY()方法取出座標點;再比如獲得點擊的事件類型,可以通過不同的Action(MotionEvent.ACTION_UP、MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE)來進行區分。
 而我們知道android的View結構是樹形結構,View放在一個ViewGroup裏面,這個ViewGroup可能放在另一個ViewGroup裏面,甚至還有可能繼續嵌套,一層層地疊起來。那我們的觸摸事件到底該分給誰呢?其實當用戶點擊屏幕後,產生事件由系統捕獲到,再傳遞給Activity,Activity再傳遞給頁面的最外層View,之後由最外層View分發給下面的View。如果中間有View進行處理,則事件不在往下傳遞,如果沒事,則傳遞到最裏層View後傳回給系統。
 本文將從Activity、View、ViewGroup的事件傳遞來分析android事件分發與攔截機制原理,如果想分析系統底層的原理,可以查看系統源碼。

觸摸事件的類型

觸摸事件對應的是MotionEvent類,事件的類型主要有如下三種:

  • ACTION_DOWN:用戶手指按下時產生,一個按下操作標誌着一次觸摸事件的開始。
  • ACTION_MOVE:用戶手指按下後,在擡起之前,如果移動的距離超過一定的閾值,那麼會被判定爲ACTION_MOVE操作。
  • ACTION_UP:用戶手指擡起時產生,一個按下操作標誌着一次觸摸事件的結束。

 在一次屏幕觸摸操作中,ACTION_DOWN和ACTION_UP這兩個事件是必需的,而ACTION_MOVE視情況而定,當然還有一些其他事件,像:ACTION_CANCEL、ACTION_OUTSIDE等,這些事件可以根據具體的需要來做區分處理。

事件傳遞的三個階段

在Activity、View、ViewGroup的事件傳遞時,主要分三個階段。

  • 分發(Dispatch):事件的分發對應這dispatchTouchEvent方法,在android系統中,所有的觸摸事件都是通過這個方法來分發的,方法原型如下:
public boolean dispatchTouchEvent(MotionEvent ev)

 在這個方法中,根據當前視圖的具體實現邏輯,來決定是直接消費這個事件還是將事件繼續分發給子視圖處理,方法返回爲true表示當前視圖消費掉,不再繼續分發事件;方法返回爲super.dispatchTouchEven表示繼續分發該事件。如果當前視圖是ViewGroup及其子類,則會調用onInterceptTouchEvent方法判斷是否攔截該事件。

  • 攔截(Intercept):事件的攔截對應這onInterceptTouchEvent方法,這個方法只在ViewGroup及其子類中才會存在,在View、Activity中是不存在的。方法的原型如下:
public boolean onInterceptTouchEvent(MotionEvent ev)

 這個方法也是通過返回的布爾值來決定是否攔截對應的事件,根據具體的實現邏輯,返回true表示攔截這個事件,不繼續分發給子視圖,同時交由自身的onTouchEvent方法進行消費;返回false或者super.onInterceptTouchEvent表示不對事件進行攔截,需要繼續傳遞給子視圖。

  • 消費(Consume):事件的消費對應着onTouchEvent方法,方法原型如下:
public boolean onTouchEvent(MotionEvent event)

 該方法返回值爲true表示當前視圖可以處理對應的事件,事件將不會向上傳遞給父視圖;返回值爲false表示當前視圖不處理這個事件,事件會被傳遞給父視圖的onTouchEvent方法進行處理。
 在android系統中,擁有事件傳遞處理能力的類有以下三種:

  • Activity:擁有dispatchTouchEvent和onTouchEvent兩個方法。
  • ViewGroup:擁有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三個方法。
  • View:擁有dispatchTouchEvent和onTouchEvent兩個方法。

事件分發的流程圖

在這裏插入圖片描述

Activity事件傳遞機制

 Activity擁有dispatchTouchEvent和onTouchEvent兩個方法,爲更好的瞭解兩個方法邏輯,我們從源碼入手,看看兩個方法的實現:

dispatchTouchEvent方法分析:

源碼如下:

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

源碼解析:
1、首先判斷當事件爲ACTION_DOWN時,調用onUserInteraction方法,而該方法在Activity是一個空方法,具體代碼如下:

 public void onUserInteraction() {
 }

2、之後再判斷窗口的superDispatchTouchEvent方法是否處理,如果返回true,則返回,否則繼續。我們對superDispatchTouchEvent進行跟蹤,我們知道在android中的窗口實現類是PhoneWindow,所有在PhoneWindow類中我們找到該方法的實現,具體源碼如下:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
	return mDecor.superDispatchTouchEvent(event);
}

 我們可以看出superDispatchTouchEvent是調用mDecor的superDispatchTouchEvent,mDecor是DecorView的對象, 我們繼續跟進看看具體源碼實現如下:

public boolean superDispatchTouchEvent(MotionEvent event) {
	return super.dispatchTouchEvent(event);
}

 而DecorView是繼承FrameLayout類的,FrameLayout又是ViewGroup的子類,到此我們就看清楚了,Activity的事件分發主要代碼還是ViewGroup來實現的。

3、最後如果superDispatchTouchEvent返回false,則調用onTouchEvent方法。

onTouchEvent方法分析:

源碼如下:

 public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

源碼解析:
 onTouchEvent的實現比較簡單,先判斷是否需要關閉,如果是,則返回true,否則返回false。爲了清楚地說明,我們先來定義一個MainActivity,並重寫dispatchTouchEvent和onTouchEvent兩個方法,並在每個觸發事件加了log,具體代碼如下:

public class MainActivity extends AppCompatActivity {
    private final String TAG = "MainActivity";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG,"onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG,"onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG,"onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.d(TAG,"onTouchEvent ACTION_CANCEL");
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG,"dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG,"dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG,"dispatchTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.d(TAG,"dispatchTouchEvent ACTION_CANCEL");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

當我們點擊頁面中空白處或處理處理事件的View時,在logcat中打印如下:在這裏插入圖片描述
這個是mDecor沒有攔截點擊事件,可以看出事件先傳遞給Activity的dispatchTouchEvent方法,再傳遞給Activity的onTouchEvent方法。如果mDecor在分發事件進行攔截,則不會傳遞給Activity的onTouchEvent方法。

View事件傳遞機制

 雖然ViewGroup是View的子類,但是這裏所說的View專指除ViewGroup外的View控件,如:TextView、Button等,View控件本身已經是最小的單位,不能在作爲其他View的容器。View控件擁有dispatchTouchEvent和onTouchEvent兩個方法。爲更好的瞭解兩個方法邏輯,我們從源碼入手,看看兩個方法的實現。

dispatchTouchEvent方法分析:

源碼如下:

 public boolean dispatchTouchEvent(MotionEvent event) {

        ......省略部分代碼

        boolean result = false;

        ......省略部分代碼

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
 		......省略部分代碼
        
        return result;
    }

源碼解析:
1、首先我們可以從地10行可以看出,(mViewFlags & ENABLED_MASK) == ENABLED且handleScrollBarDragging返回爲true時,設置 result = true,即表示事件已經處理。ENABLED_MASK用來判斷此視圖的啓用狀態,啓用狀態的解釋因子類而異,一般是用來控制是否可以點擊、拖拽等事件。handleScrollBarDragging方法,從名字可看出該方法是處理 ScrollBar 的 drag 操作的。

2、緊接着15~17行,先判斷ListenerInfo 是否爲空(即是否有監聽),再判斷是否設置了OnTouchListener監聽,如果都有話的,就調用onTouch方法,並設置 result = true。

3、再看21行,先判斷result 是否爲false,如果是的話,則調用onTouchEvent方法,最後返回result 。

 可以看出在View在進行事件分發的時候,先分發給handleScrollBarDragging方法,再分發給onTouch方法,最後判斷是否已經分發了,如果沒有,則分發給onTouchEvent方法。

onTouchEvent方法分析:

由於onTouchEvent代碼比較長,在這裏就摘取部分代碼進行說明:

 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }

 第1~3行可以看出,先獲取該視圖的各種點擊事件是否可以點擊,其他包含點擊事件、長按事件、上下文點擊事件,如果設置其中一種,則clickable爲true;

 由第5~13行可知,當視圖沒有啓用狀態,直接返回clickable值,可見isEnabled()爲false時,各種點擊事件失效。

  if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

 由第14行可知,調用了performClick()方法,而在performClick方法裏,回去執行onClick方法(即響應點擊事件),由此可知到,點擊事件的響應是在ACTION_UP狀態時響應(即手指擡起時回調)。

 if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }

 由第12行可知,checkForLongClick方法是用來處理長按事件的,可得長按事件時就開始產生。

ViewGroup的事件傳遞機制

 ViewGroup是作爲View的控件容器存在的,擁有dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三個方法,可以看出和View的唯區別就是多了一個onInterceptTouchEvent。而ViewGroup中的onTouchEvent方法還是用的View中的,所有就不再對onTouchEvent方法進行分析,只對dispatchTouchEvent和onInterceptTouchEvent進行分析。下看從源碼來看看兩個方法的實現:

onInterceptTouchEvent方法分析:

源碼如下:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

源碼解析:
 由上面可以知道,onInterceptTouchEvent方法默認是返回false,表示父容器默認不攔截事件,但是當手指在Scrollbar上是時,通過isOnScrollbarThumb方法返回值來確認是否要攔截。

dispatchTouchEvent方法分析:

由於dispatchTouchEvent代碼比較長,在這裏就摘取部分代碼進行說明:

 // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

 從上面代碼可以看出,在dispatchTouchEvent中,當一個事件開始時,即事件類型爲ACTION_DOWN(我們可以認爲一個事件的開始是ACTION_DOWN),便會清空事件分發的目標和狀態,然後執行resetTouchState方法重置了觸摸狀態。

  // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

 這段代碼主要就是ViewGroup對事件是否需要攔截進行的判斷。當事件爲ACTION_DOWN或mFirstTouchTarget 不爲空,且沒有FLAG_DISALLOW_INTERCEPT標記(該標記可以通過requestDisallowInterceptTouchEvent方法設置,及是否要父視圖攔截事件)時,調用onInterceptTouchEven方法判斷是否攔截事件。下面先對mFirstTouchTarget是否爲null這兩種情況進行說明。當事件沒有被攔截時,ViewGroup的子元素成功處理事件後,mFirstTouchTarget會被賦值並且指向其子元素。也就是說這個時候mFirstTouchTarget!=null。可是一旦事件被攔截,mFirstTouchTarget不會被賦值,mFirstTouchTarget也就爲null(這個可以在後面的分析中看到它的實現)。

 下面再來看一下ViewGroup對沒有攔截的事件是如何處理的:

 // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }

 第30~33,可以看出,遍歷所有的子View,並拿到一個child出來;

 第40~46行,可以看出,這裏主要是處理child是否是一個可訪問性焦點的視圖;

 第48~52行,可以看出,通過canViewReceivePointerEvents和isTransformedTouchPointInView方法判斷child是否能接收該事件和當前點擊事件是否在child視圖內,如果不是,則continue,即拿下一個子View。

 第54~59行,判斷當前child是否在觸摸目標鏈表中,如果在則直接跳出循環;
 第63~82行,這個是將事件分發給子View核心代碼,dispatchTransformedTouchEvent方法就是用來做事件分發轉換的,在這個方法裏我們可以找到如下代碼:

if (child == null) {
	handled = super.dispatchTouchEvent(event);
} else {
	 handled = child.dispatchTouchEvent(event);
 }

 這裏可以看出,當child爲空。也就是執行了super.dispatchTouchEvent()方法,由於ViewGroup繼承自View,所以這個時候又將事件交由父類的dispatchTouchEvent進行處理。當child不爲空,則調用child的dispatchTouchEvent方法。
 在第79行可以看出,當前child如果分發了事件,則會將當前child觸摸目標鏈表中。
 第91~99行,是用來處理沒有找到接收事件的View,就會從觸摸目標鏈表中找到最近添加的最少的目標做事件的處理者。

下面再來看看ViewGroup對事件攔截後,及觸摸目標鏈表的維護:

  // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

 第2~6行,當mFirstTouchTarget 時,直接調用dispatchTransformedTouchEvent方法進行事件的分發轉換,這個方法在前面已經說過了。那在什麼情況下mFirstTouchTarget 會爲空呢?當ViewGroup在事件爲ACTION_DOWN時直接攔截或觸摸目標鏈表爲空時,mFirstTouchTarget 會爲空。

 第11~34行,我們可以看出,這個有一個循環,是用來遍歷觸摸目標鏈表的,這也是維護觸摸目標鏈表的核心代碼,先看第13行,當alreadyDispatchedToNewTouchTarget 爲true且target == newTouchTarget時,設置handled 爲true,這是主要是用來排除剛剛添加的來的目標視圖,因爲剛剛加進觸摸目標鏈表視圖已經在分發尋找View的過程中對事件進行處理了。

 我們再看第16行,當resetCancelNextUpFlag爲true或intercepted爲true,即target 視圖取消一下步事件或父視圖攔截事件時,cancelChild 爲true;這個在第22~31行可以看到,當cancelChild 爲true,在將target 視圖從觸摸目標鏈表中移除並回收掉。

 我們再看第18行,這個就不用說了,這就是對觸摸目標鏈表中其他觸摸目標視圖進行事件分發轉換處理。

 好了,到此android事件分發機制原理源碼分析到此就講完了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章