Handler源码解析

首先先让我们来了解下handler中常用的一些方法:
1.createAsync(Looper looper, Handler.Callback callback) createAsync(Looper looper)
创建一个新的处理程序,它所发布的消息和运行表不会受到同步障碍的影响,比如显示vsync。
2.dispatchMessage(Message msg)
分发消息
3.handleMessage(Message msg)
接收消息
4.hasCallbacks(Runnable r)
检查消息队列中是否有任何回调r的消息挂起
5.hasMessages(int what) hasMessages(int what, Object object)
检查消息队列中是否有任何代码为“what”的消息挂起。
6.obtainMessage()
从Message pool中获取一个message对象
7.post(Runnable r)
将可运行的r添加到消息队列中
8.postAtTime(Runnable r, long uptimeMillis) postAtTime(Runnable r, Object token, long uptimeMillis)
在某个时间执行runnable
9.postDelayed(Runnable r, long delayMillis) postDelayed(Runnable r, Object token, long delayMillis)
当前时间延迟delayMillis个毫秒后执行封装runnable
10.removeCallbacks(Runnable r) removeCallbacks(Runnable r, Object token)
移除队列中的runnable
11.removeCallbacksAndMessages(Object token)
当传入的参数为null时,则移除所有的callbacks和messages
12.removeMessages(int what) removeMessages(int what, Object object)
MessageQueue将Message.what与函数传入的what相同的Message从队列中移除
13 sendEmptyMessage(int what)
发送一个仅包含what的message
14.sendEmptyMessageAtTime(int what, long uptimeMillis)
指定时间发送仅包含what的message
15.sendEmptyMessageDelayed(int what, long delayMillis)
延迟发送仅包含what的message
16.sendMessage(Message msg)
发送消息
17.sendMessageAtFrontOfQueue(Message msg)
立即发送Message到队列,而且是放在队列的最前面
18postAtFrontOfQueue(Runnable r)
立即发送Runnable的Message到队列,而且是放在队列的最前面
19.sendMessageAtTime(Message msg, long uptimeMillis)
指定时间发送消息
20.sendMessageDelayed(Message msg, long delayMillis)
延时发送消息

接下先从Handler构造方法开始,无参数的构造方法也只是在构造方法内调用两个参数构造方法


 public Handler() {
        this(null, false);
    }


 public Handler(@Nullable Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;  获取Looper内的 MessageQueue对象
        mCallback = callback;
        mAsynchronous = async;
    }


 @UnsupportedAppUsage
    final Looper mLooper;
    final MessageQueue mQueue;
    @UnsupportedAppUsage
    final Callback mCallback;
    final boolean mAsynchronous;
    @UnsupportedAppUsage
    IMessenger mMessenger;

每一个handler都需要一个Looper来从消息队列中不停循环取出消息,所以在handler构造方法内通过
mLooper = Looper.myLooper();获取一个Looper

myLooper()是Looper类的一个静态方法

public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

此处sThreadLocal为Looper类的一个静态常量ThreadLocal

 @UnsupportedAppUsage
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

sThreadLocal.get();方法是ThreadLocal类的一个方法

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

Looper 介绍
不知你有没有思考过一个问题,启动一个 Java 程序的入口函数是 main 方法,但是当 main 函数执行完毕之后此程序停止运行,也就是进程会自动终止。但是当我们打开一个 Activity 之后,只要我们不按下返回键 Activity 会一直显示在屏幕上,也就是 Activity 所在进程会一直处于运行状态。实际上 Looper 内部维护一个无限循环,保证 App 进程持续进行。

ActivityThread 的 main 方法是一个新的 App 进程的入口

  public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

        // Install selective syscall interception
        AndroidOs.install();

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);

        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();-----------------------1 调用Looper类prepareMainLooper()方法初始化 Looper类是一个常量类-----------------
        
        // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
        // It will be in the format "seq=114"
        long startSeq = 0;
        if (args != null) {
            for (int i = args.length - 1; i >= 0; --i) {
                if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                    startSeq = Long.parseLong(
                            args[i].substring(PROC_START_SEQ_IDENT.length()));
                }
            }
        }
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();-----------------------------2------------------------------------------------------------------

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

