Android 进阶4:EventBus3.0源码分析

前几篇文章分析了Activity的源码,后来看了看Window以及ViewRoot的相关源码,这些只是还没梳理,真的是视觉疲劳,来一个EventBus源码分析压压惊~。

其实关于EventBus的源码分析,网上也有很多,但是终究是别人的,以前也看过很多遍,但是决定是自己写下来比较好,座右铭:看一遍,不如自己写一遍。
关于EventBus的小总结我们先看下,带着知识点去看源码,然后分析为什么要这么写,还是必须这样写。

  1. POSTING:发布事件和订阅事件总是在一个线程。避免操作UI,可能造成ANR。
  2. MAIN : 无论发布事件位于什么线程,订阅事件总是在UI主线程
  3. BACKGROUND: 发布事件位于子线程,那么订阅事件就位于该子线程;如果发布事件位于Ui主线程,那么订阅事件就另外开启一个子线程。订阅事件方法中不能更新UI。
  4. ASYNC:无论发布事件位于什么线程,订阅事件总是位于另外在开启子线程。
  5. EventBus索引的添加
  6. 有这样一个场景:我们的EventModel A 继承自EventModel B,并实现了接口EventInterface C,此时在订阅类中,三个订阅方法参数分别为:EventModel A, EventModel B , EventInterface;最后在发布事件 EventModel A, 此时的结果应该是:三个订阅方法都能够收到事件。但是如果我们只想让参数为EventModel A订阅方法收到事件,应该怎么做? 详情看代码描述
  7. EventBus发送粘性事件,等等,说一下粘性时间,ActivityA 先创建,然后发送粘性事件,然后创建并跳转到ActivityB,此时在ActivityB注册了粘性事件的回调方法,此时粘性回调方法将执行,这是什么原理?
  8. 同一个订阅类如果注册两次为什么报错?

问题点6代码描述:

    @Subscribe(threadMode = ThreadMode.MAIN , sticky = true)
    public void onEventObject(EventModelA object) {
        Log.e("-----------","我是子类");
    }

    @Subscribe(threadMode =  ThreadMode.MAIN, sticky = true)
    public void onEventObject(EventModelB event) {
        Log.e("-----------","我是父类");
    }

    @Subscribe(threadMode =  ThreadMode.MAIN, sticky = true)
    public void onEventObject(EventInterfaceC event) {
        Log.e("-----------","我是父接口");
    }
EventBus.getDefault().postSticky(new EventModelA ());

