EventBus源码解读

1、注册方法register

EventBus.getDefault().register(this);

register主要调用两个方法,findSubscriberMethods和subscribe:

public void register(Object subscriber) {
    //获取订阅者类
    Class<?> subscriberClass = subscriber.getClass();
    //根据订阅者类来获取到订阅方法
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            //遍历订阅方法,调用订阅方法
            subscribe(subscriber, subscriberMethod);
        }
    }
}

查找订阅方法findSubscriberMethods,如果编译是生成索引,或者二次加载缓存中有索引,执行索引查找,否则反射查找:

/**
 * 找到订阅方法
 *
 * @param subscriberClass 订阅者类
 * @return 订阅方法列表
 */
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    //先从缓存里找订阅方法
    //METHOD_CACHE -> Map<Class<?>, List<SubscriberMethod>>: key为订阅者类,value为订阅方法列表
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        //如果缓存里有,直接返回使用
        return subscriberMethods;
    }
    //是否使用 subscriber index,ignoreGeneratedIndex默认为false
    if (ignoreGeneratedIndex) {
        //反射查找
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
        //缓存查找
        subscriberMethods = findUsingInfo(subscriberClass);
    }
    if (subscriberMethods.isEmpty()) {
        //如果没有找到任何订阅方法,抛出异常,提醒用户使用 @Subscribe 方法来声明订阅方法
        //也就是说,如果用户register注册了,但是没有任何@Subscribe订阅方法,会抛出异常来提示用户
        throw new EventBusException("Subscriber " + subscriberClass
                + " and its super classes have no public methods with the @Subscribe annotation");
    } else {
        //如果订阅方法不为空,放入缓存中,以方便下次复用,key为订阅类的类名
        METHOD_CACHE.put(subscriberClass, subscriberMethods);
        return subscriberMethods;
    }
}

反射获取订阅方法:

private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
        findUsingReflectionInSingleClass(findState);
        //查找父类,具体执行要看 skipSuperClasses 标志位
        findState.moveToSuperclass();
    }
    //返回订阅方法列表
    return getMethodsAndRelease(findState);
}
 
/**
 * 通过类的反射提取订阅信息
 *
 * @param findState
 */
private void findUsingReflectionInSingleClass(FindState findState) {
    Method[] methods;
    try {
        // This is faster than getMethods, especially when subscribers are fat classes like Activities
        // getMethods(): 返回由类或接口声明的以及从超类和超接口继承的所有公共方法。
        // getDeclaredMethods(): 返回类声明的方法,包括 public, protected, default (package),但不包括继承的方法
        // 所以,相对比于 getMethods 方法,该方法速度更加快,尤其是在复杂的类中,如 Activity。
        methods = findState.clazz.getDeclaredMethods();
    } catch (Throwable th) {
        // Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
        try {
            methods = findState.clazz.getMethods();
        } catch (LinkageError error) { // super class of NoClassDefFoundError to be a bit more broad...
            String msg = "Could not inspect methods of " + findState.clazz.getName();
            if (ignoreGeneratedIndex) {
                //请考虑使用 EventBus annotation processor 来避免反射
                msg += ". Please consider using EventBus annotation processor to avoid reflection.";
            } else {
                msg += ". Please make this class visible to EventBus annotation processor to avoid reflection.";
            }
            //找不到该类中方法,抛出异常
            throw new EventBusException(msg, error);
        }
        // 因为getMethods()方法已经获取了超类的方法,所以这里设置不再去检查超类
        findState.skipSuperClasses = true;
    }
    //遍历找到的方法
    for (Method method : methods) {
        //获取方法修饰符: public->1;private->2;protected->4;static->8;final->16
        int modifiers = method.getModifiers();
        //如果是public,且不是 abstract | static 类的
        if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
            //获取方法参数类型
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 1) {
                //获取方法的注解 Subscribe
                Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                if (subscribeAnnotation != null) {
                    //第一个参数就是事件类型
                    Class<?> eventType = parameterTypes[0];
                    //检查是否已经添加了订阅该类型事件的订阅方法,true->没有添加;false->已添加
                    if (findState.checkAdd(method, eventType)) {
                        //没有添加过,根据找到的参数来新建一个订阅方法对象,加入 subscriberMethods 列表中
                        ThreadMode threadMode = subscribeAnnotation.threadMode();
                        findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                    }
                }
            } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
                String methodName = method.getDeclaringClass().getName() + "." + method.getName();
                throw new EventBusException("@Subscribe method " + methodName +
                        "must have exactly 1 parameter but has " + parameterTypes.length);
            }
        } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
            String methodName = method.getDeclaringClass().getName() + "." + method.getName();
            throw new EventBusException(methodName +
                    " is a illegal @Subscribe method: must be public, non-static, and non-abstract");
        }
    }
}

