EventBus拾遗—索引

EventBus 是 Android 中基于 观察者模式 实现的 发布-订阅事件总线 第三方开源框架。

其优势如下:

  • 轻量级框架,执行效率高

  • 使用简单,工程代码保持简洁优雅

  • 简化了组件之间的通信,将事件的发送者和接收者解耦

 

一、基础回顾

1.1 简单使用

  • 定义事件
public static class MessageEvent { /* Additional fields if needed */ }
  • 准备订阅方法
@Subscribe(threadMode = ThreadMode.MAIN)  
public void onMessageEvent(MessageEvent event) {/* Do something */};
  • 订阅事件/取消订阅
EventBus.getDefault().register(this);

EventBus.getDefault().unregister(this);
  • 发送事件
EventBus.getDefault().post(new MessageEvent());

1.2 订阅方法格式

  • public方法
  • 非抽象方法
  • 非静态方法
  • 只有一个订阅事件参数

1.3 粘性事件

与普通事件不同,在订阅者注册前,发送了粘性事件,当订阅者注册后,会立即触发粘性事件,这与粘性广播有点像。

1.4 事件的继承

子事件类继承了父事件类。如果子事件和父事件同时被订阅,当发送子事件时,也将会发送父事件,即方法也会被触发。

1.5 避免OOM

由于EventBus默认使用newCachedThreadPool创建线程池,所以在创建EventBus的时候,可通过自定义线程池的方式避免内存问题。

 

二、源码回顾

2.1 关键类

  • SubscriberMethod:封装订阅方法的相关信息(method,threadMode,eventType等)
  • Subscription:二次封装订阅者对象及SubscriberMethod
  • eventType:事件类型的class对象
  • subscriptionsByEventType:HashMap类型
    • key:事件类型的class对象(eventType)
    • value:事件类型对应的Subscription集合
  • typesBySubscriber:HashMap类型
    • key:订阅者对象(subscriber)
    • value:订阅者对象中所有的订阅方法的eventType集合

2.2 Subscribe注解

从 EventBus 3.0 开始,使用Subscribe注解配置事件订阅方法。其代码如下:

// src/org/greenrobot/eventbus/EventBus.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {

    // 指定执行事件订阅方法的线程模式,默认为POSTING
    ThreadMode threadMode() default ThreadMode.POSTING;

    // 是否接收粘性事件,默认为false
    boolean sticky() default false;

    // 指定事件订阅方法的优先级,默认为0
    int priority() default 0;
}

其中,ThreadMode属性有以下几种模式:

  • POSTING:默认的线程模式,发送事件和处理事件在相同的线程,避免了线程切换的损耗。
  • MAIN:在主线程发送事件,则直接在主线程处理事件;否则先将事件入队列,然后通过Handler切换到主线程,依次处理事件。
  • MAIN_ORDERED:无论在哪个线程发送事件,都先将事件入队列,然后通过Handler切换到主线程,依次处理事件。
  • BACKGROUND:在主线程发送事件,则先将事件入队列,然后通过线程池,依次处理事件;否则发送事件和处理事件在相同的线程。
  • ASYNC:无论在哪个线程发送事件,都先将事件放入队列,然后通过线程池,依次处理事件。

2.3 订阅事件

首先,第一次执行EventBus类的getDefault单例方法时,在其构造方法中完成了相关属性的初始化:

// src/org/greenrobot/eventbus/EventBus.java

EventBus(EventBusBuilder builder) {
    logger = builder.getLogger();
    subscriptionsByEventType = new HashMap<>();
    typesBySubscriber = new HashMap<>();
    stickyEvents = new ConcurrentHashMap<>();
    mainThreadSupport = builder.getMainThreadSupport();
    mainThreadPoster = mainThreadSupport != null ? mainThreadSupport.createPoster(this) : null;
    backgroundPoster = new BackgroundPoster(this);
    asyncPoster = new AsyncPoster(this);
    indexCount = builder.subscriberInfoIndexes != null ? builder.subscriberInfoIndexes.size() : 0;
    subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes,
            builder.strictMethodVerification, builder.ignoreGeneratedIndex);
    logSubscriberExceptions = builder.logSubscriberExceptions;
    logNoSubscriberMessages = builder.logNoSubscriberMessages;
    sendSubscriberExceptionEvent = builder.sendSubscriberExceptionEvent;
    sendNoSubscriberEvent = builder.sendNoSubscriberEvent;
    throwSubscriberException = builder.throwSubscriberException;
    eventInheritance = builder.eventInheritance;
    executorService = builder.executorService;
}

