Android 探索消息处理机制

《Android系统源代码情景分析》 参考博客

Android系统主要通过MessageQueue,Looper,Handler三个类来实现Android应用程序的消息处理机制.

  • MessageQueue:用来描述消息队列
  • Looper:用来创建消息队列以及进入消息循环
  • Handler类用来发送消息和处理消息
概述
  1. Android 应用程序的消息处理机制不仅可以在Java中用,也可以在C++中用.Java层中的Looper类和MessageQueue类是通过C++层的Looper类和NativeMessageQueue类实现的.
  2. Looper类中的成员变量mQueue,指向的是MessageQueue对象
public class Looper {
     final MessageQueue mQueue;
}
  1. C++中,NativeMessageQueue类中成员变量mLooper指向了C++层的Looper对象.
// android_os_MessageQueue.cpp
class NativeMessageQueue {
private: sp<Looper> mLooper;
};
  1. Java层中的MessageQueue对象中成员变量mPtr,它保存了C++层中的一个NativeMessageQueue对象的地址值,这样Java中的MessageQueue对象与C++中的NativeMessageQueue对象关联起来了.
  2. Java层中的MessageQueue对象中成员变量mMessages,它用来描述一个消息队列.
public class MessageQueue {
    Message mMessages;// 描述消息队列
    private int mPtr; // C++层中NativeMessageQueue对象的地址值
}
  1. C++层中的Looper对象中mWakeReadPipeFd,mWakeWritePipeFd变量,分别用来描述管道读端文件描述符和管道写端文件描述符.
  • 当线程的消息队列没有消息需要处理时,它会在管道读端文件描述符上进行睡眠等待
  • 其他线程通过这个管道写端文件描述符类唤醒它为止.
Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks),
        mResponseIndex(0) {
    mWakeReadPipeFd = wakeFds[0];// 所创建管道读端文件描述符
    mWakeWritePipeFd = wakeFds[1];// 管道写端描述符
}
  1. Java层调用Looper.prepare()或者Looper.prepareMainLooper()会为线程创建Looper对象与MessageQueue对象,在创建Java层MessageQueue对象过程中,会调用MessageQueue. nativeInit(),这样会在C++层中创建NativeMessageQueue与Looper对象.在创建C++层Looper对象时会创建一个管道,这些管道的读端文件描述符和写端文件描述符就保存在Looper对象的成员变量mWakeReadPipeFd,mWakeWritePipeFd中.
创建消息队列
  1. Looper.java中的prepare()prepareMainLooper().
  • 如果当前线程对应的Looper对象不存在, 那么就会创建一个新的Looper对象, 从而创建一个新的MessageQueue对象.每个线程可以对应唯一的Looper对象,对应唯一的MessageQueue对象.
public class Looper {
    // 可将他简单类比为HashMap,线程id为key,Looper对象为值.不通线程通过`get()`就可以获取该线程当初存入其中的Looper对象.
    private static final ThreadLocal sThreadLocal = new ThreadLocal();
    final MessageQueue mQueue;
    private static Looper mMainLooper = null;
    // 创建Looper对象并存与当前线程关联
    public static final void prepare() {
        if (sThreadLocal.get() != null) {
            // 规定每个线程只能有一个Looper对象
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 将Looper对象与当前线程关联
        sThreadLocal.set(new Looper());
    }
    
    public static final void prepareMainLooper() {
        // 为当前线程创建looper对象并与当前线程关联
        prepare();
        // 设置主线程Looper对象.
        setMainLooper(myLooper());
        ...
    }
    // 设置主线程的Looper对象
    private synchronized static void setMainLooper(Looper looper) {
        mMainLooper = looper;
    }
    // 
    public synchronized static final Looper getMainLooper() {
        return mMainLooper;
    }
    // 当前哪个线程调用myLooper()就返回当前线程对应的Looper对象
    public static final Looper myLooper() {
        return (Looper)sThreadLocal.get();
    }
    // 构造方法
    private Looper() {
        // 创建MessageQueue对象
        mQueue = new MessageQueue();
        mRun = true;
        mThread = Thread.currentThread();
    }
}
  1. 创建Java层MessageQueue对象
public class MessageQueue {
    private int mPtr;
    private native void nativeInit();
    // MessageQueue对象创建过程中会调用nativeInit()
    MessageQueue() {
        nativeInit();
    }
}
// android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativeInit(JNIEnv* env, jobject obj) {
    // 创建一个NativeMessageQueue对象
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    ...
    // 这里主要是将NativeMessageQueue对象地址值赋值给MessageQueue对象的mPtr变量.
    android_os_MessageQueue_setNativeMessageQueue(env, obj, nativeMessageQueue);
}
static struct {
    jclass clazz;// 这里就是Java层的MessageQueue类型
    jfieldID mPtr;// MessageQueue对象中变量mPtr值代表的就是NativeMessageQueue对象的内存地址值
} gMessageQueueClassInfo;
static void android_os_MessageQueue_setNativeMessageQueue(JNIEnv* env, jobject messageQueueObj,NativeMessageQueue* nativeMessageQueue) {
    // 将NativeMessageQueue对象的内存地址值复制给MessageQueue对象的mPtr变量.
    env->SetIntField(messageQueueObj, gMessageQueueClassInfo.mPtr,reinterpret_cast<jint>(nativeMessageQueue));
}
  1. NativeMessageQueue对象的创建
