EventBus 3.0源码注释

说明

本文主要是分析EventBus 3.0后,一些知识点总结和用时的注意事项,关于EventBus的源码分析,网上已经有很多了,比如以下两篇:
EventBus 源码解析
EventBus 3.0 源码分析
EventBus源码研读

EventBus应用

实现一个订阅者类,类里面要有订阅者和事件的处理方法

public class ThreadModeFunction {
    public static final String TAG = "ThreadModeFunction";
    public void registerOn(){
        EventBus.getDefault().register(this);
    }
    public void unregisterOff(){
        EventBus.getDefault().unregister(this);
    }
    @Subscribe(threadMode = ThreadMode.POSTING, sticky = true)
    public void onMessage(EventTypeSecond eventTypeSecond) {
        Log.d(TAG, eventTypeSecond.getMsg());
    }
    // Called in Android UI's main thread
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessage4(MessageEvent event) {
        Log.d(TAG, event.message);
    }
}

可以在其他地方发布事件

    public void sendMain(View view){
        count++;
        EventBus.getDefault().post(new MessageEvent("sendMain : "+count));
    }
    public void sendPosting(View view){
        count++;
        EventBus.getDefault().post(new EventTypeSecond("sendPosting : "+count));
    }

事件发布后,可以看到订阅者类里的事件处理方法的相关日志打印。post发布事件的时候传入的参数是事件类型,对应的,事件处理方法传入的参数也是事件类型;订阅者注册时候会通过发射把类里面的所有事件处理方法和事件保存起来,以方便事件发布的时候查找。所以,反射只执行了一次,性能上影响不大。

概念说明
  1. 事件(Event):又可称为消息,本文中统一用事件表示。其实就是一个对象,可以是网络请求返回的字符串,也可以是某个开关状态等等。事件类型(EventType)指事件所属的 Class。
    事件分为一般事件和 Sticky 事件,相对于一般事件,Sticky 事件不同之处在于,当事件发布后,再有订阅者开始订阅该类型事件,依然能收到该类型事件最近一个 Sticky 事件。
  2. 订阅者(Subscriber):订阅某种事件类型的对象。当有发布者发布这类事件后,EventBus 会执行订阅者的 onEvent 函数,这个函数叫事件响应函数。订阅者通过 register 接口订阅某个事件类型,unregister 接口退订。订阅者存在优先级,优先级高的订阅者可以取消事件继续向优先级低的订阅者分发,默认所有订阅者优先级都为 0。
  3. 发布者(Publisher):发布某事件的对象,通过 post 接口发布事件。
ThreadMode,订阅者处理线程的说明
  1. POSTING:默认的 ThreadMode,表示在执行 Post 操作的线程直接调用订阅者的事件响应方法,不论该线程是否为主线程(UI 线程)。当该线程为主线程时,响应方法中不能有耗时操作,否则有卡主线程的风险。适用场景:对于是否在主线程执行无要求,但若 Post 线程为主线程,不能耗时的操作;
  2. MAIN:在主线程中执行响应方法。如果发布线程就是主线程,则直接调用订阅者的事件响应方法,否则通过主线程的 Handler 发送消息在主线程中处理——调用订阅者的事件响应函数。显然,MainThread类的方法也不能有耗时操作,以避免卡主线程。适用场景:必须在主线程执行的操作;
  3. MAIN_ORDERED:在主线程中执行相应方法,事件总是排队的执行,因此,调用Post方法的地方会立即返回,这为事件处理提供了更严格和更一致的顺序。比如说:在MAIN模式中,如果在一个事件A执行的过程中,又post了一个事件B,则事件B先执行完,然后才是事件A执行完;而在MAIN_ORDERED模式中,这种情况会是事件A执行结束后,才是事件B执行。
  4. BACKGROUND:在后台线程中执行响应方法。如果发布线程不是主线程,则直接调用订阅者的事件响应函数,否则启动唯一的后台线程去处理。由于后台线程是唯一的,当事件超过一个的时候,它们会被放在队列中依次执行,因此该类响应方法虽然没有PostThread类和MainThread类方法对性能敏感,但最好不要有重度耗时的操作或太频繁的轻度耗时操作,以造成其他操作等待。适用场景:操作轻微耗时且不会过于频繁,即一般的耗时操作都可以放在这里;
  5. ASYNC:不论发布线程是否为主线程,都使用一个空闲线程来处理。和BackgroundThread不同的是,Async类的所有线程是相互独立的,因此不会出现卡线程的问题。适用场景:长耗时操作,例如网络访问。
