安卓开发之事件分发机制

在上一篇文章安卓开发之事件处理机制中提到了安卓中事件被激发后需要被分发然后处理,前篇文章提到了基于监听和基于回调两种事件处理方式,这次就来学习下事件分发机制以及与事件处理的关系。

事件分发

Android中的每个控件都会在界面中占得一块矩形的区域,在Android中控件大致被分为两类,即ViewGroup控件与View控件。ViewGroup 控件作为父控件可以包含多个View控件,并管理其包含的View 控件。通过ViewGroup,整个界面上的控件形成了一个树形结构,这也就是我们常说的控件树,上层控件负责下层子控件的测量与绘制,并传递交互事件。通常在Activity中使用的findViewById()方法,就是在控件树中以树的深度优先遍历来查找对应元素。在每棵控件树的顶部,都有一个ViewParent对象,这就是整棵树的控制核心,所有的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。View树结构如下图所示:
在这里插入图片描述
我们基于上图的模型来理解事件分发机制。接上篇文章,我们这里所说的事件就是指MotionEvent(手指触摸屏幕锁产生的一系列事件,包括ACTION_DOWN-手指刚接触屏幕、ACTION_MOVE-手指在屏幕上滑动、ACTION_UP-手指在屏幕上松开的一瞬间、ACTION_CANCEL:手指保持按下操作,并从当前控件转移到外层控件时会触发),事件分发实际上是对MotionEvent事件的分发过程。即当一个MotionEvent事件产生了以后,系统需要把这个事件传递给一个具体的 View去处理,这个传递的过程就是事件的分发,点击事件的分发过程由三个很重要的方法来共同完成: dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面先介绍一下这几个方法:

  • dispatchTouchEvent:该方法用来分发事件,一般不会重写这个方法
  • onInterceptTouchEvent:用来拦截事件,返回结果表明是否拦截当前事件(View中没有这个拦截方法,只有ViewGroup中有)
  • onTouchEvent:用来处理事件,返回结果代表是否消耗当前事件,和上篇文章中基于回调的事件传播类似

理解事件分发的过程,可以将上面的图简化成下图所示,想像成一个公司,总经理是ViewGroupA,部长是ViewGroupB,你是底层View
在这里插入图片描述
那么公司现在接到一个事件了,首先会传给总经理ViewGroupA,这时总经理ViewGroupA的dispatchTouchEvent方法就会被调用,如果总经理ViewGroupA的onInterceptTouchEvent方法返回为true,就说明他觉得这个事件子元素例如部长和你都不能处理,那么他自己就拦截这个事件然后处理,总经理的onTouchEvent方法就会被调用,当然默认情况下ViewGroup都不会拦截事件的,也就是返回值默认为false,如果返回为false的话,说明他觉得事件可以交给子元素处理,那么他不会拦截这个事件,然后当前事件就会继续传递给它的子元素(例如ViewGroupB),接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。当然也存在事件最后虽然交给你View来处理了,但是你也不能处理的情况,也就是onTouchEvent返回为false的情况,这时候就只能把事情交给上级去处理了,直到某上级处理完事件。看到这里是不是和上篇文章里基于回调的事件传播部分(如下图所示)说的感觉有点像呢?
在这里插入图片描述
当一个View要处理某个事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会首先被回调。这时事件如何处理要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不
会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可以看出平时常用的OnClickListener, 其优先级最低,即处于事件传递的尾端。

当然上面的流程是整个事件分发中的一部分,在Android中产生点击事件后,遵循的顺序实际上为:Activity -> Window -> 顶级View,即事件总是先传递给Activity, Activity 再传递给Window (之后再介绍Window扮演的角色) ,最后Window再传递给顶级View。顶级View 接收到事件后,就会按照上面说的事件分发流程去分发事件。最后如果onTouch和View的onTouchEvent方法返回都为false的,就会交给父容器的onTouchEvent处理,如果Activity的子元素一路都返回false无法处理事件的话,那么最终这个事件就会由Activity来处理,因为它是最终的大领导,所以不管Activity能不能成功处理,也不会再由其他对象来处理该事件了。