// android_os_MessageQueue.cpp
class NativeMessageQueue {
...
private: sp<Looper> mLooper; // 这里就是创建NativeMessageQueue对象时候创建的Looper对象
};

NativeMessageQueue::NativeMessageQueue() {
    // 检查当前线程是否存在C++层Looper对象
    mLooper = Looper::getForThread();
    // 如果不存在
    if (mLooper == NULL) {
        // 创建Looper对象
        mLooper = new Looper(false);
        // 将当前线程与Looper对象关联起来.
        Looper::setForThread(mLooper);
    }
}
  1. Looper对象的创建
  • 当一个线程没有新消息需要处理时,它就会睡眠在这个管道的读端文件描述符上(UI线程消息循环为何不会ANR终于有点眉目了).当其他线程向这个线程消息队列发送消息之后,其他线程会通过这个管道的写端文件描述符往这个管道写数据,从而将唤醒该线程,让它处理刚收到的消息.
// Looper.cpp
Looper::Looper(bool allowNonCallbacks) : mAllowNonCallbacks(allowNonCallbacks), mResponseIndex(0) {
    int wakeFds[2];
    // 创建一个管道
    int result = pipe(wakeFds);
    // 管道读端描述符
    mWakeReadPipeFd = wakeFds[0];
    // 管道写端描述符
    mWakeWritePipeFd = wakeFds[1];
    ...
#ifdef LOOPER_USES_EPOLL
    ...
    // epoll 是用来监听管道的
    struct epoll_event eventItem;
    memset(& eventItem, 0, sizeof(epoll_event));
    eventItem.events = EPOLLIN;
    eventItem.data.fd = mWakeReadPipeFd;
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance.  errno=%d",
            errno);
    ...
MessageQueue消息循环
  1. 开启队列循环
public class Looper {
    public static final void loop() {
        // 获取当前线程对应的Looper对象
        Looper me = myLooper();
        // 获取当前线程对应的MessageQueue对象
        MessageQueue queue = me.mQueue;
        ...
        while (true) {
            // 如果有消息msg将不等于null
            // 如果没有消息当前线程就会进入睡眠状态
            Message msg = queue.next();
            ...
            if (msg != null) {
                ...
            }
        }
    }
}
消息读取
  1. MessageQueue中获取消息并返回
public class MessageQueue {
        Message mMessages;
    final Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        // 如果是0表示线程不需要进入睡眠.
        // 如果是-1表示线程需要一直处于睡眠状态,直到向MessageQueue中发送消息.
        // 如果是大于0的其他值,表示线程睡眠时间,过了这个时间线程就会醒来.
        int nextPollTimeoutMillis = 0;
    
        for (;;) {
            ...
            // 阻塞操作,用于提取管道中的消息,方便接收下一次
            nativePollOnce(mPtr, nextPollTimeoutMillis);
            synchronized (this) {
                ...
                final Message msg = mMessages;
                if (msg != null) {
                    final long when = msg.when;
                    // 如果现在的时间要大于消息处理的时间, 
                    // 那么就将该消息马上返回处理掉.
                    // 并且将变量mMessages赋值上下一个Message对象.
                    if (now >= when) {
                        mBlocked = false;
                        mMessages = msg.next;
                        msg.next = null;
                        return msg;
                    } else {
                        // 如果该消息处理时间还未到的话, 那么就将消息处理等待时间和默认等待最大时间取一个最小值.
                        // 这个最小时间就是代表了线程需要睡多久后自动醒来执行取出消息的标识了.
                        nextPollTimeoutMillis = (int) Math.min(when - now, Integer.MAX_VALUE);
                    }
                } else {
                    // 如果没有消息的话就让线程一直睡眠下去,直到有向MessageQueue中发送消息, 线程会被唤醒
                    nextPollTimeoutMillis = -1;
                }
                ...
            }
        }
    }
}
  1. nativePollOnce()剖析