整体架构及类关系图

架构
EventBus是订阅者模式,每次通过post发送消息时,所有订阅这个消息的订阅者都会被触发
类关系图
从上图也能大概看出,EventBus的实现比较简单,总共大概二十个文件。EventBus作为核心的类,存储了订阅者的所有信息,实现了register和post的方法,总之,以EventBus作为开始,基本上EventBus的所有流程都能走一遍。

订阅者注册,register

EventBusBuilder类成员说明

/**
 * Creates EventBus instances with custom parameters and also allows to install a custom default EventBus instance.
 * Create a new builder using {@link EventBus#builder()}.
 * 通过初始化了一个EventBusBuilder()对象来分别初始化EventBus的一些配置,当我们在写一个需要自定义配置的框架的时候,这种实现方法非常普遍,将配置解耦出去,使我们的代码结构更清晰
 */
public class EventBusBuilder {
    private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();

    boolean logSubscriberExceptions = true;//监听异常日志
    boolean logNoSubscriberMessages = true;//如果没有订阅者,显示一个log
    boolean sendSubscriberExceptionEvent = true;//发送监听到异常事件
    boolean sendNoSubscriberEvent = true;//如果没有订阅者,发送一条默认事件
    boolean throwSubscriberException;//如果失败,抛出异常
    boolean eventInheritance = true;//event的子类是否也能响应订阅者
    boolean ignoreGeneratedIndex;//是否生成的文件,每次都通过发射
    boolean strictMethodVerification;//是否严格验证事件的处理方法
    ExecutorService executorService = DEFAULT_EXECUTOR_SERVICE;
    List<Class<?>> skipMethodVerificationForClasses;
    List<SubscriberInfoIndex> subscriberInfoIndexes;
    Logger logger;
    MainThreadSupport mainThreadSupport;

EventBus类成员说明

/**
 * EventBus is a central publish/subscribe event system for Android. Events are posted ({@link #post(Object)}) to the
 * bus, which delivers it to subscribers that have a matching handler method for the event type. To receive events,
 * subscribers must register themselves to the bus using {@link #register(Object)}. Once registered, subscribers
 * receive events until {@link #unregister(Object)} is called. Event handling methods must be annotated by
 * {@link Subscribe}, must be public, return nothing (void), and have exactly one parameter
 * (the event).
 *
 * @author Markus Junginger, greenrobot
 */
public class EventBus {

    /** Log tag, apps may override it. */
    public static String TAG = "EventBus";
    //EventBus既可以通过单例获取,也可以通过new获取一个对象,两个的数据互不影响
    static volatile EventBus defaultInstance;
    //配置类,配置项都放在这个类里,有个默认值,可以修改。可以考虑以后写代码的时候加个配置配置类,方便管理
    private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();
    //缓存事件,事件的父类的所有事件。EventBus支持事件类型的继承:比如事件类型A继承事件类型B,
    // 如果发布了事件A,所有注册事件B的订阅者也会得到处理。
    private static final Map<Class<?>, List<Class<?>>> eventTypesCache = new HashMap<>();
    //key:订阅的事件,value:订阅这个事件的所有订阅者集合
    private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
    //key:订阅者对象,value:这个订阅者订阅的事件集合
    private final Map<Object, List<Class<?>>> typesBySubscriber;
    //粘性事件,就是指订阅者注册事件后,会发送一次最近发生的事件。所以,这个类用来保存发送的粘性事件
    //key:粘性事件的class对象, value:事件对象
    private final Map<Class<?>, Object> stickyEvents;
    /**
     * ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,而这段数据是不会与其他线程共享的。
     * 其内部原理是通过生成一个它包裹的泛型对象的数组,在不同的线程会有不同的数组索引值,通过这样就可以做到每个线程通过 get() 方法获取的时候,取到的只能是自己线程所对应的数据。
     在 EventBus 中, ThreadLocal 所包裹的是一个 PostingThreadState 类,它仅仅是封装了一些事件发送中过程所需的数据。
     */
    private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
        @Override
        protected PostingThreadState initialValue() {
            return new PostingThreadState();
        }
    };