EventBus 的 register(this) 方法

    /**
     * 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.
     */
     /*
     翻译:注册给定的订阅服务用于接收事件,订阅者必须调用unregister取消注册对象。
     			订阅服务器有事件处理方法,这些方法需要有ThreadModel(线程模式)和优先级的配置
     */
    public void register(Object subscriber) {
    
        //获取订阅者的Class对象
        Class<?> subscriberClass = subscriber.getClass();
        
        //通过这个类对象获取SubscriberMethod的集合,至于集合是什么后面讲到。
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
        
        //遍历SubscriberMethods集合,执行subscribe方法,参数为:订阅者的类和subscriberMethod。
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

register的注释翻译,这个不需要解释了, 方法内,先获取了订阅类的Class对象,把这个Class对象为参数,通过
findSubscriberMethods(Class) 方法获取到List集合泛型为SubscriberMethod, 从findSubscriberMethods方法名来看,就是:找到订阅的方法。 至于这个方法是怎么工作的我们下文分析。有必要看下SubscriberMethod类了:

/** Used internally by EventBus and generated subscriber indexes. */
public class SubscriberMethod {
    final Method method; //Java通过反射获取到的订阅类的信息
    final ThreadMode threadMode; //订阅类中订阅方法的线程模型TreadMode
    final Class<?> eventType; //订阅方法参数的Class对象,也就是我们的Model的Class对象
    final int priority; //订阅回调方法的优先级
    final boolean sticky; //订阅回调方法是否是粘性的
    /** Used for efficient comparison */
    String methodString; //订阅方法的方法名
	.........

通过代码可以看出 SubscriberMethod 其实就是一个Model类,它代表了:某一个订阅类的订阅方法信息。
那么回过头看下findSubscriberMethods(Class)就是:返回某一个注册订阅类的订阅方法的集合。然后接下来遍历这个集合,执行subscribe方法,参数为:订阅类(subscriber)和其某一个订阅的方法(subscriberMethod);

    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    	//获得订阅方法的Class对象。
        Class<?> eventType = subscriberMethod.eventType;
        
        //创建Subscription,参数为:订阅者(subscriber)和订阅方法的信息类(subscriberMethod)
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        
        //subscriptionsByEventType获得CopyOnWriteArrayList集合,泛型为Subscription
        //eventType就是注册回调方法类的参数的Class对象,也就是上述例子中的:EventModelA的Class对象
        //subscriptions也就是所有包含该回调方法的订阅类和该回调方法的信息集合
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
          //以该参数Class为Key值的Value是null,也就是同一进程中这是这个订阅类是第一个使用该Model为参数的回调方法
            subscriptions = new CopyOnWriteArrayList<>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            if (subscriptions.contains(newSubscription)) {
            //这个异常是不是很常见?是不是发生在我们重复注册的时候?!
            //原因:该参数Class对象对应的集合重复添加了该对象,所以报错
            //如果我们重复注册了,那么之前已经执行过了,subscriptions.add(i, newSubscription); 
            //此时subscriptions 已经包含了该对象,所以报错
                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中。
                subscriptions.add(i, newSubscription);
                break;
            }
        }

		//通过subscriber,获取typesBySubscriber的value值,类型是什么呢?
        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        //在此处确定类型为:方法参数Model的的Class。
        subscribedEvents.add(eventType);

		//是否是粘性的
        if (subscriberMethod.sticky) {
         //是否需要检查父类和接口
         /*
         eventInheritance默认为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);
            }
        }
    }

上述方法注释比较多,不再赘述,就是为什么不能重复注册? 原因该参数Class对应的集合已经包含了该Subscription(订阅类和订阅方法的对应信息),问题点9解决了。checkPostStickyEventToSubscription方法内部最终调用的还是postToSubscription方法:


	//通过invoke分发事件
	/**
	Subscription :注册过的订阅类和订阅方法的信息类
	event:订阅方法内的参数
	isMainThread:发布事件是否在主线程
	*/
    private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
        switch (subscription.subscriberMethod.threadMode) {
            case POSTING:
                //直接在发布线程中invoke,通过反射调用注册订阅的方法。
                invokeSubscriber(subscription, event);
                break;
            case MAIN:
          		//发布事件是否是在主线程,如果是在主线程,那么直接invoke。
                if (isMainThread) {
                    invokeSubscriber(subscription, event);
                } else {
                	//如果不是在主线程,那么事件交给UI主线程的Looper调度消息。总之还是在主线程
                    mainThreadPoster.enqueue(subscription, event);
                }
                break;
            case MAIN_ORDERED:
            //区别就是MAIN_ORDERED优先判断将事件交给UI主线程的Looper调度消息
                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);
        }
    }

postToSubscription方法是发布事件的,可以看下在subscribe方法最后判断了注册订阅的方法是否粘性的,如果是and之前发布过粘性事件(stickyEvent可以看成一个发布粘性事件的集合),此时执行postToSubscription方法,用于发布事件。发布时间内有五种类型,这五种类型解释了问题点的1,2,3,4。问题点8是不是解决一半了,解决了,在注册的时候,我们判断是否之前发布过粘性事件,如果是,在注册订阅的时候直接运行postToSubscription方法。至于另一半没解决就是:stickyEvent这个东西什么时候添加要发布的粘性事件呢?

至此注册流程就算完了。 用一张流程图总结一下:
在这里插入图片描述

EventBus 的 post流程