检查是否已经添加了订阅该类型事件的订阅方法 checkAdd:

final Map<Class, Object> anyMethodByEventType = new HashMap<>();
final Map<String, Class> subscriberClassByMethodKey = new HashMap<>();
 
boolean checkAdd(Method method, Class<?> eventType) {
    // 2 level check: 1st level with event type only (fast), 2nd level with complete signature when required.
    // Usually a subscriber doesn't have methods listening to the same event type.
    // 通常情况下,一个订阅者不会有多个订阅方法来订阅同一个类型事件(出现这种情况,不就是用户瞎JB写么)
 
    // 扩充:HashMap put() 方法返回值说明:
    // 如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value;
    // 如果是新的一个key,则返回的是null;
    Object existing = anyMethodByEventType.put(eventType, method);
    if (existing == null) {
        //表示还没有存在订阅该类型事件的订阅方法
        return true;
    } else {
        //已经存在订阅该类型事件的订阅方法了
        //existing就是先存入anyMethodByEventType的订阅统一类型事件的订阅方法
        if (existing instanceof Method) {
            if (!checkAddWithMethodSignature((Method) existing, eventType)) {
                // Paranoia check
                throw new IllegalStateException();
            }
            // Put any non-Method object to "consume" the existing Method
            anyMethodByEventType.put(eventType, this);
        }
        return checkAddWithMethodSignature(method, eventType);
    }
}
 
private boolean checkAddWithMethodSignature(Method method, Class<?> eventType) {
    methodKeyBuilder.setLength(0);
    methodKeyBuilder.append(method.getName());
    methodKeyBuilder.append('>').append(eventType.getName());
 
    // "方法名>事件类型"
    // 意图:这样如果存在在同一个类中,有多个订阅方法订阅了同一个事件,这样当这个事件分发的时候,所有的订阅方法都会收到该事件。
    String methodKey = methodKeyBuilder.toString();
    Class<?> methodClass = method.getDeclaringClass();
    Class<?> methodClassOld = subscriberClassByMethodKey.put(methodKey, methodClass);
    /* 扩充:isAssignableFrom() 方法说明:
     * 当前的Class对象所表示的类,是不是参数中传递的Class对象所表示的类的父类,超接口,或者是相同的类型。
     * 是则返回true,否则返回false。
     */
    //methodClassOld 为空,表示没有添加过,或者是methodClassOld是methodClass的父类
    if (methodClassOld == null || methodClassOld.isAssignableFrom(methodClass)) {
        // Only add if not already found in a sub class
        // 只有在子类中没有找到的情况下才添加
        return true;
    } else {
        // Revert the put, old class is further down the class hierarchy
        subscriberClassByMethodKey.put(methodKey, methodClassOld);
        return false;
    }
}