代码1处对当前进程进行初始化Looper对象
代码 2 处调用 Looper 的 loop 方法开启无限循环。

先来看下prepareMainLooper方法内代码

public static void prepareMainLooper() {
		// //设置不允许退出的Looper
        prepare(false);---------------------------------1----------------------------------------------
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();----------------------------2----------------------------------------------
        }
    }

代码1处调用prepare()创建 Looper 对象 并在Looper构造方法内创建一个MessageQueue
代码2处调用 sMainLooper = myLooper();获取Looper对象


 private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
        	//创建前先判断当前线程是否有一个Looper   确保在一个线程中 只能有一个Looper
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }


 private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

set方法首先获取到了当前的线程,然后获取一个map。这个map是以键值对形式存储内容的。如果获取的map为空,就创建一个map。如果不为空就塞进去值。要注意的是,这里面的key是当前的线程,这里面的value就是Looper。也就是说,线程和Looper是一一对应的。也就是很多人说的Looper和线程绑定了,其实就是以键值对形式存进了一个map中。
然后在sThreadLocal.set()内new了一个Looper并绑定到当前线程中

sMainLooper = myLooper() 的myLooper()方法内获取当前线程Looper

 public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

Looper 的任务
用一句话总结 Looper 做的事情就是:不断从 MessageQueue 中取出 Message,然后处理 Message 中指定的任务。

在初始化完成之后,在 ActivityThread 的 main 方法中,还调用了 Looper.loop 方法开启无限循环,Looper 的主要功能就是在这个循环中完成的。