    //Eventbus支持Android和Java,这个在Android中,用来标记Android的UI线程
    // @Nullable
    private final MainThreadSupport mainThreadSupport;
    // @Nullable
    //主线程post,获取主线程的loop,通过handler执行
    private final Poster mainThreadPoster;
    //事件 Background 处理,通过Executors.newCachedThreadPool()一个事件一个事件执行。
    private final BackgroundPoster backgroundPoster;
    //事件异步处理
    private final AsyncPoster asyncPoster;
    //订阅者响应函数信息存储和查找类
    private final SubscriberMethodFinder subscriberMethodFinder;
    //ExecutorService是Executor直接的扩展接口,也是最常用的线程池接口
    //默认的值为Executors.newCachedThreadPool()
    private final ExecutorService executorService;

    private final boolean throwSubscriberException;
    private final boolean logSubscriberExceptions;
    private final boolean logNoSubscriberMessages;
    private final boolean sendSubscriberExceptionEvent;
    private final boolean sendNoSubscriberEvent;
    private final boolean eventInheritance;

获取EventBus

    /** Convenience singleton for apps using a process-wide EventBus instance. */
    public static EventBus getDefault() {
        EventBus instance = defaultInstance;
        if (instance == null) {
            synchronized (EventBus.class) {
                instance = EventBus.defaultInstance;
                if (instance == null) {
                    instance = EventBus.defaultInstance = new EventBus();
                }
            }
        }
        return instance;
    }
    /**
     * Creates a new EventBus instance; each instance is a separate scope in which events are     delivered. To use a
     * central bus, consider {@link #getDefault()}.
     */
    public EventBus() {
        this(DEFAULT_BUILDER);
    }
//EvnetBus有两种方式获得,单例和new一个对象,两种方式的数据是相互独立的。

注册方法分析

    /**
     * Registers the given subscriber to receive events. Subscribers must call {@link #unregister(Object)} once they
     * are no longer interested in receiving events.
     * <p/>
     * Subscribers have event handling methods that must be annotated by {@link Subscribe}.
     * The {@link Subscribe} annotation also allows configuration like {@link
     * ThreadMode} and priority.
     */
    public void register(Object subscriber) {
        //首先获得订阅者的class对象
        Class<?> subscriberClass = subscriber.getClass();
        //通过subscriberMethodFinder来找到订阅者订阅了哪些事件.返回一个SubscriberMethod对象的List,SubscriberMethod
        //里包含了这个方法的Method对象,以及将来响应订阅是在哪个线程的ThreadMode,以及订阅的事件类型eventType,以及订阅的优
        //先级priority,以及是否接收粘性sticky事件的boolean值.
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

SubscriberMethodFinder类相关说明

/**
 * 用来查找和缓存订阅者响应函数的信息的类
 */
class SubscriberMethodFinder {
    /*
     * In newer class files, compilers may add methods. Those are called bridge or synthetic methods.
     * EventBus must ignore both. There modifiers are not public but defined in the Java class file format:
     * http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6-200-A.1
     */
    private static final int BRIDGE = 0x40;
    private static final int SYNTHETIC = 0x1000;

    private static final int MODIFIERS_IGNORE = Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC;
    //缓存订阅者相应函数
    private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();

    private List<SubscriberInfoIndex> subscriberInfoIndexes;
    private final boolean strictMethodVerification;
    private final boolean ignoreGeneratedIndex;//忽略注解器生成的MyEventBusIndex类,通过反射获取订阅方法信息

    private static final int POOL_SIZE = 4;
    //做订阅方法的校验和保存,并通过FIND_STATE_POOL静态数组来保存FindState对象,可以使FindState复用,避免重复创建过多的对象
    private static final FindState[] FIND_STATE_POOL = new FindState[POOL_SIZE];

    SubscriberMethodFinder(List<SubscriberInfoIndex> subscriberInfoIndexes, boolean strictMethodVerification,
                           boolean ignoreGeneratedIndex) {
        this.subscriberInfoIndexes = subscriberInfoIndexes;
        this.strictMethodVerification = strictMethodVerification;
        this.ignoreGeneratedIndex = ignoreGeneratedIndex;
    }

//查找处理方法
    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        //如果订阅者已经在缓存里,直接返回
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
        //是否忽略注解器生成的MyEventBusIndex类
        if (ignoreGeneratedIndex) {
            //利用反射来读取订阅类中的订阅方法信息
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            //从注解器生成的MyEventBusIndex类中获得订阅类的订阅方法信息
            subscriberMethods = findUsingInfo(subscriberClass);
        }
        //register所在的类或超类里必须要有处理事件的方法,要不然再次注册的时候会抛异常
        if (subscriberMethods.isEmpty()) {
            throw new EventBusException("Subscriber " + subscriberClass
                    + " and its super classes have no public methods with the @Subscribe annotation");
        } else {
            METHOD_CACHE.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }

保存订阅者信息

 // Must be called in synchronized block, 这里涉及到把数据增加到对应的集合,所以要同步
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
        //订阅者的相关描述
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        //CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,
        // 而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,
        // 再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,
        // 因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            //如果已经注册过,再次注册就会抛异常,所以注册之前要看一下是否已经注册过
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }

        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            //同一个事件,优先级高的订阅者放在前面
            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }
事件发布,post

在处理post的时候,每个线程都会维护一个状态:PostingThreadState,这个类里有个集合用来保存这个线程待处理的消息,每个线程的消息的一个接一个处理的。

post方法