public class MessageQueue {
    // 是一个Native方法,
    // timeoutMillis 该线程睡眠的时间0不睡眠,-1一直睡眠,大于0需要睡眠多久
    // ptr 这个上面一直在说, 他是C++层的NativeMessageQueue对象内存地址值.
    private native void nativePollOnce(int ptr, int timeoutMillis);
}
// android_os_MessageQueue.cpp
class NativeMessageQueue {
    void NativeMessageQueue::pollOnce(int timeoutMillis) {
        // Looper 对象中的方法
        mLooper->pollOnce(timeoutMillis);
    }
}
// Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        ...
        if (result != 0) {
            ...
            return result;
        }
        // 检查当前线程是否有新消息需要处理,如果有信息消息需要处理pollInner()返回值就不会为0,这样逻辑就到Java层,Java层就开始处理消息了.
        result = pollInner(timeoutMillis);
    }
}
// 这个方法有点长
int Looper::pollInner(int timeoutMillis) {
    ...
    int result = ALOOPER_POLL_WAKE;
    ...
    // 在创建C++层Looper对象时候,创建了一个管道.将该管道读端描述符注册到该epoll对象中,用来监听管道的IO写事件.
    // 如果epoll中这个文件读文件描述符没有监听到写事件,那么该线程将睡眠,睡眠时长由timeoutMillis决定.
    // 如果timeoutMillis是-1则会一直睡眠下去
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ...
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeReadPipeFd) {
            if (epollEvents & EPOLLIN) {
                // 如果在读文件描述符中监听到了写事件的话, 就走这里, 下面来看看这个函数.
                awoken();
            } 
        }
    ...
    }
    return result;
}
// 这个函数最主要的作用就是将当前线程关联的管道中的数据读出来,清理旧数据,
// 这样就可以正常监听该管道的下一次消息写入了.
void Looper::awoken() {
    ...
    char buffer[16];
    ssize_t nRead;
    do {
        nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
    } while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}
消息的发送
  1. Handler创建
// Handler.java
public class Handler {
    final MessageQueue mQueue;
    final Looper mLooper;
    final Callback mCallback;
    public Handler() {
        ...
        // 获取当前线程对应的Looper
        mLooper = Looper.myLooper();
        // 创建Handler对象之前, Looper对象是必须存在的.
        // 这个得注意下.
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        ...
        // 获取当前线程对应的MessageQueue对象
        mQueue = mLooper.mQueue;
        mCallback = null;
    }
     public Handler(Callback callback) {
        mLooper = Looper.myLooper();
        ...
        mQueue = mLooper.mQueue;
        mCallback = callback;
     }

}
  1. 消息发送
// Handler.java
public class Handler {
    final MessageQueue mQueue;
    final Looper mLooper;
    final Callback mCallback;    
    public boolean sendMessageAtTime(Message msg, long uptimeMillis)
    {
        boolean sent = false;
        MessageQueue queue = mQueue;
        if (queue != null) {
            // 将Handler保存在Message对象中,也就是这里容易造成Activity内存泄漏,因为内部类持有外部类引用.
            // Handler对象持有Activity引用,然后将Handler对象放入MessageQueue对象中.
            // 所以一般会让Handler变为静态内部类,静态内部类不持有外部类引用.
            msg.target = this;
            // 将Message对象发送到当前Handler对象得成员变量mQueue所表示得消息队列中去.
            sent = queue.enqueueMessage(msg, uptimeMillis);
        }
        return sent;
    }
}
// MessageQueue.java
public class MessageQueue {
      Message mMessages;
    // 将消息发送到队列中
    final boolean enqueueMessage(Message msg, long when) {
        ... 
        final boolean needWake;
        synchronized (this) {
            ...
            // 将传入得when时间值赋值给传入的Message对象的when变量.
            msg.when = when;
            Message p = mMessages;
            // 如果消息队列mMessages变量为null
            // 传入的时间是0,也就是立马执行
            // 上一个消息的执行时间大于当前消息的执行时间
            // 那么就需要立马唤醒线程
            if (p == null || when == 0 || when < p.when) {
                // Message是一个链表结构,将当前传入的msg对象作为链表头
                msg.next = p;
                // msg对象保存在MessageQueue中的mMessages变量中
                mMessages = msg;
                // 需要唤醒
                needWake = mBlocked; 
            } else {
                // 这里说的就是这个传入事件的时间在Message消息队列中间
                // 这里就是遍历消息队列, 将该消息放入合适的位置
                Message prev = null;
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
                msg.next = prev.next;
                prev.next = msg;
                // 因为这个消息也不需要立马执行, 所以就不需要唤醒线程
                needWake = false; // still waiting on head, no need to wake up
            }
        }
        if (needWake) {
            // 这里是去C++层唤醒当前线程
            nativeWake(mPtr);
        }
        return true;
    }
    // ptr就是C++层对应的NativeMessageQueue对象的内存地址值
    private native void nativeWake(int ptr);
}
  1. 唤醒线程
