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事件分发机制原理源码分析到此就讲完了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值