然后,register方法依次完成了 查找和订阅 2个过程:

// src/org/greenrobot/eventbus/EventBus.java

public void register(Object subscriber) {
    // 获取订阅者的Class对象
    Class<?> subscriberClass = subscriber.getClass();
    // 根据Class对象,查找其所有的订阅方法
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        // 遍历查到的所有订阅方法,绑定订阅者和订阅方法
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            subscribe(subscriber, subscriberMethod);
        }
    }
}
  • 查找
// src/org/greenrobot/eventbus/SubscriberMethodFinder.java

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    // 从METHOD_CACHE中获取SubscriberMethod方法的集合
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    // 如果缓存中有,则直接返回
    if (subscriberMethods != null) {
        return subscriberMethods;
    }

    // ignoreGeneratedIndex默认为false,表示是否忽略索引
    if (ignoreGeneratedIndex) {
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
        // 如果缓存中没有,则通过反射遍历当前订阅者类的所有方法,再筛选出符合条件的订阅方法
        subscriberMethods = findUsingInfo(subscriberClass);
    }
    if (subscriberMethods.isEmpty()) {
        // 如果没有符合条件的方法,则抛出异常
        throw new EventBusException("Subscriber " + subscriberClass
                + " and its super classes have no public methods with the @Subscribe annotation");
    } else {
        // 保存查找到的SubscriberMethod方法
        METHOD_CACHE.put(subscriberClass, subscriberMethods);
        return subscriberMethods;
    }
}
  • 订阅
// src/org/greenrobot/eventbus/EventBus.java

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    // 获得当前订阅方法的事件类型
    Class<?> eventType = subscriberMethod.eventType;
    // 把订阅者对象和SubscriberMetho二次封装成Subscription
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    // 查找subscriptionsByEventType是否存在以当前eventType为key的值
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    // 如果不存在,则创建一个subscriptions,并保存到subscriptionsByEventType中
    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);
        }
    }

    // 按订阅方法的优先级插入到subscriptions相应的位置
    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;
        }
    }

    // 查找是否存在与订阅者对象对应的事件类型集合
    List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
    // 不存在则创建一个subscribedEvents,并保存到typesBySubscriber中
    if (subscribedEvents == null) {
        subscribedEvents = new ArrayList<>();
        typesBySubscriber.put(subscriber, subscribedEvents);
    }
    // 保存当前订阅方法的事件类型到集合中
    subscribedEvents.add(eventType);

    // 此处省略部分代码...
}

2.4 发送/处理事件

  • 普通事件

首先,将要发送的事件保存到事件队列,然后循环遍历事件队列,将事件移出队列,并交给postSingleEvent方法处理:

// src/org/greenrobot/eventbus/EventBus.java

public void post(Object event) {
    // PostingThreadState类携带了事件队列和线程模式等信息
    PostingThreadState postingState = currentPostingThreadState.get();
    // 获取事件队列
    List<Object> eventQueue = postingState.eventQueue;
    // 将要发送的事件添加到事件队列
    eventQueue.add(event);
    // isPosting默认为false
    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方法中,根据eventInheritance属性决定是否向上遍历事件的父类型;接着调用postSingleEventForEventType方法,遍历发送事件类型对应的Subscription集合;接着调用postToSubscription方法处理事件:

// src/org/greenrobot/eventbus/EventBus.java

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    Class<?> eventClass = event.getClass();
    boolean subscriptionFound = false;
    // eventInheritance默认为true,表示是否向上查找事件的父类
    if (eventInheritance) {
        // 查找当前事件类型的Class,连同当前事件类型的Class保存到集合
        List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
        int countTypes = eventTypes.size();
        // 遍历Class集合,继续处理事件
        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));
        }
    }
}
// src/org/greenrobot/eventbus/EventBus.java

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
    CopyOnWriteArrayList<Subscription> subscriptions;
    synchronized (this) {
        // 获取事件类型对应的Subscription集合
        subscriptions = subscriptionsByEventType.get(eventClass);
    }
    // 如果已订阅了对应类型的事件
    if (subscriptions != null && !subscriptions.isEmpty()) {
        for (Subscription subscription : subscriptions) {
            // 记录事件
            postingState.event = event;
            // 记录对应的subscription
            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;
}

最后,在postToSubscription方法中,根据订阅方法的ThreadMode,直接或间接的通过反射的方式,执行可接收事件的订阅方法:

// src/org/greenrobot/eventbus/EventBus.java

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);
    }
}
  • 粘性事件

在发送和订阅事件时,通过stickyEvents做了相应的处理。在订阅过程中,会根据当前订阅方法的事件类型,判断是否发送过该类型的粘性事件;如果发送过,最终也会通过postToSubscription方法完成事件的执行。具体查看代码注释:

// src/org/greenrobot/eventbus/EventBus.java

public void postSticky(Object event) {
    synchronized (stickyEvents) {
        // 在发送粘性事件之前,将其保存到stickyEvents中
        stickyEvents.put(event.getClass(), event);
    }
    // Should be posted after it is putted, in case the subscriber wants to remove immediately
    post(event);
}

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    // 此处省略部分代码...

    // 如果订阅方法的Subscribe注解的sticky属性为true
    if (subscriberMethod.sticky) {
        if (eventInheritance) {
            // Existing sticky events of all subclasses of eventType have to be considered.
            // Note: Iterating over all events may be inefficient with lots of sticky events,
            // thus data structure should be changed to allow a more efficient lookup
            // (e.g. an additional map storing sub classes of super classes: Class -> List<Class>).
            Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
            for (Map.Entry<Class<?>, Object> entry : entries) {
                Class<?> candidateEventType = entry.getKey();
                if (eventType.isAssignableFrom(candidateEventType)) {
                    Object stickyEvent = entry.getValue();
                    // 处理粘性事件
                    checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                }
            }
        } else {
            // 获取保存的粘性事件
            Object stickyEvent = stickyEvents.get(eventType);
            // 处理粘性事件
            checkPostStickyEventToSubscription(newSubscription, stickyEvent);
        }
    }
}

private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
    if (stickyEvent != null) {
        // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
        // --> Strange corner case, which we don't take care of here.
        postToSubscription(newSubscription, stickyEvent, isMainThread());
    }
}

2.5 取消订阅

与订阅和发送事件相比,取消订阅的代码简单很多,目的是释放typesBySubscribersubscriptionsByEventType中缓存的订阅信息:

// src/org/greenrobot/eventbus/EventBus.java

public synchronized void unregister(Object subscriber) {
    // 获得当前订阅者对象的所有订阅方法的事件类型
    List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
    if (subscribedTypes != null) {
        // 遍历当前订阅者对象的所有订阅方法的事件类型,释放之前缓存的Subscription
        for (Class<?> eventType : subscribedTypes) {
            unsubscribeByEventType(subscriber, eventType);
        }
        // 删除当前订阅者对象的所有订阅方法的事件类型
        typesBySubscriber.remove(subscriber);
    } else {
        logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
    }
}
// src/org/greenrobot/eventbus/EventBus.java

private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
    // 获取与当前事件类型对应的Subscription集合
    List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions != null) {
        int size = subscriptions.size();
        // 遍历Subscription集合
        for (int i = 0; i < size; i++) {
            Subscription subscription = subscriptions.get(i);
            // 如果当前subscription类的订阅者 和 希望取消订阅的订阅者相同,则删除当前subscription对象
            if (subscription.subscriber == subscriber) {
                subscription.active = false;
                subscriptions.remove(i);
                i--;
                size--;
            }
        }
    }
}

 

三、索引

3.1 基本特点

EventBus3.0默认是在运行时,通过反射和内存缓存来查找订阅了事件的方法信息,这在普通项目上基本是不用考虑运行时的性能损耗。当然,如果是在比较老的大型项目中,存在大量的订阅了事件的方法,考虑到用LiveData进行重构的成本,还是需要用索引优化下运行时的性能损耗。索引见名知义,其通过在编译时用注解处理器查找订阅了事件的方法信息,然后生成一个辅助的索引类来保存这些基本信息,大大降低了运行时的性能损耗。

3.2 如何使用

1. 首先,在app目录的build.gradle文件中,添加如下配置:

android {
    ...

    defaultConfig {
        ...

        javaCompileOptions {
            annotationProcessorOptions {
                // 指定eventbus索引类的名称
                arguments = [ eventBusIndex : 'com.dir1.dir2.XxxEventBusIndex' ]
            }
        }
    }

    ...
}

dependencies {
    ...

    // 引入eventbus库
    implementation 'org.greenrobot:eventbus:3.1.1'
    // 引入eventbus注解处理器
    annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1'
}

2. 然后,通过在代码中添加注解处理器自动生成的索引类,以完成EventBus单例对象的实例化。

EventBus.builder().addIndex(new XxxEventBusIndex()).installDefaultEventBus();

3. 最后,在编译时,生成对应的XxxEventBusIndex索引类。而之后使用EventBus的方式如常。