// android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jobject obj, jint ptr) {
    // ptr就是C++层对应的NativeMessageQueue对象的内存地址值
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    return nativeMessageQueue->wake();
}
// 看看mLooper中
void NativeMessageQueue::wake() {
    mLooper->wake();
}
// Looper.cpp
void Looper::wake() {
    ...
    do {
        // mWakeWritePipeFd 创建Looper对象的时候创建了一个管道写端描述符
        // 这里就是向管道中写一个字符, 然后去唤醒当前线程.
        // 假如现在Looper.loop()运行着,线程A正睡眠在管道的读端描述符,突然线程B向线程A代表的消息队列中发送了一个消息,那么线程A肯定会在读端描述符监听到有消息写入.
        // 这个时候线程A将被唤醒,而MessageQueue.next()将不被nativePollOnce()阻塞,继续向下执行,返回Message到Looper中,然后交由Handler去处理.
        nWrite = write(mWakeWritePipeFd, "W", 1);
    } while (nWrite == -1 && errno == EINTR);
    ...
}
消息处理
  1. 拿到消息
// MessageQueue.java
public class MessageQueue {
    final Message next() {
        for (;;) {
            ...
            // 阻塞,线程会睡眠
            nativePollOnce(mPtr, nextPollTimeoutMillis);
            synchronized (this) {
                final Message msg = mMessages;
                if (msg != null) {
                    final long when = msg.when;
                    if (now >= when) {
                        mBlocked = false;
                        mMessages = msg.next;
                        msg.next = null;
                        // 返回消息
                        return msg;
                    } else {
                        nextPollTimeoutMillis = (int) Math.min(when - now, Integer.MAX_VALUE);
                    }
                } else {
                    nextPollTimeoutMillis = -1;
                }
                
            }
            ...
        }
    }
}
// Looper.java
public class Looper {
    public static final void loop() {
        Looper me = myLooper();
        MessageQueue queue = me.mQueue;
        
        while (true) {
            // 这里会被阻塞,线程唤醒后拿到消息了
            Message msg = queue.next(); // might block
            if (msg != null) {
                ...
                // 使用Handler中的dispatchMessage()进行消息分发.
				// 这里其实间接解释了一个问题,Handler的线程切换.
				// UI线程创建了Looper对象,当子线程中创建的Handler对象内部持有UI线程创建的Looper对象时,该子线程创建的Handler发送的消息将被添加到UI Looper中,
				// 在来看代码,此时UI Looper拿到消息(它处于UI线程)调用子线程Handler对象的dispatchMessage(msg),
				// 然后dispatchMessage(msg)方法以后的逻辑就在UI线程中执行了,也就完成了线程的切换.
                msg.target.dispatchMessage(msg);
                ...
                msg.recycle();
            }
        }
    }
}
  1. 消息分发
public class Handler {
    final MessageQueue mQueue;
    final Looper mLooper;
    final Callback mCallback;
    public Handler(Callback callback) {
        ...
        mLooper = Looper.myLooper();
        ..
        mQueue = mLooper.mQueue;
        // mCallback对象
        mCallback = callback;
    }


    private final Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        // Message对象的callback变量就是一个Runnable对象
        m.callback = r;
        return m;
    }

    public void dispatchMessage(Message msg) {
        // 如果这个Message对象的callback变量不为空,
        // 也就是这个Runnable对象存在
        if (msg.callback != null) {
            // 消息走这里
            handleCallback(msg);
        } else {
            // 假如mCallback对象存在的话
            if (mCallback != null) {
                 // 消息走这里
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            // 否则就由最常见的handleMessage方法处理.
            handleMessage(msg);
        }
    }
    // 这里就是Runnable对象存在的情况,消息会走这里
    private final void handleCallback(Message message) {
        // 直接调用Runnable对象的run()
        message.callback.run();
    }
    // 这里就是Runnable对象是空, 但是mCallback对象不为空,消息会走这里,当然这个得由自己实现
    public interface Callback {
        public boolean handleMessage(Message msg);
    }
    // 这个方法太常用了, Runnable对象是空,mCallback对象是空,最后就走这里了.
    public void handleMessage(Message msg) {
    }
}
// Runnable.java
public interface Runnable {
    // 只有一个run()
    public void run();
}