写一个Demo来验证下上面的流程,代码参考《安卓群英传》。新建ViewGroupA.java:

public class ViewGroupA extends LinearLayout {
    public ViewGroupA(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("MEvent:","ViewGroupA dispatchTouchEvent执行了");
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("MEvent:","ViewGroupA onInterceptTouchEvent执行了");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("MEvent","ViewGroupA onTouchEvent执行了");
        return super.onTouchEvent(event);
    }
}

ViewGroupB.java:

public class ViewGroupB extends LinearLayout {
    public ViewGroupB(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("MEvent:","ViewGroupB dispatchTouchEvent执行了");
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("MEvent:","ViewGroupB onInterceptTouchEvent执行了");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("MEvent","ViewGroupB onTouchEvent执行了");
        return super.onTouchEvent(event);
    }
}

MyView.java:

public class MyView extends View {
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("MEvent:","View dispatchTouchEvent执行了");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("MEvent","View onTouchEvent执行了");
        return super.onTouchEvent(event);
    }
}

Activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <com.demo.mevent.ViewGroupA
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:background="#ff0033">
        <com.demo.mevent.ViewGroupB
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:gravity="center"
            android:background="#336699">
            <com.demo.mevent.MyView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:clickable="true"
                android:background="#ffff00"/>
        </com.demo.mevent.ViewGroupB>
    </com.demo.mevent.ViewGroupA>
</LinearLayout>

UI界面效果如下:
在这里插入图片描述
点击中间View后查看Logcat:
在这里插入图片描述
可以看到事件的传递流程: ViewGroupA首先得到点击事件,并由它的dispatchTouchEvent方法来分发,由于它的onInterceptTouchEvent方法默认返回false没有做出拦截,因此事件就传递给了子元素ViewGroupB,而同样由于ViewGroupB的onInterceptTouchEvent方法在它的dispatchTouchEvent方法分发事件时没有做出拦截,所以事件最终被传递给MyView,由MyView来处理这个事件。

如果MyView的onTouchEvent方法返回值为false,也就是它也无法处理这个事件:
在这里插入图片描述
这时候再点击如下图所示:
在这里插入图片描述
MyView的onTouchEvent方法虽然执行了,但是它无法处理,于是交给部长ViewGroupB,由它的onTouchEvent来处理这个事件,但是由于部长ViewGroupB的onTouchEvent也没有成功处理这个事件,所以这个事件又传递给总经理ViewGroupA,由它的onTouchEvent方法来处理这个事件,这时候由于它是大领导,所以不管ViewGroupA能不能成功处理,也不会再有其他对象来处理该事件了。

假如部长ViewGroupB的onTouchEvent返回为true,也就是他成功处理了这个事件了,这种情况下就不用ViewGroupA去处理了:
在这里插入图片描述
如果在事件分发的过程中部长ViewGroupB拦截了这个事件,也就是onInterceptTouchEvent返回了true,这时候就没你View什么事了:
在这里插入图片描述事件传递到部长ViewGroupB就终止传递,由ViewGroupB的onTouchEvent方法来处理事件,这时候假如部长ViewGroupB的onTouchEvent返回为true表明他已经成功处理了这个事件,因此也不会再返回给ViewGroupA的onTouchEvent来处理了。

源码分析

Demo看完了,让我们再从源码角度分析下上述的过程。参考Android笔记-从ViewGroup的dispatchTouchEvent源码分析事件分发机制