注意:

  • 指定索引类的名称,其格式必须是:PackageName.IndexClassName。
  • 在编译时,生成索引类的路径是不固定的,具体的位置由Gradle版本所决定。
  • 在编译时,生成索引类的前提条件是:至少有一个@Subscribe注解配置的订阅方法。

3.3 原理分析

1. 如何在编译时,解析、存储相关的索引信息?

通过一个HashMap常量SUBSCRIBER_INDEX存储相关的索引信息 (以订阅者class类型为key,以封装了订阅者class类型及其所有订阅方法的对象为value)。

public class XxxEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

        putIndex(new SimpleSubscriberInfo(MainActivity.class, true, new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("handleEvent", String.class),
        }));

    }

    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }

    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}

2. 如何在运行时,将索引与EventBus单例关联?

// src/org/greenrobot/eventbus/EventBusBuilder.java

// 第一步:通过addIndex()方法,添加索引对象到EventBusBuilder实例
public EventBusBuilder addIndex(SubscriberInfoIndex index) {
    if (subscriberInfoIndexes == null) {
        subscriberInfoIndexes = new ArrayList<>();
    }
    subscriberInfoIndexes.add(index);
    return this;
}

// 第二步:通过installDefaultEventBus()方法,将EventBusBuilder对象做为EventBus构造方法的参数,创建一个EventBus实例赋值给defaultInstance成员变量
public EventBus installDefaultEventBus() {
    synchronized (EventBus.class) {
        if (EventBus.defaultInstance != null) {
            throw new EventBusException("Default instance already exists." +
                    " It may be only set once before it's used the first time to ensure consistent behavior.");
        }
        EventBus.defaultInstance = build();
        return EventBus.defaultInstance;
    }
}

3. 在register方法的查找过程中,索引是如何工作的?

如果在build.gradle文件中添加了索引的配置项,而且在代码中没有通过EventBusBuilder自定义忽略索引,那么register方法的查找规则便是:若已缓存过,则直接缓存查找,否则优先索引查找,而反射查找兜底。

// src/org/greenrobot/eventbus/SubscriberMethodFinder.java

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        return subscriberMethods;
    }

    // ignoreGeneratedIndex默认为false,表示是否忽略索引
    if (ignoreGeneratedIndex) {
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
        subscriberMethods = findUsingInfo(subscriberClass);
    }

    // 此处省略部分代码...
}

findUsingInfo方法会先调用getSubscriberInfo方法获取编译时保存的索引信息,然后赋值给findState.subscriberInfo做暂存,若findState.subscriberInfo中的subscriberMethods集合不为空,则循环遍历添加到findState.subscriberMethods集合中,再经过一系列处理,便是期望的查找结果。

// src/org/greenrobot/eventbus/SubscriberMethodFinder.java

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
        // 查找SubscriberInfo
        findState.subscriberInfo = getSubscriberInfo(findState);
        if (findState.subscriberInfo != null) {
            // 获得当前订阅者类中,所有订阅了事件的方法
            SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
            for (SubscriberMethod subscriberMethod : array) {
                // FindState的anyMethodByEventType是否已添加过以当前eventType为key的信息
                if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                    // 将subscriberMethod对象添加到subscriberMethods集合
                    findState.subscriberMethods.add(subscriberMethod);
                }
            }
        } else {
            // 通过反射查找订阅了事件的方法
            findUsingReflectionInSingleClass(findState);
        }
        // 修改findState.clazz为subscriberClass的父类进行遍历
        findState.moveToSuperclass();
    }
    // 查找到的订阅方法保存在FindState实例的subscriberMethods集合中,使用subscriberMethods构建一个新的List<SubscriberMethod>,释放掉findState
    return getMethodsAndRelease(findState);
}

private SubscriberInfo getSubscriberInfo(FindState findState) {
    if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
        SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
        if (findState.clazz == superclassInfo.getSubscriberClass()) {
            return superclassInfo;
        }
    }
    // subscriberInfoIndexes是在addIndex()方法中添加的、编译时生成的索引类对象
    if (subscriberInfoIndexes != null) {
        // 遍历索引类实例集合,根据订阅者的Class类查找SubscriberInfo
        for (SubscriberInfoIndex index : subscriberInfoIndexes) {
            SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
            if (info != null) {
                return info;
            }
        }
    }
    return null;
}

最终调用到编译时生成的索引类对象的getSubscriberInfo方法,获取保存在SUBSCRIBER_INDEX中的索引信息。

@Override
public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
    SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
    if (info != null) {
        return info;
    } else {
        return null;
    }
}

 

 

参考:

https://greenrobot.org/eventbus/

https://github.com/greenrobot/EventBus

https://www.jianshu.com/p/d9516884dbd4

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值