上面叙述了太多的东西而且有点散,下面来分析下实际的用例,加深下记忆.

HandlerThread 源码解析
  1. HandlerThread基本使用
  • 创建HandlerThread对象
HandlerThread mHandlerThread = new HandlerThread("handlerThread");
  • 开启线程
mHandlerThread.start();
  • 创建Handler
Handler workHandler = new Handler( handlerThread.getLooper() ) {
            @Override
            public boolean handleMessage(Message msg) {
                ...//消息处理
                return true;
            }
        });
  • 发送消息
Message msg = Message.obtain();
workHandler.sendMessage(msg);
  • 结束,停止消息循环
mHandlerThread.quit();
  1. 源码分析
  • HandlerThread的创建
public class HandlerThread extends Thread {
    public HandlerThread(String name) {
        // 设置线程名称
        super(name);
        // 设置线程优先级
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
}
  • 开启线程,执行线程的run()
public class HandlerThread extends Thread {
    @Override
    public void run() {
        // 获取当前线程所在进程的线程Id
        mTid = Process.myTid();
        // 该方法在Looper类中,下面会分析.
        Looper.prepare();
        synchronized (this) {
            // 下面会分析myLooper()
            mLooper = Looper.myLooper();
            // 这里也很关键,下面分析.
            notifyAll();
        }
        // 设置当前线程优先级
        Process.setThreadPriority(mPriority);
        // 该方法可以重写,设置Looper循环开启前的一些工作
        onLooperPrepared();
        // Looper循环开启
        Looper.loop();
        mTid = -1;
    }
}

下面说下Looper中的两个方法

public final class Looper {
    // 静态的ThreadLocal对象
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    public static void prepare() {
        prepare(true);
    }
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 最终可以知道prepare()方法是为当前线程创建了一个Looper对象,
        // 并且将该对象存入ThreadLocal中,方便该Looper对象与创建它的线程所关联.
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
    public static @Nullable Looper myLooper() {
        // 该方法也很简单,获取当前线程对应的Looper对象.
        return sThreadLocal.get();
    }
}
  • 创建Handler对象
public class Handler {
    // 创建Handler的时候需要传入一个Looper对象,
    // 上一步中通过prepare()方法为HandlerThread线程创建了一个Looper对象了,所以可以将HandlerThread中的Looper对象传入,示例中的代码也是这么写的.
    public Handler(Looper looper) {
        this(looper, null, false);
    }
}
  • 来说说为何HandlerThread中的run()方法会调用notifyAll()呢?
    因为当创建Handler时候需要传入HandlerThread中的Looper对象,该Looper对象是通过HandlerThread.getLooper()获取到的,看下该方法:
public class HandlerThread extends Thread {
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    // 如果线程活着,但是mLooper为null,就需要等待HandlerThread.run()方法跑起来,只有run()方法跑起来之后才能创建Looper对象.
                    // 假如Looper还未创建,调用线程会执行wait(),使自己处于等待阻塞状态,
                    // 之后,HandlerThread.run()执行之后,run()方法中的notifyAll()就会将调用线程唤醒,接着向下执行,
                    // 最后将HandlerThread中的Looper对象返回,然后传入Handler的构造函数中.
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
}

用完了HandlerThread别忘了让当前线程退出消息循环.
总结:

  • 如果要在子线程中开启消息循环机制,
  1. 为当前子线程创建一个Looper对象,也就是让当前线程调用Looper.prepare()
  2. 为当前子线程启动Looper循环,就是让当前线程调用Looper.loop()
  • Handler线程切换原理,
  1. 首先,在子线程中启动Looper循环,也就是子线程调用了Looper.loop().
  2. 使用Handler往子线程中Looper里的MessageQueue对象发送数据.
  3. Looper会通过循环取出该数据,在上面说过,Looper是在子线程中开启循环的,所以取出数据的动作也在子线程中操作.
  4. 数据由Looper取出之后,就会调用Handler中的handleMessage()方法,这时handleMessage()方法还是会由子线程去执行.
  5. 现在假设,Handler是在主线程发送的数据,最后handleMessage()方法是由子线程去执行的,这样就完成了线程的切换.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值