前面说到,当点击事件发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工作是由Activity 内部的Window来完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器( 即setContentView 所设置的View 的父容器),因此首先从Activity的dispatchTouchEvent开始分析。

 public boolean dispatchTouchEvent(MotionEvent ev) {
            // 一般事件列开始都是DOWN按下事件
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                   onUserInteraction();
            }   
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
                // 若getWindow().superDispatchTouchEvent(ev)返回true,则Activity.dispatchTouchEvent()就返回true
                // 整个事件循环就结束了,否则返回为false意味着没人能处理,那么就会继续往下调用Activity.onTouchEvent方法
            }
            return onTouchEvent(ev);
}

注释中说的很清楚了,首先事件交给Activity 所附属的Window 进行分发,如果返回为true,整个事件循环就结束了,否则Activity.onTouchEvent方法会被执行。

那么Window是如何将事件传递给ViewGroup 的呢?在理解这个问题之前先要知道一个事,Window其实是个抽象类,它里面的superDispatchTouchEvent方法是个抽象方法,因此必须找到它的实现类,这个实现类就是我们上面说的PhoneWindow,也就是此处的Window类对象实际上指的是PhoneWindow类对象(多态),所以我们直接去看PhoneWindow的superDispatchTouchEvent方法:

 public boolean superDispatchTouchEvent(MotionEvent event) {
        // mDecor是顶层View(DecorView)的实例对象
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到PhoneWindow 将事件直接传递给了前面说到过的DecorView,DecorView类是PhoneWindow类的一个内部类,它继承自FrameLayout,是所有界面的父类,而FrameLayout是ViewGroup的子类,所以DecorView的间接父类是ViewGroup。之后的过程就和前面说的事件分发一样了,会先调用ViewGroup的dispatchTouchEvent方法:

 public boolean superDispatchTouchEvent(MotionEvent event) {
        // 调用父类的方法即ViewGroup的dispatchTouchEvent方法,将事件传递到ViewGroup去处理
        return super.dispatchTouchEvent(event);    
    }

点击事件达到顶级View (一般是ViewGroup) 以后,会调用ViewGroup 的dispatchTouchEvent方法,那么接上文就看下ViewGroup的dispatchTouchEvent方法,这里直接看伪代码,详细可以参考

public boolean dispatchTouchEvent(MotionEvent event) {
        if(onInterceptTouchEvent(event)){//是否拦截
            return onTouchEvent(event);
        }
        //没有拦截
        if(child==null){
            //没有子控件
            return onTouchEvent(event);
        }else{
             //执行子控件的dispatchTouchEvent
            boolean consume= child.dispatchTouchEvent(event);
            if(!consume){//子控件没有消费事件,执行当前view的onTouchEvent
                return onTouchEvent(event);
            }else{
                return false;
            }
        }
    }

大致逻辑和前面说的一样:如果顶级ViewGroup 拦截事件即onInterceptTouchEvent返回为值true, 那么事件就直接由ViewGroup 处理,这时如果ViewGroup 的mOnTouchListener被设置,则onTouch会首先被调用,否则onTouchEvent会被调用。如果顶级ViewGroup的onInterceptTouchEvent返回值为false即它不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级ViewGroup是一致的,如此循环,完成整个事件的分发。

那么接下来就直接看子View的dispatchTouchEvent方法,当然这里包括前面对于不同安卓版本来说代码是不同的,只是看下大致的思想:

 public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        ...... 
        if (onFilterTouchEventForSecurity(event)) {
            ......
            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;
    }

从上面可以大致了解View对事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用。如果没有设置OnTouchListener或者onTouch方法返回false,那么onTouchEvent就会被调用,我们直接看View的onTouchEvent方法中对于点击事件的处理:

  public boolean onTouchEvent(MotionEvent event) {  
    ......
    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
                switch (event.getAction()) { 
                    // 若当前的事件为抬起View(主要分析)
                    case MotionEvent.ACTION_UP:  
                        boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                            ...// 种种判断
                            performClick();  
                            break;  

                    // 若当前的事件为按下View
                    case MotionEvent.ACTION_DOWN:  
                        if (mPendingCheckForTap == null) {  
                            mPendingCheckForTap = new CheckForTap();  
                        }  
                        mPrivateFlags |= PREPRESSED;  
                        mHasPerformedLongPress = false;  
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                        break;  

                    //若当前的事件为结束事件
                    case MotionEvent.ACTION_CANCEL:  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                        removeTapCallback();  
                        break;

                    // 若当前的事件为滑动View
                    case MotionEvent.ACTION_MOVE:  
                        final int x = (int) event.getX();  
                        final int y = (int) event.getY();  
        
                        int slop = mTouchSlop;  
                        if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                                (y < 0 - slop) || (y >= getHeight() + slop)) {  
                            removeTapCallback();  
                            if ((mPrivateFlags & PRESSED) != 0) {  
                                removeLongPressCallback();  
                                mPrivateFlags &= ~PRESSED;  
                                refreshDrawableState();  
                            }  
                        }  
                        break;  
                }  
                // 若该控件可点击,就一定返回true
                return true;  
            }  
             // 若该控件不可点击,就一定返回false
            return false;  
        }


    public boolean performClick() {  
        if (mOnClickListener != null) {  
            playSoundEffect(SoundEffectConstants.CLICK);  
            mOnClickListener.onClick(this);  
            return true;  
        }  
        return false;  
    }  