发布事件,分两种:post 和 postSticky(粘性的) 我们非粘性的的post.

   /** Posts the given event to the event bus. */
    public void post(Object event) {
       //获得当前发布事件的线程信息
        PostingThreadState postingState = currentPostingThreadState.get();
        //通过postingState获取当前线程的发布事件eventQueue集合。
        List<Object> eventQueue = postingState.eventQueue;
        //将发布事件添加到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 {
            	//循环eventQueue
                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对象
        Class<?> eventClass = event.getClass();
        //subscriptionFound 表示是否找到了订阅方法,默认是false
        boolean subscriptionFound = false;
       
        if (eventInheritance) {
         //此时又看到了eventInheritance判断,也就是是否循环遍历获取以父类以及实现接口为参数的的订阅方法。
         //看下lookupAllEventTypes方法的注释就行了
         //lookupAllEventTypes注释:查找所有类对象,包括超级类和接口。还应该为接口工作。
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            //循环遍历集合,给以event集成的父类或者接口为参数的订阅方法发送事件;
            for (int h = 0; h < countTypes; h++) {
                Class<?> clazz = eventTypes.get(h);
                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
        	//直接执行以event为参数的订阅方法
            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));
            }
        }
    }

上述方法主要做的任务就是,先判断eventInheritance是否为true, 也就是是否需要向以Event继承的父类或者接口为参数的订阅方法执行postSingleEventForEventType方法。那么postSingleEventForEventType方法是干什么的?


    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
        	//subscriptionsByEventType是不是很熟悉呢?我们在注册的时候给它添加过数据,就是
        	//subscriptionsByEventType.put(eventType, subscriptions);也就是:以参数Model的Class为key, 
        	//以《订阅类,以该key为参数的订阅方法》为value,此处获得value,
        	//要这个value干什么呢?肯定是遍历发送事件啊!
            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;
    }

上述代码主要就是通过发送事件的Event的class为key值,从subscriptionsByEventType中获取value值,这个value值是个集合,泛型是:《订阅类,以该key为参数的订阅方法》,然后遍历该集合,循环执行postToSubscription给订阅者的相应的回调方法发送事件。

接下来看下postSticky方法:

    public void postSticky(Object event) {
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        // Should be posted after it is putted, in case the subscriber wants to remove immediately
        post(event);
    }

首先执行的是往stickyEvents集合中添加数据,那么问题8的另一半是不是也解决了呢?!此时发送粘性事件先将事件存储到stickyEvents中,这种情况是订阅事件还没有声明订阅。如果订阅事件已经订阅了,如果post(event)就起作用了。。。

至此post 和 postSticky就算分析完了,附上一张流程图

在这里插入图片描述

EventBus 的 unregister

    /** Unregisters the given subscriber from all event classes. */
    public synchronized void unregister(Object subscriber) {
    	//typesBySubscriber是不是很熟悉呢? 它是我们在注册的时候添加的数据,用订阅者为key, 
    	//以集合为value,集合的泛型是该订阅类下的所有的订阅方法Class,
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
        	//循环遍历该订阅类下的所有的订阅方法,循环删除
            for (Class<?> eventType : subscribedTypes) {
                unsubscribeByEventType(subscriber, eventType);
            }
            //删除相关信息
            typesBySubscriber.remove(subscriber);
        } else {
            logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

上述代码很简单,也就是通过subscriber为key获取该订阅类下的所有订阅方法集合,然后遍历该集合,执行unsubscribeByEventType方法,之后删除typesBySubscriber中的subscriber相关信息。那么unsubscribeByEventType方法内做了什么操作呢?

   /** Only updates subscriptionsByEventType, not typesBySubscriber! Caller must update typesBySubscriber. */
    private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
    	//维护集合subscriptionsByEventType的正确性。
    	//先获得以要删除的订阅类中的订阅方法参数Class为参数的subscriptions集合。
        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);
                //如果集合中的订阅者等于subscription方法的订阅者,那么就删除。
                if (subscription.subscriber == subscriber) {
                    subscription.active = false;
                    subscriptions.remove(i);
                    i--;
                    size--;
                }
            }
        }
    }

上述代码整体维护的就是subscriptionsByEventType集合的正确性,操作的维度是以要删除的订阅类中的订阅方法中的参数为中心的。