索引获取订阅方法:

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
    //从FindState池中获取一个FindState对象并进行初始化
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    //这里使用了一个while循环,表示子类查找完了,会去父类继续查找
    while (findState.clazz != null) {
        //去 index 文件中查找订阅信息
        findState.subscriberInfo = getSubscriberInfo(findState);
        if (findState.subscriberInfo != null) {
            SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
            //遍历订阅方法
            for (SubscriberMethod subscriberMethod : array) {
                //检查是否已经添加了该订阅方法
                if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                    //未添加,将找到的订阅方法添加到订阅方法列表中
                    findState.subscriberMethods.add(subscriberMethod);
                }
            }
        } else {
            //如果EventBusIndex返回的订阅方法为空,则使用反射方法来查找订阅方法
            findUsingReflectionInSingleClass(findState);
        }
        //查找父类
        findState.moveToSuperclass();
    }
    //返回订阅方法列表
    return getMethodsAndRelease(findState);
}
 
private SubscriberInfo getSubscriberInfo(FindState findState) {
    //subscriberInfo 不为空,表示已经找到了订阅信息,则这次需要往父类查找
    if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
        SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
        //确定此次查找的正是父类
        if (findState.clazz == superclassInfo.getSubscriberClass()) {
            return superclassInfo;
        }
    }
    //subscriberInfoIndexes 就是 EventBus.addIndex(MyEventBusIndex()) 加进来的
    if (subscriberInfoIndexes != null) {
        for (SubscriberInfoIndex index : subscriberInfoIndexes) {
            //就是执行 MyEventBusIndex 类中的 getSubscriberInfo 方法,来获取订阅信息
            SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
            if (info != null) {
                return info;
            }
        }
    }
    return null;
}

原理是在编译期间生成了index文件,这样我们就不需要在运行时通过反射来查找了,直接通过index文件来查找。另外,通过生成的index文件,我们也可以很清晰的看到我们声明的订阅方法分布情况。

使用 Subscriber Index注意事项:

        @Subscribe 方法及其类必须是公共的。
        事件类必须是公共的。
        @Subscribe 不能在匿名类中使用。

订阅方法subscribe:

/**
 * 事件类型与订阅对象列表的一个map集合
 *
 * key -> eventType 事件类型
 * value -> Subscription 订阅对象列表,这里 Subscription 是订阅者与订阅方法的一个封装类
 */
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
/**
 * 记录订阅者与其订阅的所有事件类型列表的一个map集合
 *
 * key -> 订阅者
 * value -> 订阅者订阅的所有事件类型列表
 */
private final Map<Object, List<Class<?>>> typesBySubscriber;
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    //通过订阅方法获得事件类型参数
    Class<?> eventType = subscriberMethod.eventType;
    //通过订阅者与订阅方法来构造出一个 订阅对象
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    //通过事件类型,找到 订阅对象的集合,这边是以 CopyOnWriteArrayList 的形式
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions == null) {
        //如果订阅对象集合为空,则表明还没有注册过订阅了该类型事件的订阅方法。
        //新建一个list,然后将 该事件类型与这个新建的list,放入 subscriptionsByEventType Map 中
        subscriptions = new CopyOnWriteArrayList<>();
        subscriptionsByEventType.put(eventType, subscriptions);
    } else {
        //如果这个订阅对象集合中已经包含该newSubscription
        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;
        }
    }
 
    //通过订阅者来找到 其订阅的所有事件的类型列表 subscribedEvents
    List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
    //如果该订阅者还没有任何订阅方法,即 subscribedEvents 为空
    if (subscribedEvents == null) {
        //新建一个列表,用来放这个订阅者订阅的所有的事件的类型
        subscribedEvents = new ArrayList<>();
        //并将该订阅者与这个新建的列表,放入到 typesBySubscriber map 中
        typesBySubscriber.put(subscriber, subscribedEvents);
    }
    //将该事件类型加入到 事件类型列表中
    subscribedEvents.add(eventType);
 
    //如果订阅方法支持粘性事件
    if (subscriberMethod.sticky) {
        //是否考虑事件类的层次结构,默认为true
        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);
        }
    }
}

2、解注册方法unregister

EventBus.getDefault().unregister(this);
public synchronized void unregister(Object subscriber) {
    //通过订阅者找到其订阅的所有事件类型列表
    List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
    if (subscribedTypes != null) {
        //遍历事件类型列表
        for (Class<?> eventType : subscribedTypes) {
            //通过事件类型,注销订阅者
            unsubscribeByEventType(subscriber, eventType);
        }
        //将该订阅者从typesBySubscriber map 中移除
        typesBySubscriber.remove(subscriber);
    } else {
        //log提示:在没有注册的前提下执行了注销动作
        logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
    }
}
 