当ACTION_ UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick 方法内部会调用它的onClick方法。大致流程就是这样,具体的可以去看源码深入分析。

Window

上面在介绍事件分发顺序的时候提到了Window,我们来学习下相关概念。如下图所示,每个Activity 都包含一个 Window对象,在Android中Window对象通常由PhoneWindow来实现。PhoneWindow 将一个DecorView 设置为整个应用窗口的根View 。DecorView作为窗口界面的顶层视图,封装了一些窗口操作的通用方法。可以说,DecorView将要显示的具体内容呈现在了PhoneWindow上, 这里面的所有View 的监听事件,都通过WindowManagerService来进行接收,并通过Activity 对象来回调相应的onClickListener。在显示上,它将屏幕分成两部分,一个是TitleView, 另一个是ContentView。 看到这里,大家一定看见了一个非常熟悉的布局ContentView。 它是一个id为content 的Framelayout,activity_ main.xml 就是设置在这样一个 Framelayout 里。Android中通过在Activity中使用setContentView()方法来设置一个布局,在调用该方法后,ActivityManagerService会回调onResume()方法, 此时系统才会把整个DecorView 添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制,布局内容就算真正显示出来了,这也是为什么requestWindowFeature(Window.FEATURE_NO_TITLE)方法要在setContentView()方法之前被调用的原因。这里主要介绍Window的几个主要知识点:
在这里插入图片描述

Window是什么

Window是一个窗口的概念,它是一个抽象类,如下:

public abstract class Window {
    public static final int FEATURE_NO_TITLE = 1;
    public static final int FEATURE_CONTENT_TRANSITIONS = 12;
    //...
     public abstract View getDecorView();
     public abstract void setContentView(@LayoutRes int layoutResID);
     public abstract void setContentView(View view);
     public abstract void setContentView(View view, ViewGroup.LayoutParams params);
     public <T extends View> T findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }
    //...
}

可以看到里面有我们熟悉的一些字段和方法,这里就以上面图中Activity对应的Window为例,在Android中Window对象通常由PhoneWindow来实现,PhoneWindow中有一个顶级View-DecorView,继承自FrameLayout,我们可以通过getDecorView()获得它,当我们调用Activity的setContentView时,其实最终会调用Window的setContentView,当我们调用Activity的findViewById时,其实最终调用的是Window的findViewById,这也间接的说明了Window是View的直接管理者。但是由代码也可以看到Window并不是真实存在的,它更多的表示一种抽象的功能集合,View才是Android中的视图呈现形式,绘制到屏幕上的是View不是Window,但是View不能单独存在,它需要依附在Window这个抽象的概念上面。

