项目地址:
https://github.com/greenrobot/EventBus
EventBus是我们在项目当中最常用的开源框架之一。对于EventBus的使用方法也是非常的简单。然而EventBus内部的实现原理也不是很复杂。在这里便针对EventBus3.0的源码进行一下详细的分析
一、使用
1.1 在gradle中添加
// 3.0版本
compile 'org.greenrobot:eventbus:3.0.0'
// 2.4版本
compile 'de.greenrobot:eventbus:2.4.0'
1.2 定义事件
public class MessageEvent { /* Additional fields if needed */ }
1.3 注册订阅者
2.4版本中有四种注册方法,区分了普通注册和粘性事件注册,并且在注册时可以选择接收事件的优先级,
3.0版本将粘性事件以及订阅事件的优先级换了一种更好的实现方式,所以3.0版本中的注册就变得简单,只有一个register()方法即可.
// 3.0 版本
EventBus.getDefault().register(this);
//2.4版本的注册
EventBus.getDefault().register(this);
EventBus.getDefault().register(this, 100);
EventBus.getDefault().registerSticky(this, 100);
EventBus.getDefault().registerSticky(this);
1.4 响应事件订阅方法
在2.4版本中只有通过onEvent开头的方法会被注册,而且响应事件方法触发的线程通过onEventMainThread或onEventBackgroundThread这些方法名区分,而在3.0版本中.通过@Subscribe注解,来确定运行的线程threadMode,是否接受粘性事件sticky以及事件优先级priority,而且方法名不在需要onEvent开头,所以又简洁灵活了不少.
//3.0版本
@Subscribe(threadMode = ThreadMode.BACKGROUND, sticky = true, priority = 100)
public void test(String str) {
}
//2.4版本
public void onEvent(String str) {
}
public void onEventMainThread(String str) {
}
public void onEventBackgroundThread(String str) {
}
1.5 发送事件
我们可以通过EventBus的post()方法来发送事件,发送之后就会执行注册过这个事件的对应类的方法.或者通过postSticky()来发送一个粘性事件.在代码是2.4版本和3.0版本是一样的.
EventBus.getDefault().post("zzx");
EventBus.getDefault().postSticky("zzx");
1.6 解除注册
当我们不在需要接收事件的时候需要解除注册unregister,2.4和3.0的解除注册也是相同的.代码如下:
EventBus.getDefault().unregister(this);
二、源码分析
看Eventbus几行代码就搞定了事件的订阅与接收,内部原理是怎么实现的呢,让我们一起去分析
2.1 我们使用是直接EventBus.getDefault(), 先看一下EventBus
实例怎么来的
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
这里就是设计模式里我们常用的单例模式了,目的是为了保证getDefault()
得到的都是同一个实例。如果不存在实例,就调用了EventBus
的构造方法
private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();
public EventBus() {
this(DEFAULT_BUILDER);
}
EventBus(EventBusBuilder builder) {
//key:订阅的事件,value:订阅这个事件的所有订阅者集合
//private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
subscriptionsByEventType = new HashMap<>();
//key:订阅者对象,value:这个订阅者订阅的事件集合
//private final Map<Object, List<Class<?>>> typesBySubscriber;
typesBySubscriber = new HashMap<>();
//粘性事件 key:粘性事件的class对象, value:事件对象
//private final Map<Class<?>, Object> stickyEvents;
stickyEvents = new ConcurrentHashMap<>();
//事件主线程处理
mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
//事件 Background 处理
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;
}
可以看出是通过初始化了一个EventBusBuilder()对象来分别初始化EventBus的一些配置,当我们在写一个需要自定义配置的框架的时候,这种实现方法非常普遍,将配置解耦出去,使我们的代码结构更清晰.注释里我标注了大部分比较重要的对象,这里没必要记住,看下面的文章时如果对某个对象不了解,可以再回来看看.
2.2 register()
3.0的注册只提供一个register()方法了,所以我们先来看看register()方法做了什么
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);
}
}
}
在这里可以看到register这个方法中的代码很简短,但是在它的内容却一点也不简单。对于register中的参数,就是我们的订阅者,也就是我们经常传入的this对象。在这个方法中主要完成了两件事情。首先通过findSubscriberMethods方法来查找订阅者中所有的订阅方法。然后遍历订阅者的订阅方法来完成订阅者的订阅操作。下面来详细的分析这两步的实现过程。
2.2.1 订阅方法的查找过程 findSubscriberMethods
方法实现
首先在这里来看一下findSubscriberMethods
这个方法是如何查找订阅者的订阅方法。在这先描述一下SubscriberMethod类。对于SubscriberMethod类中,主要就是用保存订阅方法的Method对象,线程模式,事件类型,优先级,是否粘性事件等属性。下面就来看一下findSubscriberMethods方法。
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
//从缓存中获取SubscriberMethod集合
List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}
//ignoreGeneratedIndex是否忽略注解器生成的MyEventBusIndex 3.0才有的
// ignoreGeneratedIndex 默认是false
if (ignoreGeneratedIndex) {
//通过反射获取subscriberMethods
subscriberMethods = findUsingReflection(subscriberClass);
} else {
//通过注解器生成的MyEventBusIndex信息获取subscriberMethods,
//如果没有配置MyEventBusIndex,依然通过通过反射获取subscriberMethods
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;
}
}
在这个方法里面的逻辑依然也是十分的清晰。首先会从缓存中查找是否有订阅方法的集合,若是存在则直接返回缓存中的该订阅者的订阅方发集合。若是不存在,便从订阅者中找出全部的订阅方法。对于ignoreGeneratedIndex
属性表示是否忽略注解器生成的MyEventBusIndex
(在项目重新rebuild以后,会自动生成在build文件夹下,类名也可以自己定义)。如何生成MyEventBusIndex类以及他的使用,可以参考官方文档。ignoreGeneratedIndex
的默认值为false
,可以通过EventBusBuilder
来设置它的值。在这里会根具ignoreGeneratedIndex
的值来采用不同的方式获取订阅方法的集合subscriberMethods
。在获得subscriberMethods
以后,如果订阅者中不存在@Subscribe
注解且为public的订阅方法,则会抛出异常。这也就是说对于订阅者若是要成功注册到EventBus中,在订阅者中必须存在通过@Subscribe注解且为public类型的订阅方法。在成功获取到订阅方法集合以后,便将订阅方法集合添加到缓存中。对于这个缓存它是以订阅者的类作为key,订阅方法集合作为value的Map类型。
由于在我们的项目中经常通过EventBus单例模式来获取默认的EventBus对象。所以就针对ignoreGeneratedIndex为false的情况下看一下EventBus是如何获得订阅方法集合的。在这里调用了findUsingInfo
方法。下面就来看一下findUsingInfo
这个方法
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
//创建和初始化FindState对象
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass);
while (findState.clazz != null) {
//获取订阅者信息,没有配置MyEventBusIndex返回null
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 {
//通过反射来查找订阅方法
findUsingReflectionInSingleClass(findState);
}
//进入父类查找订阅方法
findState.moveToSuperclass();
}
//回收处理findState,并返回订阅方法的List集合
return getMethodsAndRelease(findState);
}
下面就来看一下如何通过反射来查找订阅方法,也就是看一下findUsingReflectionInSingleClass的执行过程。
private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
methods = findState.clazz.getMethods();
findState.skipSuperClasses = true;
}
for (Method method : methods) {
//对订阅方法的类型进行过滤
int modifiers = method.getModifiers();
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) {
//保存到findState对象当中
Class<?> eventType = parameterTypes[0];
if (findState.checkAdd(method, eventType)) {
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");
}
}
}
这段代码的执行过程其实并不是很复杂。在这里主要是使用了Java的反射和对注解的解析。首先通过反射来获取订阅者中所有的方法。并根据方法的类型,参数和注解来找到订阅方法。找到订阅方法后将订阅方法相关信息保存到FindState当中。到这里便完成对订阅者中所有订阅方法的查找
接下来继续回到register方法中,看subscribe
方法实现
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
//获取订阅方法中的订阅事件
Class<?> eventType = subscriberMethod.eventType;
//创建一个Subscription来保存订阅者和订阅方法
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
//获取当前订阅事件中Subscription的List集合
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions == null) {
//该事件对应的Subscription的List集合不存在,则重新创建并保存在subscriptionsByEventType中
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
//判断是订阅者是否已经被注册
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}
//将newSubscription按照订阅方法的优先级插入到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);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
//将当前的订阅事件添加到subscribedEvents中
subscribedEvents.add(eventType);
//粘性事件的处理,在这里不做详细分析
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);
}
}
}
在这个方法中便是订阅者真正的注册过程。首先会根据subscriber和subscriberMethod来创建一个Subscription对象。之后根据事件类型获取或创建一个Subscription集合subscriptions并添加到typesBySubscriber对象中。最后将刚才创建的Subscription对象添加到subscriptions之中。到此注册过程就完成了
你只要记得一件事:扫描了所有的方法,把匹配的方法最终保存在subscriptionsByEventType(Map,key:eventType ; value:CopyOnWriteArrayList )中;
eventType是我们方法参数的Class,Subscription中则保存着subscriber, subscriberMethod(method, threadMode, eventType), priority;包含了执行改方法所需的一切.
2.3 事件的发送 post 实现
现在来看一下事件的整个发送过程。在获取到EventBus对象以后,通过post方法来进行对事件的提交,下面就来看一下整个post方法是如何对事件进行提交的。
public void post(Object event) {
//PostingThreadState保存着事件队列和线程状态信息
PostingThreadState postingState = currentPostingThreadState.get();
//获取事件队列,并将当前事插入到事件队列中
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);
if (!postingState.isPosting) {
//当前线程是否为主线程
postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
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;
}
}
}
首先是通过currentPostingThreadState.get()方法来得到当前线程PostingThreadState的对象,为什么是说当前线程我们来看看currentPostingThreadState的实现:
private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
@Override
protected PostingThreadState initialValue() {
return new PostingThreadState();
}
};
final static class PostingThreadState {
//通过post方法参数传入的事件集合
final List<Object> eventQueue = new ArrayList<Object>();
boolean isPosting; //是否正在执行postSingleEvent()方法
boolean isMainThread;
Subscription subscription;
Object event;
boolean canceled;
}
currentPostingThreadState的实现是一个包含了PostingThreadState的ThreadLocal对象,关于ThreadLocal
==ThreadLocal== 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,而这段数据是不会与其他线程共享的。其内部原理是通过生成一个它包裹的泛型对象的数组,在不同的线程会有不同的数组索引值,通过这样就可以做到每个线程通过 get() 方法获取的时候,取到的只能是自己线程所对应的数据。
在 EventBus 中, ThreadLocal 所包裹的是一个 PostingThreadState 类,它仅仅是封装了一些事件发送中过程所需的数据。
回到 post() 方法,我们看到其核心代码是这句:
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);
}
每次调用post()
的时候都会传入一个事件,这个事件会被加入到队列。而每次执行postSingleEvent()都会从队列中取出一个事件,这样不停循环取出事件处理,直到队列全部取完。
再看 postSingleEvent()
方法
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class<?> eventClass = event.getClass();
boolean subscriptionFound = false;
//是否触发订阅了该事件(eventClass)的父类,以及接口的类的响应方法.
if (eventInheritance) {
//查找eventClass类所有的父类以及接口
List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
int countTypes = eventTypes.size();
//循环postSingleEventForEventType
for (int h = 0; h < countTypes; h++) {
Class<?> clazz = eventTypes.get(h);
//只要右边有一个为true,subscriptionFound就为true
subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
}
} else {
//post单个
subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
}
//如果没发现
if (!subscriptionFound) {
if (logNoSubscriberMessages) {
Log.d(TAG, "No subscribers registered for event " + eventClass);
}
if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
eventClass != SubscriberExceptionEvent.class) {
//发送一个NoSubscriberEvent事件,如果我们需要处理这种状态,接收这个事件就可以了
post(new NoSubscriberEvent(this, event));
}
}
}
跟着上面的代码的注释,我们可以很清楚的发现是在postSingleEventForEventType()方法里去进行事件的分发,代码如下:
private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
CopyOnWriteArrayList<Subscription> subscriptions;
//获取订阅了这个事件的Subscription列表.
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 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);
}
}
总结上面的代码就是,首先从subscriptionsByEventType里获得所有订阅了这个事件的Subscription列表,然后在通过postToSubscription()方法来分发
事件,在postToSubscription()通过不同的threadMode在不同的线程里invoke()订阅者的方法,ThreadMode共有四类:
1、PostThread
:默认的 ThreadMode,表示在执行 Post 操作的线程直接调用订阅者的事件响应方法,不论该线程是否为主线程(UI 线程)。当该线程为主线程时,响应方法中不能有耗时操作,否则有卡主线程的风险。适用场景:对于是否在主线程执行无要求,但若 Post 线程为主线程,不能耗时的操作;
2、MainThread
:在主线程中执行响应方法。如果发布线程就是主线程,则直接调用订阅者的事件响应方法,否则通过主线程的 Handler 发送消息在主线程中处理——调用订阅者的事件响应函数。显然,MainThread类的方法也不能有耗时操作,以避免卡主线程。适用场景:必须在主线程执行的操作;
3、BackgroundThread
:在后台线程中执行响应方法。如果发布线程不是主线程,则直接调用订阅者的事件响应函数,否则启动唯一的后台线程去处理。由于后台线程是唯一的,当事件超过一个的时候,它们会被放在队列中依次执行,因此该类响应方法虽然没有PostThread类和MainThread类方法对性能敏感,但最好不要有重度耗时的操作或太频繁的轻度耗时操作,以造成其他操作等待。适用场景:操作轻微耗时且不会过于频繁,即一般的耗时操作都可以放在这里;
4、Async
:不论发布线程是否为主线程,都使用一个空闲线程来处理。和BackgroundThread不同的是,Async类的所有线程是相互独立的,因此不会出现卡线程的问题。适用场景:长耗时操作,例如网络访问
这里我们只来看看UI线程中invokeSubscriber(subscription, event);是如何实现的,
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);
}
}
实际上就是通过反射调用了订阅者的订阅函数并把event对象作为参数传入.至此post()流程就结束了
2.4 unregister() 方法
public synchronized void unregister(Object subscriber) {
//通过typesBySubscriber来取出这个subscriber订阅者订阅的事件类型,
List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
if (subscribedTypes != null) {
//分别解除每个订阅了的事件类型
for (Class<?> eventType : subscribedTypes) {
unsubscribeByEventType(subscriber, eventType);
}
//从typesBySubscriber移除subscriber
typesBySubscriber.remove(subscriber);
} else {
Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}
}
然后接着看unsubscribeByEventType()方法的实现:
private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
//subscriptionsByEventType里拿出这个事件类型的订阅者列表.
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) {
subscription.active = false;
subscriptions.remove(i);
i--;
size--;
}
}
}
}
最终分别从typesBySubscriber和subscriptions里分别移除订阅者以及相关信息即可.
在2.4版本里EventBus写出的代码可读性不是太好因为所有订阅方法都是onEvent开头,这样就使代码的可读性降低不少,但是3.0之后我们就不用担心这些了,因为
方法名已经不再需要onEvent开头了。所以总体上来说EventBus还是值得我们在项目中使用的。