解绑流程就是维护了typesBySubscriber和subscriptionsByEventType两个集合的数据

解绑流程分析完了,附上一张流程图:
在这里插入图片描述

索引的添加

关于EventBus的添加,该功能是在EventBus3.0之后添加的新功能,据说能大幅提升性能?为什么能提升性能呢?

添加索引传送门

添加索引的方法是:

EventBus eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();

此处的MyEventBusIndex是我们定义的自定义名称,看下addIndex的方法:


    /** Adds an index generated by EventBus' annotation preprocessor. */
    public EventBusBuilder addIndex(SubscriberInfoIndex index) {
        if (subscriberInfoIndexes == null) {
            subscriberInfoIndexes = new ArrayList<>();
        }
        subscriberInfoIndexes.add(index);
        return this;
    }

将index对象添加到了subscriberInfoIndexes集合中,那么SubscriberInfoIndex是什么呢?

/**
 * Interface for generated indexes.
 */
public interface SubscriberInfoIndex {
    SubscriberInfo getSubscriberInfo(Class<?> subscriberClass);
}

可以看到是个接口,那么实现类在哪里呢?就是我们定义的MyEventBusIndex类:

/** This class is generated by EventBus, do not edit. */
public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

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

		//每个订阅类
        putIndex(new SimpleSubscriberInfo(TestActivity.class, true, new SubscriberMethodInfo[] {
        	//每个订阅方法的相关信息
            new SubscriberMethodInfo("onEventObject", EventObject.class, ThreadMode.MAIN, 0, true),
            new SubscriberMethodInfo("onEventObject", SecondEvent.class, ThreadMode.MAIN, 0, true),
            new SubscriberMethodInfo("onEventObject", TestInterfaceEvent.class, ThreadMode.MAIN, 0, true),
            new SubscriberMethodInfo("onEventObject", ThirdEvent.class, ThreadMode.MAIN, 0, true),
        }));

    }

    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;
        }
    }
}

这个类是编译自动生成,不是手动添加的。那么在编译的时候,获取了订阅类的相关信息,在注册的时候是怎么用的呢?在上文注册源码分析的时候提到了这句代码:

List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);

那么subscriberMethodFinder方法做了什么呢?

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
        //判断是否添加索引
        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 {
            METHOD_CACHE.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }

ignoreGeneratedIndexb标记代表是否添加索引,默认是false,也就是没有添加索引,那么findUsingInfo方法是干什么的呢?

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
        	//获取存储的相关索引信息
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) { //索引信息不为null
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else { //索引存储为null,该方法内部就是通过反射获取订阅类的相关订阅信息
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        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;
            }
        }
        if (subscriberInfoIndexes != null) {
          //遍历取出索引集合中的相关信息
            for (SubscriberInfoIndex index : subscriberInfoIndexes) {
                SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
                if (info != null) {
                    return info;
                }
            }
        }
        return null;
    }

我们可以知道,其实消耗时间的重头部分就是在注册的时候,因为注册的时候(没添加索引),需要通过Java的反射获取注册类的订阅方法相关信息,这部分是非常耗时间的,它发生在运行过程。但是添加完了索引之后,会在编译的时候生成一个类,该类包含了注册类的相关信息,发生在编译过程。

通过上述代码分析,原理可以总结两点:
1:源码架构:实际上维护的就是两个Map集合,分别是:以订阅方法的参数class为key, 然后存储相关订阅信息,value也就是整个应用以该Class类为参数的订阅类信息;另一个是以订阅类为key, 此时value为该订阅类的所有方法。
2:提高性能:第一点是关闭父类以及接口查找分发事件;第二点 添加索引,索引添加的原理就是提前在编译的时候加载好注册类的相关信息。

最后说下关于EvenBus提高性能的相关设置,可以在Application中设置如下:

EventBus.builder().addIndex(new MyEventBusIndex()).eventInheritance(false).installDefaultEventBus();

参考文献:【Bugly干货分享】老司机教你 “飙” EventBus 3

Android学习交流群:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lmq121210/article/details/82788722