Window类型

安卓中Window总体可以分为3类:

  • 应用Window:对应一个Activity,加载Activity由AMS完成
  • 子Window:不能单独存在,需要依附于特定类型的父Window,例如Dialog对话框
  • 系统Window:系统Window不需要对应任何Activity,也不需要有父Window,并且应用程序不能创建系统窗口。例如系统状态栏、Toast

Activity、Window、View之间的关系

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
       ActivityClientRecord r = mActivities.get(token);
       ...
       //在这里执行performResumeActivity的方法中会执行Activity的onResume()方法
        r = performResumeActivity(token, clearHide, reason);
       ...
       if (r.window == null && !a.mFinished && willBeVisible) {
          //获得当前Activity的PhoneWindow对象
          r.window = r.activity.getWindow();
          //获得当前phoneWindow内部类DecorView对象
          View decor = r.window.getDecorView();
          //设置窗口顶层视图DecorView可见度
          decor.setVisibility(View.INVISIBLE);
          //获取ViewManager对象,这里getWindowManager()实质上获取的是ViewManager的子类对象WindowManager
          ViewManager wm = a.getWindowManager();
          ...
          //获取ViewRootImpl对象
          ViewRootImpl impl = decor.getViewRootImpl();
           ...
          }
          if (a.mVisibleFromClient) {
              if (!a.mWindowAdded) {
                   //标记根布局DecorView已经添加到窗口
                   a.mWindowAdded = true;
                   //在这里WindowManager将DecorView添加到PhoneWindow中
                   wm.addView(decor, l);
                   } 
                   ...
          }
          ...
    }

还是这个方法,Android中通过在Activity中使用setContentView()方法来设置一个布局,在调用该方法后,ActivityManagerService会回调onResume()方法, 此时系统才会把整个DecorView 添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制。Window通过WindowManager的addView()、removeView()、updateViewLayout()对View进行管理(WindowManager继承自ViewManager,这三个方法定义在ViewManager中,因此上面代码中是ViewManager wm = a.getWindowManager)。

Window、WindowManager、WindowManagerService之间的关系

紧接着WindowManager继续看,WindowManager的具体实现类其实是WindowManagerImp,我们看一下相应方法的实现:

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;
    //...
    
    private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }
    
      @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        //...
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        //...
        mGlobal.updateViewLayout(view, params);
    }
    
     @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }
}

可以看到WindowManagerImp也没有做什么,它把addView等3个方法的操作都委托给了WindowManagerGlobal这个单例类,继续WindowManagerGlobal类的addView方法:

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow){
    //...
    ViewRootImpl root;
    root = new ViewRootImpl(view.getContext(), display);//注释1
    //...
    root.setView(view, wparams, panelParentView);
}

又调用了ViewRootImpl的root.setView()方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
  	//...	
    //这里会进行View的绘制流程
    requestLayout();
     //...
    //通过session与WMS建立通信
     res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(),
                                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                mAttachInfo.mOutsets, mInputChannel);
    //...
}

requestLayout这里就是View的measure、layout、draw流程,之后在mWindowSession的addToDisplay中通过Binder与WMS进行跨进程通信,请求显示窗口上的视图,至此View就会显示到屏幕上。简单来说,WindowManager最后将具体的实现交给了WMS处理。

Activity/Dialog创建过程

这里以Activity、Dialog Window 创建过程为例:

  • Dialog Window创建和Activity Window类似,同样是通过PolicyManager.makeNewWindow() 来实现。
  • 初始化DecorView并将Dialog的视图添加到DecorView中去。和Activity类似,同样是通过Window.setContentView() 来实现。
  • 将DecorView添加到Window中显示。和Activity一样,都是在自身要出现在前台时才会将添加Window。
    - Dialog.show() 方法:完成DecorView的显示。
    - WindowManager.remoteViewImmediate() 方法:当Dialog被dismiss时移除DecorView。

参考:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值