    /** Posts the given event to the event bus. */
    public void post(Object event) {
        //获取当前线程post的一个状态
        PostingThreadState postingState = currentPostingThreadState.get();
        //当前线程维护一个list,每次post被调用后,都会把事件先放入post,一旦开始处理list,
        //会把list的所有事件循环处理完。
        List<Object> eventQueue = postingState.eventQueue;
        eventQueue.add(event);

        if (!postingState.isPosting) {
            postingState.isMainThread = isMainThread();
            postingState.isPosting = true;
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset");
            }
            try {
                //循环处理
                while (!eventQueue.isEmpty()) {
                    postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                postingState.isPosting = false;
                postingState.isMainThread = false;
            }
        }
    }

postSingleEvent说明

    private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
        boolean subscriptionFound = false;
        //如果消息可继承,
        if (eventInheritance) {
            //获取事件相关的所有事件
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            for (int h = 0; h < countTypes; h++) {
                Class<?> clazz = eventTypes.get(h);
                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
        }
        if (!subscriptionFound) {
            if (logNoSubscriberMessages) {
                logger.log(Level.FINE, "No subscribers registered for event " + eventClass);
            }
            if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                    eventClass != SubscriberExceptionEvent.class) {
                post(new NoSubscriberEvent(this, event));
            }
        }
    }

处理所有这个事件的订阅者

    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            //获取事件的所有订阅者
            subscriptions = subscriptionsByEventType.get(eventClass);
        }
        if (subscriptions != null && !subscriptions.isEmpty()) {
            //循环处理订阅者
            for (Subscription subscription : subscriptions) {
                postingState.event = event;
                postingState.subscription = subscription;
                boolean aborted = false;
                try {
                    postToSubscription(subscription, event, postingState.isMainThread);
                    aborted = postingState.canceled;
                } finally {
                    postingState.event = null;
                    postingState.subscription = null;
                    postingState.canceled = false;
                }
                if (aborted) {
                    break;
                }
            }
            return true;
        }
        return false;
    }


private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
        switch (subscription.subscriberMethod.threadMode) {
            case POSTING:
                invokeSubscriber(subscription, event);
                break;
            case MAIN:
                if (isMainThread) {
                    invokeSubscriber(subscription, event);
                } else {
                    mainThreadPoster.enqueue(subscription, event);
                }
                break;
            case MAIN_ORDERED:
                if (mainThreadPoster != null) {
                    mainThreadPoster.enqueue(subscription, event);
                } else {
                    // temporary: technically not correct as poster not decoupled from subscriber
                    invokeSubscriber(subscription, event);
                }
                break;
            case BACKGROUND:
                if (isMainThread) {
                    backgroundPoster.enqueue(subscription, event);
                } else {
                    invokeSubscriber(subscription, event);
                }
                break;
            case ASYNC:
                asyncPoster.enqueue(subscription, event);
                break;
            default:
                throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
        }
    }