/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        //  //拿到Looper的消息队列
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        boolean slowDeliveryDetected = false;

        for (;;) {
            Message msg = queue.next(); // might block  --------------------------------1------------------------------------
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            // Make sure the observer won't change while processing a transaction.
            final Observer observer = sObserver;

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            Object token = null;
            if (observer != null) {
                token = observer.messageDispatchStarting();
            }
            long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
            try {
                msg.target.dispatchMessage(msg);------------------------------------------2-----------------------------------------------------------
                if (observer != null) {
                    observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } catch (Exception exception) {
                if (observer != null) {
                    observer.dispatchingThrewException(token, msg, exception);
                }
                throw exception;
            } finally {
                ThreadLocalWorkSource.restore(origWorkSource);
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();//Looper在分发结束以后,会将用完的消息回收掉,并添加到回收池里。
        }
    }

可以看出loop方法内执行了一个死循环

代码1 处不断地调用 MessageQueue 的 next 方法取出 Message。message 若不为 空, 就执行代码2 处代码,从 Message 中取出 target 对象,这个traget就是发送这个message的Handler,然后调用其 dispatchMessage 方法分发 Message 。

 /**
     * Handle system messages here.
     */
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {//msg.callback  异步消息callback
            handleCallback(msg);
        } else {
            if (mCallback != null) {//同步消息callback
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
            
        }
    }


/**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(@NonNull Message msg) {
    }

消息会先分发给Meesgae的callback,我们没有定义这个callback,那我们接下来看,还有一个mCallback。这个mCallback是创建Handler的时候可以选择传一个CallBack回调,里面依然是handleMessage方法。也就是说你可以自定义一个类继承Handler,重写handleMessage方法,也可以直接new一个Handler传一个回调。

在dispatchMessage内调用了一个空方法的 handleMessage(msg),这个 handleMessage(msg)就是我们创建handler是需要重写的方法

接下来我们看下Handler的sendMessage方法

 public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }
    
 public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

 public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

经过一系列调用后,调用handler内 enqueueMessage方法将message插入消息队列

 private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;-------------------------------------------------1------------------------------------------------
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {//判断是否是异步
            msg.setAsynchronous(true); //设置为异步消息
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
==============================================================================================================
//常量类 MessageQueue内的enqueueMessage方法
 boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");----------------------------2----------------------------------
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;  //拿到队列头部
            boolean needWake;
            -----------------3如果消息不需要延时,或者消息的执行时间比头部消息早,插到队列头部-------------------------------
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
				//消息插到队列中间
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

在enqueueMessage方法内最终调用常量类MessageQueue内的enqueueMessage
在代码1 处,将 Handler 自身设置为 Message的target 对象。因此后续 Message 会调用此 Handler 的 dispatchMessage 来处理;
代码2 处会判断如果 Message 中的 target 为空,则直接抛出异常;
代码 3 处会按照 Message 的时间 when 来有序得插入 MessageQueue 中,可以看出 MessageQueue 实际上是一个有序队列,只不过是按照 Message 的执行时间来排序。

Handler 的 post(Runnable)

 public final boolean post(@NonNull Runnable r) {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

 private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

post(Runnable) 会将 Runnable 赋值到 Message 的 callback 变量中,那么这个 Runnable 是在什么地方被执行的呢?Looper 从 MessageQueue 中取出 Message 之后,会调用 dispatchMessage 方法进行处理,再看下其实现:

public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);                          ------------------1--------------------------
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }


 private static void handleCallback(Message message) {
        message.callback.run();
    }

如果 Message 的 Callback 不为 null,一般为通过 post(Runnabl) 方式,会直接执行 Runnable 的 run 方法。因此这里的 Runnable 实际上就是一个回调接口,跟线程 Thread 没有任何关系。
如果 Message 的 Callback 为 null,这种一般为 sendMessage 的方式,则会调用 Handler 的 hanlerMessage 方法进行处理。

Handler之同步屏障机制(sync barrier)

Handler中的Message可以分为两类:同步消息、异步消息。消息类型可以通过以下函数得知

public boolean isAsynchronous() {
    return (flags & FLAG_ASYNCHRONOUS) != 0;
}

一般情况下这两种消息的处理方式没什么区别,只有在设置了同步屏障时才会出现差异。

同步屏障可以通过MessageQueue.postSyncBarrier函数来设置

private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

可以看到,该函数仅仅是创建了一个Message对象并加入到了消息链表中。乍一看好像没什么特别的,但是这里面有一个很大的不同点是该Message没有target。

我们通常都是通过Handler发送消息的,Handler中发送消息的函数有post***、sendEmptyMessage***以及sendMessage***等函数,而这些函数最终都会调用enqueueMessage函数

//Handler.java
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    //...
    return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到enqueueMessage为msg设置了target字段。

所以,从代码层面上来讲,同步屏障就是一个Message,一个target字段为空的Message。

2 同步屏障的工作原理
同步屏障只在Looper死循环获取待处理消息时才会起作用,也就是说同步屏障在MessageQueue.next函数中发挥着作用。

next函数我们在 Handler工作原理源码解析 中曾经分析过,只不过由于该机制并不影响Handler整体工作流程因此没有展开讲,下面我将相关代码重新贴出来

Message next() {
    //...
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        //...

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {//碰到同步屏障
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                // do while循环遍历消息链表
                // 跳出循环时,msg指向离表头最近的一个异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    //...
                } else {
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        //将msg从消息链表中移除
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    //返回异步消息
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            //...
        }

        //...
    }
}

可以看到,当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息。

3 如何发送异步消息
通常我们使用Handler发消息时,这些消息都是同步消息,如果我们想发送异步消息,那么在创建Handler时使用以下构造函数中的其中一种(async传true)

public Handler(boolean async);
public Handler(Callback callback, boolean async);
public Handler(Looper looper, Callback callback, boolean async);

然后通过该Handler发送的所有消息都会变成异步消息

4 同步屏障的应用
Android应用框架中为了更快的响应UI刷新事件在ViewRootImpl.scheduleTraversals中使用了同步屏障

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //设置同步障碍,确保mTraversalRunnable优先被执行
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //内部通过Handler发送了一个异步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

mTraversalRunnable调用了performTraversals执行measure、layout、draw
为了让mTraversalRunnable尽快被执行,在发消息之前调用MessageQueue.postSyncBarrier设置了同步屏障

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值