private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
    //通过事件类型来找到相关的订阅对象列表
    List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions != null) {
        int size = subscriptions.size();
        //遍历订阅对象列表
        for (int i = 0; i < size; i++) {
            Subscription subscription = subscriptions.get(i);
            if (subscription.subscriber == subscriber) {
                //从订阅对象列表中找到该订阅者,将其 active 状态改为 false,并从订阅对象列表中移除
                subscription.active = false;
                subscriptions.remove(i);
                i--;
                size--;
            }
        }
    }
}

3、发送事件post

EventBus.getDefault().post(new MessageEvent());
public void post(Object event) {
    //PostingThreadState 是事件与发送状态的封装类
    PostingThreadState postingState = currentPostingThreadState.get();
    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;
        }
    }
}
 
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    //获取事件的类对象
    Class<?> eventClass = event.getClass();
    boolean subscriptionFound = false;
    //是否考虑事件类的层次结构,默认为true
    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));
        }
    }
}
 
/**
 * 根据事件类型,将事件发送给订阅了该类型事件的订阅方法
 * @param event
 * @param postingState
 * @param eventClass
 * @return true->找到订阅了该类型事件的订阅方法;false->没有订阅该类型事件的订阅方法
 */
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;
            try {
                //将事件发送给订阅对象
                postToSubscription(subscription, event, postingState.isMainThread);
                aborted = postingState.canceled;
            } finally {
                //重置 PostingState
                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 {
                //利用Handler切换到主线程,最终还是执行invokeSubscriber
                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) {
                //如果当前处于主线程中,将利用线程池,切换到子线程中处理,最终还是会调用invokeSubscriber
                backgroundPoster.enqueue(subscription, event);
            } else {
                //如果当前处于子线程,则直接在该子线程中处理事件
                invokeSubscriber(subscription, event);
            }
            break;
        case ASYNC:
            //无论处于什么线程,最终都是利用线程池,切换到子线程中处理,最终还是会调用invokeSubscriber
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}
 
 
void invokeSubscriber(Subscription subscription, Object event) {
    try {
        //利用反射调用订阅方法
        subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
    } catch (InvocationTargetException e) {
        handleSubscriberException(subscription, event, e.getCause());
    } catch (IllegalAccessException e) {
        throw new IllegalStateException("Unexpected exception", e);
    }
}

4、发送粘性事件postSticky

EventBus.getDefault().postSticky(new MessageEvent());
/**
 * 用来存放粘性事件
 *
 * key -> 粘性事件的类对象
 * value -> 粘性事件
 */
private final Map<Class<?>, Object> stickyEvents;
public void postSticky(Object event) {
    //同步锁,将粘性事件存入 stickyEvents
    synchronized (stickyEvents) {
        stickyEvents.put(event.getClass(), event);
    }
    //事件加入后,与普通事件一样,调用 post() 方法发送事件
    post(event);
}

订阅者在注册订阅方法中,如果当前订阅方法支持粘性事件,则会去stickyEvents集合中查件是否有对应的粘性事件,如果找到粘性事件,则发送该事件。

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    //通过订阅方法获得事件类型参数
    Class<?> eventType = subscriberMethod.eventType;
    //通过订阅者与订阅方法来构造出一个 订阅对象
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    ...
    .省略部分代码.
    ...
    //如果订阅方法支持粘性事件
    if (subscriberMethod.sticky) {
        //是否考虑事件类的层次结构,默认为true
        if (eventInheritance) {
            Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
            for (Map.Entry<Class<?>, Object> entry : entries) {
                Class<?> candidateEventType = entry.getKey();
                //eventType 是否是 candidateEventType 的父类
                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) { 
        postToSubscription(newSubscription, stickyEvent, isMainThread());
    }
}

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值