post相关类的说明

这里写图片描述

在真正处理事件的时候,是通过一个队列实现的,这个队列的逻辑和单向链表的逻辑是一致的。

PendingPost类:封装了事件,订阅等信息

final class PendingPost {
    //为了避免对象的重复创建,这个集合放置已经处理过的PendingPost
    private final static List<PendingPost> pendingPostPool = new ArrayList<PendingPost>();

    Object event;
    Subscription subscription;
    PendingPost next;//每个对象指向下一个,形成链表

    private PendingPost(Object event, Subscription subscription) {
        this.event = event;
        this.subscription = subscription;
    }

    /**
     * 获取一个PendingPost,如果pendingPostPool里面有,就取一个,没有创建
     * @param subscription
     * @param event
     * @return
     */
    static PendingPost obtainPendingPost(Subscription subscription, Object event) {
        synchronized (pendingPostPool) {
            int size = pendingPostPool.size();
            if (size > 0) {
                PendingPost pendingPost = pendingPostPool.remove(size - 1);
                pendingPost.event = event;
                pendingPost.subscription = subscription;
                pendingPost.next = null;
                return pendingPost;
            }
        }
        return new PendingPost(event, subscription);
    }

    /**
     * 释放PendingPost,并把释放的PendingPost加入到pendingPostPool
     * @param pendingPost
     */
    static void releasePendingPost(PendingPost pendingPost) {
        pendingPost.event = null;
        pendingPost.subscription = null;
        pendingPost.next = null;
        synchronized (pendingPostPool) {
            // Don't let the pool grow indefinitely
            if (pendingPostPool.size() < 10000) {
                pendingPostPool.add(pendingPost);
            }
        }
    }

}

PendingPostQueue:队列

final class PendingPostQueue {
    private PendingPost head;
    private PendingPost tail;

    //新的事件放到消息的尾部
    synchronized void enqueue(PendingPost pendingPost) {
        if (pendingPost == null) {
            throw new NullPointerException("null cannot be enqueued");
        }
        if (tail != null) {
            tail.next = pendingPost;
            tail = pendingPost;
        } else if (head == null) {
            head = tail = pendingPost;
        } else {
            throw new IllegalStateException("Head present, but no tail");
        }
        notifyAll();
    }
    //从队列的头部获取消息
    synchronized PendingPost poll() {
        PendingPost pendingPost = head;
        if (head != null) {
            head = head.next;
            if (head == null) {
                tail = null;
            }
        }
        return pendingPost;
    }

    synchronized PendingPost poll(int maxMillisToWait) throws InterruptedException {
        if (head == null) {
            wait(maxMillisToWait);
        }
        return poll();
    }

}

以BackgroundPoster为例说明

/**
 * Posts events in background.
 *
 * @author Markus
 */
final class BackgroundPoster implements Runnable, Poster {
    //队列
    private final PendingPostQueue queue;
    private final EventBus eventBus;
    //保证只有上个事件处理完,线程池才会处理下个事件,BackgroundPoster是后台执行事件,但是事件是顺序执行的。
    //和AsyncPoster相对比,AsyncPoster会并发执行所有事件
    private volatile boolean executorRunning;

    BackgroundPoster(EventBus eventBus) {
        this.eventBus = eventBus;
        queue = new PendingPostQueue();
    }

    public void enqueue(Subscription subscription, Object event) {
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        synchronized (this) {
            queue.enqueue(pendingPost);
            if (!executorRunning) {
                executorRunning = true;
                //线程池执行
                eventBus.getExecutorService().execute(this);
            }
        }
    }

    @Override
    public void run() {
        try {
            try {
                while (true) {
                    PendingPost pendingPost = queue.poll(1000);
                    if (pendingPost == null) {
                        synchronized (this) {
                            // Check again, this time in synchronized
                            pendingPost = queue.poll();
                            if (pendingPost == null) {
                                executorRunning = false;
                                return;
                            }
                        }
                    }
                    eventBus.invokeSubscriber(pendingPost);
                }
            } catch (InterruptedException e) {
                eventBus.getLogger().log(Level.WARNING, Thread.currentThread().getName() + " was interruppted", e);
            }
        } finally {
            executorRunning = false;
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值