当前位置: 首页 > news >正文

EventBus(事件总线)的使用和源码的简单解析

Google Guava EventBus(事件总线)的使用和源码的简单解析

什么是EventBus?

事件总线(EventBus)是一种广泛用于软件架构中的设计模式,用于实现解耦和松散耦合的通信机制。它可以帮助组织和管理应用程序中不同组件之间的通信,以提高应用程序的可维护性、可扩展性和灵活性。

在事件总线模式中,不同的组件通过订阅和发布事件来进行通信。发布者发布一个事件,订阅者可以订阅该事件并在事件发生时执行相关的操作。事件总线充当了中介者的角色,它负责管理所有的事件和订阅者,并确保事件正确地传递到所有订阅者。

事件总线模式可以在很多场景中使用,例如 Android 应用程序中的通信、分布式系统中的消息传递等。常见的事件总线实现包括 Google Guava 的 EventBus 和 Square 的 Otto。

总的来说,EventBus就是应用了发布者/订阅者模式,用来帮助我们进行各组件间通信的工具。我们这里解析的是Google Guava 的 EventBus,它的官方文档在这里

EventBus三要素

github上的图:
来自官网的图片
EventBus里有三个比较重要的概念:

  1. Event 消息事件
  2. Publisher 消息发布者
  3. Subscriber 消息订阅者

乍一看这订阅者/发布者模式还和观察者模式有几分相似之处,不同之处就在于EventBus的存在,它作为一个中间件充当了消息传递的助手,使得订阅者不必直接订阅发布者,订阅事件总线即可。

EventBus的五种线程模型

EventBus的线程模型指的是根据事件的发布和订阅所在的线程,决定应该在哪个线程中处理事件。EventBus中有以下四种线程模型:

    1. POSTING(默认):在发布事件的线程中执行,即同步执行,速度快。
    1. MAIN:在主线程(UI线程)中执行,如果当前线程是主线程,直接执行订阅方法;否则,通过主线程的Poster来执行。
    1. BACKGROUND:在后台线程中执行,如果当前线程是主线程,通过后台的Poster来执行;否则,直接执行订阅方法。
    1. ASYNC:在新的子线程中执行,每个事件都会创建一个新的子线程,即异步执行,速度较慢。

除此之外,还有一种模型:

    1. MAIN_ORDERED :MAIN_ORDERED 模式也是运行在主线程上的模式,不同于 MAIN 模式的是,它保证了事件的顺序性。也就是说,当一个事件在主线程中被发布时,它会先进入一个队列中,之后再一个个的被处理。这样可以保证相同事件类型的事件按照发送的顺序依次被执行。如果当前线程不是主线程,那么它就直接被执行,这也是为了避免在子线程中的事件被阻塞。

我们会在消息处理方法中利用@Subscribe注解指定线程模型。

EventBus的简单使用

使用EventBus三步走

这里我们根据github上的三步分为四步走:

  1. 定义Event事件类:
public class MessageEvent {private String message;public MessageEvent(String message){this.message = message;}public String getMessage(){return message;}public void setMessage(String message){this.message = message;}
}

这里定义了一个MessageEvent用于传递事件。

  1. 声明订阅者,注册订阅方法并且指定线程模型
    @Subscribe(threadMode = ThreadMode.MAIN)//选择线程模型,说明事件将在主线程中处理public void onMoonEvent(MessageEvent messageEvent){tv_message.setText(messageEvent.getMessage());}

这里在MainActivity里注册了一个订阅方法(消息处理方法),指定了线程模型为MAIN,说明订阅方法在主线程里执行。完整Demo在后面放出。

3.注册订阅者与订阅默认的事件总线

   bt_subscription.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//MainActivity注册了这条事件总线if(!EventBus.getDefault().isRegistered(MainActivity.this)){EventBus.getDefault().register(MainActivity.this);}}});

这里先判断MainActivity是否已经与默认的事件总线订阅,如果没有订阅就进行订阅。

4.发送消息事件–触发订阅方法

    bt_message.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {EventBus.getDefault().post(new MessageEvent("欢迎来到,德莱联盟"));finish();}});

这里我在第二个Activity里发送了一个消息事件到默认的事件总线中,这样MainActivity中的订阅方法就会被触发。

2.小Demo

这里我贴出我的小Demo:

  1. MainActivity:
public class MainActivity extends AppCompatActivity {private TextView tv_message;private Button bt_message;private Button bt_subscription;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tv_message = findViewById(R.id.tv_message);bt_message = findViewById(R.id.bt_message);bt_subscription = findViewById(R.id.bt_subscription);bt_message.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {startActivity(new Intent(MainActivity.this,SecondActivity.class));}});bt_subscription.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//MainActivity注册了这条事件总线if(!EventBus.getDefault().isRegistered(MainActivity.this)){EventBus.getDefault().register(MainActivity.this);}}});}@Overrideprotected void onDestroy() {super.onDestroy();EventBus.getDefault().unregister(this);}//通过参数类型来区分应该执行哪个方法@Subscribe(threadMode = ThreadMode.MAIN)//选择线程模型,说明事件将在主线程中处理public void onMoonEvent(MessageEvent messageEvent){tv_message.setText(messageEvent.getMessage());}@Subscribe(threadMode = ThreadMode.MAIN,sticky = true)public void ononStickyEvent(MessageEvent messageEvent){tv_message.setText(messageEvent.getMessage());}@Subscribe(threadMode = ThreadMode.MAIN)public void secondBack(String mes){Toast.makeText(this, mes, Toast.LENGTH_SHORT).show();}
}
  1. SecondActivity:
public class SecondActivity extends AppCompatActivity {TextView tv_message;Button bt_message;Button bt_sticky;Button mes;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_second);tv_message = findViewById(R.id.tv_message1);bt_message = findViewById(R.id.bt_message1);bt_sticky = findViewById(R.id.bt_sticky);mes = findViewById(R.id.mes_event);bt_message.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {EventBus.getDefault().post(new MessageEvent("欢迎来到,德莱联盟"));finish();}});bt_sticky.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {EventBus.getDefault().postSticky(new MessageEvent("粘性事件"));finish();}});mes.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {EventBus.getDefault().post(new String("另一个事件"));finish();}});}
}
  1. MessageEvent:
//自定义的Event事件
public class MessageEvent {private String message;public MessageEvent(String message){this.message = message;}public String getMessage(){return message;}public void setMessage(String message){this.message = message;}
}

https://github.com/MonsterTTL/Android-Impove/tree/master/EventBusDemo
具体的实例代码放在我的github上👆了,里面还涉及到了粘性事件以及多个订阅事件。

源码解析

getDefault()方法

先来看我们最常用的获取EventBus的方法

	static volatile EventBus defaultInstance;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;}

很显然这里用到了DCL单例模式,确保缺省状态下EventBus的实例只有一个。我们顺着这个方法往下捋,看new EventBus()方法。

new EventBus()方法

	private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();public EventBus() {this(DEFAULT_BUILDER);}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;}

很显然,获取default EventBus的调用流程是这样的:getDefault() -> EventBus()->EventBus(DEFAULT_BUILDER)。说道Builder,那么应该是采取建造者模式创建的,我们接下来分析EventBusBuilder。

EventBusBuilder

builder里延伸下去的内容有点多,我们直接先看EventBusBuilder的build方法:

public EventBus build() {return new EventBus(this);}

很显然,该类的作用是构建一个EventBus实例,并提供一些配置选项。该类具有多个属性,如是否记录订阅者异常、是否发送没有订阅者事件等,以及设置自定义线程池和日志处理程序等。类中的各种方法可用于配置这些属性,例如logSubscriberExceptions()方法可用于设置是否记录订阅者异常。 这里我们就不关注打印日志,记录异常等配置了,我们关注具体的创建过程。

我们先来看看以下几个可选的方法:

/*** 设置 EventBus 是否开启事件类继承的支持。默认情况下,EventBus会考虑事件类继承关系,即父类有订阅者时,子类的订阅者也会被
通知。但是,关闭这个特性可以提高事件发布的速度。如果事件类的继承层级比较复杂,关闭继承支持的性能提升应该会大于20%。但需要注意
的是,事件发布通常只占应用程序内部CPU时间的一小部分,除非高速发布事件,例如每秒钟发布数百/数千个事件。                                ***/public EventBusBuilder eventInheritance(boolean eventInheritance) {this.eventInheritance = eventInheritance;return this;}/*** 这是用来指定线程池类型的,除了使用Builder内部自带的线程池,我们也可以使用自己传入的线程池*/public EventBusBuilder executorService(ExecutorService executorService) {this.executorService = executorService;return this;}/*** 这是用来跳过指定订阅者的方法验证的,当你注册一个类作为事件订阅者时,EventBus会自动检查该类是否具有合法的事件订阅方法。* 这些方法必须是公共的,没有返回值,只有一个参数,并且在方法上需要使用@Subscribe注解。然而,有些情况下,你可能希望让某* 个类作为订阅者,但是它并不符合这些要求。例如,它可能有一些不是用@Subscribe注解的方法,但是你仍然希望这些方法能够被* EventBus识别并调用。在这种情况下,你可以使用skipMethodVerificationFor()方法来跳过验证,允许这些方法被注册为事件订阅    * 方法。*/public EventBusBuilder skipMethodVerificationFor(Class<?> clazz) {if (skipMethodVerificationForClasses == null) {skipMethodVerificationForClasses = new ArrayList<>();}skipMethodVerificationForClasses.add(clazz);return this;}/**  允许在生成了索引的情况下也强制使用反射进行订阅者的查找。默认情况下,如果有生成的索引,则会使用它来查找订阅者,而不是使用反射。通过调用这个方法并将其参数设置为 true,可以强制使用反射进行查找。 */public EventBusBuilder ignoreGeneratedIndex(boolean ignoreGeneratedIndex) {this.ignoreGeneratedIndex = ignoreGeneratedIndex;return this;}/** 开启严格方法验证 . 在 EventBus 中,严格检查是指在注册和订阅事件时检查相关方法的参数和返回类型是否正确匹配,以确保事件可以正确地被分发。如果开启了严格检查,那么在注册或订阅事件时,如果发现有方法的参数或返回类型与事件类型不匹配,EventBus 会抛出一个异常,防止程序出现意料之外的错误。*/public EventBusBuilder strictMethodVerification(boolean strictMethodVerification) {this.strictMethodVerification = strictMethodVerification;return this;}/** 用于添加 SubscriberInfoIndex 对象到 EventBusBuilder 中。SubscriberInfoIndex 接口用于提供订阅者类和订阅方法信息的索引,以便 EventBus 能够快速找到订阅者及其订阅方法。在 EventBus 中, */public EventBusBuilder addIndex(SubscriberInfoIndex index) {if (subscriberInfoIndexes == null) {subscriberInfoIndexes = new ArrayList<>();}subscriberInfoIndexes.add(index);return this;}

接下来看下比较重要的几个方法:

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

这个方法是用来安装默认EventBus里的defaultInstance的:

class EventBus{
...static volatile EventBus defaultInstance;//默认的实例private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();//默认的Builder
...
}

这段代码是通过构建者模式构建一个默认的EventBus实例,并安装在默认的EventBus上,只能在第一次使用默认的EventBus之前调用,否则会抛出EventBusException异常。在synchronized块内,首先判断是否已经存在默认的EventBus实例,如果存在则抛出异常,否则构建EventBus实例并将其设置为默认实例,最后返回默认实例。

接下来看另一个方法:

    MainThreadSupport getMainThreadSupport() {if (mainThreadSupport != null) {return mainThreadSupport;} else if (AndroidComponents.areAvailable()) {return AndroidComponents.get().defaultMainThreadSupport;} else {return null;}}

这个方法是用来获取主线程支持的,那什么是主线程支持呢,我们点进去看看:

public interface MainThreadSupport {boolean isMainThread();Poster createPoster(EventBus eventBus);
}

这里我们可以看到,MainThreadSupport是一个接口,定义了isMainThread和createPoster方法,根据这两个方法名,我们可以推测这个接口的作用就是用来支持消息在主线程里传递的,isMainThread用于判断当前线程是否是主线程,createPoster用于创建一个poster以在主线程里传递消息。

当然这只是一个接口,我们要看具体的实现还是得折回去看getMainThreadSupport方法,我们看这一段:

	else if (AndroidComponents.areAvailable()) {return AndroidComponents.get().defaultMainThreadSupport;}

这段代码的作用就是判断当前平台是不是Android平台,如果是Android平台,就调用Android.get().defaultMainThreadSupport获取默认的主线程支持类。那这里又得涉及到AndroidComponents类了,我们开一个子标题讨论这个类。

AndroidComponents类

AndroidComponents类是一个抽象类,内容比较短,这里先贴出它的全部源码:

public abstract class AndroidComponents {private static final AndroidComponents implementation;static {implementation = AndroidDependenciesDetector.isAndroidSDKAvailable()? AndroidDependenciesDetector.instantiateAndroidComponents(): null;}public static boolean areAvailable() {return implementation != null;}public static AndroidComponents get() {return implementation;}public final Logger logger;public final MainThreadSupport defaultMainThreadSupport;public AndroidComponents(Logger logger, MainThreadSupport defaultMainThreadSupport) {this.logger = logger;this.defaultMainThreadSupport = defaultMainThreadSupport;}
}

这里将其声明为抽象的意义应该就是防止其实例化,因为AndroidComponents的具体实现是由AndroidDependenciesDetector来决定的,这个类会检测当前是否在Android环境下,如果是,则返回实际的实现,否则返回null。而AndroidDependenciesDetector这个类是不允许被外部直接实例化的,因此AndroidComponents也应该遵循同样的规则。

真正重要的代码部分是在静态代码块内,代码块在初始化时会会检测当前是否在Android环境下,如果是,则返回实际的实现,否则返回null。

接下来我们再深入向下扒,找到MainThreadSupport的具体实现类,再往下看我们可以发现这个类是由反射机制实现的,不过我们先不关心这个,直接找到具体实现类DefaultAndroidMainThreadSupport,直接看它的源码:

public class DefaultAndroidMainThreadSupport implements MainThreadSupport {@Overridepublic boolean isMainThread() {return Looper.getMainLooper() == Looper.myLooper();}@Overridepublic Poster createPoster(EventBus eventBus) {return new HandlerPoster(eventBus, Looper.getMainLooper(), 10);}
}

可以发现,这个类实际上十分简单,通过Looper判断是否是主线程,接下来看他的Poster类。
具体的Poster类:

public class HandlerPoster extends Handler implements Poster {private final PendingPostQueue queue;private final int maxMillisInsideHandleMessage;private final EventBus eventBus;private boolean handlerActive;public HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {super(looper);this.eventBus = eventBus;this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;queue = new PendingPostQueue();}public void enqueue(Subscription subscription, Object event) {PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);synchronized (this) {queue.enqueue(pendingPost);if (!handlerActive) {handlerActive = true;if (!sendMessage(obtainMessage())) {throw new EventBusException("Could not send handler message");}}}}
...

我们先来看这前半部分,在构造方法中新出现了一个queue,是一个post队列,是用于存储post请求的队列,具体的PendingPostQueue类我们就不在具体看了,只要知道其内部使用链表的结构连接的,而PendingPostQueue类里记录了这个post队列的头部和尾部。

enqueue方法中将发送的Post请求进行入队,如果handler不处于活跃状态,就将其置位活跃状态,然后获取一个Message对象并将其发送给MessageQueue中,接下来我们看它是如何处理post请求的:

public void handleMessage(Message msg) {boolean rescheduled = false;try {long started = SystemClock.uptimeMillis();while (true) {PendingPost pendingPost = queue.poll();if (pendingPost == null) {synchronized (this) {// Check again, this time in synchronizedpendingPost = queue.poll();if (pendingPost == null) {handlerActive = false;return;}}}eventBus.invokeSubscriber(pendingPost);long timeInMethod = SystemClock.uptimeMillis() - started;if (timeInMethod >= maxMillisInsideHandleMessage) {if (!sendMessage(obtainMessage())) {throw new EventBusException("Could not send handler message");}rescheduled = true;return;}}} finally {handlerActive = rescheduled;}}

由于HandlerPoster本身就是一个Handler,所以上一步发送的Message实际上就是由它自己处理的,就是在这个方法中处理,Post请求具体是在这里执行的:

eventBus.invokeSubscriber(pendingPost);

这个方法会将Event事件推送到具体的订阅者类中去执行,接下来我们重新回到EventBus类中看看这个过程是怎样执行的。

Subscription类

在正式介绍EventBus是怎样执行推送之前,我们需要先了解两个前置类的信息,首先我们先来看Subscriptiont类:

final class Subscription {final Object subscriber;final SubscriberMethod subscriberMethod;/*** Becomes false as soon as {@link EventBus#unregister(Object)} is called, which is checked by queued event delivery* {@link EventBus#invokeSubscriber(PendingPost)} to prevent race conditions.*/volatile boolean active;Subscription(Object subscriber, SubscriberMethod subscriberMethod) {this.subscriber = subscriber;this.subscriberMethod = subscriberMethod;active = true;}@Overridepublic boolean equals(Object other) {if (other instanceof Subscription) {Subscription otherSubscription = (Subscription) other;return subscriber == otherSubscription.subscriber&& subscriberMethod.equals(otherSubscription.subscriberMethod);} else {return false;}}@Overridepublic int hashCode() {return subscriber.hashCode() + subscriberMethod.methodString.hashCode();}
}

这个类里面的具体的SubscriberMethod和Object subscriber就不详细介绍了,根据名字我们也可以知道,final Object subscriber 存储的是订阅事件的对象,而 final SubscriberMethod subscriberMethod 则包含了订阅事件的方法的信息,例如该方法的名称、参数类型等等。通过这两个属性,EventBus 可以将事件正确地分发给订阅者的对应方法。

PendingPost类

然后我们再来看PendingPost类

final class PendingPost {private final static List<PendingPost> pendingPostPool = new ArrayList<PendingPost>();Object event;Subscription subscription;PendingPost next;...}

我们看到这个类里有一个静态的变量:

private final static List<PendingPost> pendingPostPool = new ArrayList<PendingPost>();

就是说,所有的PendingPost类的实例都会共享这一个池子,这个池子主要是用来优化GC性能的,用于重用已创建的对象,避免了大量的对象创建和销毁,减小了内存开销。

1.构造方法

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

其创建方法主要是传入发送的事件和订阅信息相关的Subscription,然后将其包装成一个PendingPost对象。

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

obtainPendingPost方法是根据pendingPostPool这个共享池来创建PendingPost对象,如果共享池里还有可用对象,就复用这个对象来创建新的PendingPost对象,否则就直接创建一个新的对象。

 static void releasePendingPost(PendingPost pendingPost) {pendingPost.event = null;pendingPost.subscription = null;pendingPost.next = null;synchronized (pendingPostPool) {// Don't let the pool grow indefinitelyif (pendingPostPool.size() < 10000) {pendingPostPool.add(pendingPost);}}}

最后是release方法,这个方法是用来将PendingPost对象释放会共享池中的,不过这个共享池的大小也是有限制的,最大不能超过10000。

EventBus类中如何推送事件

上面说到,具体是通过invokeSubscriber这个方法来推送事件的,事不宜迟,我们马上来看看这个方法的内容:

    void invokeSubscriber(PendingPost pendingPost) {Object event = pendingPost.event;Subscription subscription = pendingPost.subscription;PendingPost.releasePendingPost(pendingPost);if (subscription.active) {invokeSubscriber(subscription, event);}}

可以发现,具体又调用了invokeSubscriber(subscription, event);方法,我们紧接着看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);}}

很显然,这里是通过了反射机制,subscription.subscriberMethod.method.invoke(subscription.subscriber, event)表示将event事件发送到subscriber中并触发subscriber的处理方法。method是SubscriberMethod中的一个字段,表示订阅方法本身,通过invoke方法来调用这个方法,并将subscriber作为方法的调用者,event作为参数传递进去。到这里,我们就看完Event事件是如何传递给订阅者的了。

Register(订阅事件总线)

接下来我们看第二个内容,订阅事件总线时做了什么:

public void register(Object subscriber) {...Class<?> subscriberClass = subscriber.getClass();List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);synchronized (this) {for (SubscriberMethod subscriberMethod : subscriberMethods) {subscribe(subscriber, subscriberMethod);}}}

核心方法在于subscribe方法:

    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {Class<?> eventType = subscriberMethod.eventType;Subscription newSubscription = new Subscription(subscriber, subscriberMethod);CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);//获取同一个类型的消息事件下的所有Subscrption对象if (subscriptions == null) {subscriptions = new CopyOnWriteArrayList<>();subscriptionsByEventType.put(eventType, subscriptions);//如果原来还没有注册这一类型的消息事件,则新创建一个List并加入} 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;}}//这一段是用来将新的subscription对象插入到同一类型的消息事件队列的合适位置中--将订阅者与注册方法关联起来List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);if (subscribedEvents == null) {subscribedEvents = new ArrayList<>();typesBySubscriber.put(subscriber, 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);}}}

具体来说,subscribe方法做了三件事情:
1. 维护了当前EventBus中的subscriptionsByEventType表;
2. 维护了当前EventBus中的 typesBySubscriber表;
3. 处理了粘性事件;

详细的说明我放在上面的注释里面了,我们先说维护的两个表,第一个表是代表消息事件类型和Subscription关联的一个表,
第二个表是表示订阅者Subscriber和消息事件类型关联的两个表,这两个表可以在发布事件时可以快速地找到对应的订阅者并调用其处理方法。

然后是粘性事件的处理,这里有一个标志位eventInheritance,这个标志位我们在之前提到过,我们可以回到EventBuilder的内容查看,主要是涉及到继承问题的,除去继承的问题,粘性事件主要就是调用:

   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());}}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 subscriberinvokeSubscriber(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方法,该方法根据回调方法指定的线程模型来传递粘性事件,至于上面提到的三种poster,我们之后再解析。

Post方法

订阅者注册完毕后,我们就要用Post方法进行消息的发送了,Post方法又可以分为post和postSticky,接下来我们先看post方法:

    public void post(Object event) {PostingThreadState postingState = currentPostingThreadState.get();List<Object> eventQueue = postingState.eventQueue;//获取发送线程的eventQueueeventQueue.add(event);//将当前要发送时间添加进eventQueue中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()) {//eventQueue不为空postSingleEvent(eventQueue.remove(0), postingState);//通过postSingleEvent方法依次将eventQueue中的事件发送}} finally {postingState.isPosting = false;postingState.isMainThread = false;}}}

这里又冒出来了一个PostingThreadState,我们看它是从哪里来的:

    private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {@Overrideprotected PostingThreadState initialValue() {return new PostingThreadState();}};
...final static class PostingThreadState {final List<Object> eventQueue = new ArrayList<>();boolean isPosting;boolean isMainThread;Subscription subscription;Object event;boolean canceled;}

这里涉及到一个ThreadLocal类,我们先来看看:

在Java中,ThreadLocal是一个非常有用的类,它允许您在单个线程中存储和检索数据,而不会与其他线程共享。ThreadLocal通过创建一个副本来解决线程安全问题,每个线程都有自己的副本,所以每个线程都可以独立地改变自己的副本,而不会影响其他线程。
ThreadLocal提供了三个主要方法:
get() - 返回当前线程的变量副本(如果有);如果没有,则返回默认值。
set(T value) - 设置当前线程的变量副本。
remove() - 移除当前线程的变量副本。
ThreadLocal通常用于在单个线程中存储和检索上下文信息,例如数据库连接、事务对象等。使用ThreadLocal可以避免在多线程环境下对这些资源进行同步操作,从而提高性能。

所以在这里的语境下,PostingThreadState就是每个发送post请求线程中的局部变量,且各个发送线程之间是不会共享这个PostingThreadState变量的,每个发送post请求的线程都会有自己私有的PostingThreadState变量。

post方法做的大致如下:

  1. 先获取到当前发送线程的PostingThreadState变量

  2. 接着获取当前线程要发送的事件队列eventQueue,然后将当前要发送的事件添加进入eventQueue队列中

  3. 如果当前发送线程处于空闲状态,就将eventQueue中的事件通过postSingleEvent方法依次发送

    既然说调用了postSingleEvent方法,接下来我们理所当然地查看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));}}}

这里又涉及到了eventInheritance标志位,这里我们不管这个标志位,查看这个方法的核心在于:

 subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);

我们接着查看这个postSingleEventForEventType方法:

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.event = null;postingState.subscription = null;postingState.canceled = false;}if (aborted) {break;}}return true;}return false;}

这个方法接收了发送的事件event,发送线程的局部变量postingState,还有事件类型eventClass。接下来它获取了对应事件类型下的subscriptions队列,对其中的每个subscription对象调用了postToSubscription方法,如果一切成功,就返回true,否则返回false。至于postToSubscription这个方法我们在之前也提到过,就是:

 private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) 

上次提到它是用来传递粘性事件,当然它也可以用来传递普通事件,这个方法将根据指定的线程模型来选择如何传递事件给订阅者回调处理。

至于粘性事件的POST:

    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 immediatelypost(event);}

不同点就在于在执行post方法之前还将其加入到了粘性事件队列stickyEvents,以便在其他订阅者完成注册时能接收到粘性事件。

三种Poster

之前提到过EventBus里有三种Poster,在MainThreadSuppot中我们已经认识了HandlerPoster,接下来我们看看其他两种Poster,这里我们为了方便,贴出postToSubscription的源码方便理解:

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 subscriberinvokeSubscriber(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);}}

很显然,根据不同的线程模型,该方法会采取的不同的策略和poster,mainThreadPoster就是我们一开始解析过的HandlerPoster,接下来先看backgroundPoster:

	private final PendingPostQueue queue;private final EventBus eventBus;private volatile boolean executorRunning;BackgroundPoster(EventBus eventBus) {this.eventBus = eventBus;queue = new PendingPostQueue();}

BackgroundPoster内部会有自己的一条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);}}}

这里BackgroundPoster用了同步代码块,说明虽然Background模型是在子线程中运行的,但是其会遵循顺序依次执行,同一个发送队列发送的事件触发的多个Background线程模型的回调方法不能同时运行。

至于任务该如何执行,eventBus中会有一个线程池,这个线程池类型是根据builder决定的,默认情况下,这个线程池的类型将会是CachedThreadPool。接下来再看它的run方法:

public void run() {try {try {while (true) {PendingPost pendingPost = queue.poll(1000);if (pendingPost == null) {synchronized (this) {// Check again, this time in synchronizedpendingPost = 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;}}

run方法很简单,就是不断将queue中的PendingPost取出放入线程池中执行eventBus的invokeSubscriber方法:
在这里插入图片描述
接下来我们看AsncPoster:

AsyncPoster(EventBus eventBus) {this.eventBus = eventBus;queue = new PendingPostQueue();}public void enqueue(Subscription subscription, Object event) {PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);queue.enqueue(pendingPost);eventBus.getExecutorService().execute(this);}@Overridepublic void run() {PendingPost pendingPost = queue.poll();if(pendingPost == null) {throw new IllegalStateException("No pending post available");}eventBus.invokeSubscriber(pendingPost);}

AsyncPoster和BackgroundPoster其实有点像,不同之处就在于AsyncPoster的post请求不必顺序执行,多个回调可以同时进行。

总结

到此为止,我们就已经解析了EventBus源码的核心基础部分了,让我们总结一下流程图:在这里插入图片描述
这里的流程图是简化的版本,这里具体实现的细节被隐藏起来了,主要是帮助我们理解机制流程。

相关文章:

EventBus(事件总线)的使用和源码的简单解析

Google Guava EventBus(事件总线)的使用和源码的简单解析 什么是EventBus&#xff1f; 事件总线&#xff08;EventBus&#xff09;是一种广泛用于软件架构中的设计模式&#xff0c;用于实现解耦和松散耦合的通信机制。它可以帮助组织和管理应用程序中不同组件之间的通信&…...

《汇编语言》- 读书笔记 - 第2章-寄存器

《汇编语言》- 读书笔记 - 第2章-寄存器 2.0 8086CPU 寄存器段地址:偏移地址 2.1 通用寄存器2.2 字在寄存器中的存储2.3 几条汇编指令表2.1汇编指令举例表2.2 程序段中指令的执行情况之一问题 2.1表2.3 程序段中指令的执行情况之二问题 2.2 检测点 2.12.4 物理地址2.5 16位结构…...

English Learning - L3 综合练习 1 VOA-Color 2023.04.26 周三

English Learning - L3 综合练习 1 VOA-Color 2023.04.26 周三 主题整体听一遍精听句子 1扩展 way of doing | way to do sth 句子 2扩展 Expression扩展 base 句子 3句子 4扩展 red-hot 句子 5句子 6扩展 fiery 句子 7句子 8句子 9句子 10句子 11扩展 born 句子 12句子 13句子…...

50道web前端工程师面试题及答案解析,你学会了吗

简介&#xff1a;本文包含了50个实用的前端面试题及答案解析&#xff0c;涵盖了HTML、CSS、JavaScript、DOM、Ajax、MVC、模块化、ES6、SPA、Webpack、Babel、Virtual DOM、响应式设计、移动优先设计、响应式图片、CSS 预处理器、后处理器、模块化、布局、盒模型、浮动、定位、…...

【链表OJ题 1】反转链表

目录 题目来源&#xff1a; 代码实现 1、方法一 1.1分析 2、方法二 2.1 分析 题目来源&#xff1a; 力扣 题目描述&#xff1a; 代码实现 1、方法一 struct ListNode* reverseList(struct ListNode* head) {struct ListNode* prev NULL, * cur head;while (cur){st…...

【华为OD机试真题】计算网络信号 (javaC++python)100%通过率 超详细代码注释

计算网络信号 知识点广搜数组 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 网络信号经过传递会逐层衰减,且遇到阻隔物无法直接穿透,在此情况下需要计算某个位置的网络信号值,注意:网络信号可以绕过阴隔物array[m][n]的一维数组代表网格地图,array[i][j]=0代表i…...

Tomcat8和Tomcat9乱码问题

今天新开了一个小项目&#xff0c;我丢&#xff0c;乱码了&#xff0c;咋回事&#xff0c;好久没遇到过了&#xff0c;都忘了咋回事。今天必须记录下来&#xff0c;避免继续踩坑 Tomcat 8 不需要进行任何配置即可&#xff0c;它默认的是GBK&#xff0c;而win10 win7 默认的也是…...

Lesson13 IP协议

IP: 提供一种能力,将数据从A主机送到B主机的能力,但不一定会成功 主机 : 配有 IP 地址 , 但是不进行路由控制的设备 ; 路由器: 即配有 IP 地址 , 又能进行路由控制 ; 节点 : 主机和路由器的统称; 协议头格式 如何封装和解包: 定长报头 自描述字段 如何交付(分用) : 8…...

【每日一题Day192】LC1033移动石子直到连续 | 分类讨论 贪心

移动石子直到连续【LC1033】 三枚石子放置在数轴上&#xff0c;位置分别为 a&#xff0c;b&#xff0c;c。 每一回合&#xff0c;你可以从两端之一拿起一枚石子&#xff08;位置最大或最小&#xff09;&#xff0c;并将其放入两端之间的任一空闲位置。形式上&#xff0c;假设这…...

2023年软件测试常见面试题100%问必背全套教程

随着数字化时代的到来&#xff0c;软件测试越来越受到重视。在未来的几年里&#xff0c;软件测试将继续成为信息技术领域中的热门职业之一。如果你是一名正在寻找或准备进入软件测试行业的人&#xff0c;那么这套常见面试题全套教程对你来说会非常有用。 这套教程旨在帮助你了…...

TypeScript 基本概念

TypeScript 是什么&#xff1f; 目标&#xff1a;能够说出什么是 TypeScript TS 官方文档 TS 中文参考 - 不再维护 TypeScript 简称&#xff1a;TS&#xff0c;是 JavaScript 的超集&#xff0c;JS 有的 TS 都有 TypeScript Type JavaScript&#xff08;在 JS 基础之上…...

libfacedetection 人脸检测库 检测速度慢的问题

目录 一、libfacedetection 性能介绍 英特尔CPU 使用AVX2指令集 使用AVX512指令集 嵌入式设备 二、加速检测速度 libfacedetetion的前向推理速度很快的原因 使用axv2加速指令 一、libfacedetection 性能介绍 在上一篇文章中&#xff0c;我发现使用摄像头检测&#xff0c;构…...

项目骨架搭建

CSS样式补充 精灵图 CSS精灵图&#xff08;CSS Sprites&#xff09;是一种网页优化技术&#xff0c;通过将多个小图像合并成一个大图像&#xff0c;然后通过CSS的背景定位&#xff08;background-position&#xff09;属性来显示对应的图像部分。这种技术可以减少HTTP请求次数…...

“火灾不分昼夜,安全在我心中”——五一前厂房消防检查纪实

检查人员: Scott, Jason, Willson, Hanson 检查时间: 2023年4月28日 检查地点: 1厂房、2厂房室内外 检查内容: 一、室内外消火栓: 室内栓外观正常&#xff1b; 室外栓: 栓体防冻防尘套破损、遗失&#xff0c;消防栓缺少防撞保护&#xff1b; 按规定距离厂房外墙不宜小于5…...

UNIX环境高级编程——进程关系

9.1 引言 本章详细说明进程组以及会话的概念&#xff0c;还将介绍登录shell&#xff08;登录时所调用的&#xff09;和所有从登录shell启动的进程之间的关系。 9.2 终端登录 9.3 网络登录 9.4 进程组 每个进程除了有一进程ID之外&#xff0c;还属于一个进程组&#xff0c;进…...

C# ref和out用法和区别

首先&#xff1a;两者都是按地址传递的&#xff0c;使用后都将改变原来参数的数值。 其次&#xff1a;ref可以把参数的数值传递进函数&#xff0c;但是out是要把参数清空&#xff0c;就是说你无法把一个数值从out传递进去的&#xff0c;out进去后&#xff0c;参数的数值为空&am…...

信息复制的革命:印刷术【提高信噪比】

文章目录 引言I 保证信息不被噪音所影响1.1 校对抄写错误的方法1.2 印刷术II 雕版印刷和活字印刷2.1 雕版印刷术2.2 毕昇的胶泥活字印刷2.3 古腾堡的铅活字印刷引言 科学的诞生,丰富了信息产生的源头。文字和纸张,加速了信息的传播和文明的进步。I 保证信息不被噪音所影响 复…...

【MySQL】事务

事务是一组操作的集合,我们将一组操作视为一个整体,所以事务里面的操作的时候要么同时成功,要么同时失败,之所以会有事务也是因为我们在实际生活中会用到 最典型的例子就是转账操作:A向B进行转账,A这边扣款成功的同时B那边一定是收款成功的,如果没有事务的话就会出现A扣款成功但…...

学习HCIP的day.03

目录 OSPF&#xff1a;开放式最短路径优先协议 OSPF的数据包 -- 5种 OSPF的状态机 OSPF的工作过程 OSPF的基础配置 关于OSPF协议从邻居建立成为邻接的条件 OSPF的接口网络类型 OSPF&#xff1a;开放式最短路径优先协议 无类别链路状态型IGP协议&#xff1b;由于其基于拓…...

Maven项目的配置

Maven是什么&#xff1f;它的作用是什么&#xff1f; Maven是一种开源的构建工具&#xff0c;它可以自动化构建、测试、部署和管理Java项目。它提供了一个中心化的构建过程&#xff0c;包括依赖管理、项目结构管理、插件管理等&#xff0c;使得开发人员更方便地维护和协作应用…...

Spring Boot使用(基础)

目录 1.Spring Boot是什么? 2.Spring Boot使用 2.1Spring目录介绍 2.2SpringBoot的使用 1.Spring Boot是什么? Spring Boot就是Spring脚手架,就是为了简化Spring开发而诞生的 Spring Boot的优点: 1.快速集成框架,提供了秒级继承各种框架,提供了启动添加依赖的功能 2.内…...

6WINDGate-overview

6WINDGate Overview Author&#xff1a;Once Day Date&#xff1a;2023年4月29日 本文是对6WIND官网文档的整理和翻译&#xff0c;仅供学习和研究之用&#xff0c;原始文章可参考下面文档&#xff1a; 6WINDGate Documentation - 6WIND6WINDGate Modules — 6WINDGate Modul…...

Java8新特性-流式操作

在Java8中提供了新特性—流式操作&#xff0c;通过流式操作可以帮助我们对数据更快速的进行一些过滤、排序、去重、最大、最小等等操作并且内置了并行流将流划分成多个线程进行并行执行&#xff0c;提供更高效、快速的执行能力。接下来我们一起看看Java8为我们新增了哪些便捷呢…...

Nautilus Chain Layer 3 圆桌会议圆满举办,超4.8K用户观看

在 4 月 21 日&#xff0c;Nautilus Chain 举办了以 “Layer 3 区块链的意义和发展以及Crypto的演变”为主题的线上圆桌会议&#xff0c;我们邀请了众多行业嘉宾包括 GitcoinDAO社区管理者Bob jiang、Whalers Community 发起者崔棉大师、Chatpuppy 联合创始人 古千峰、Whalers …...

本地elasticsearch中文分词器 ik分词器安装及使用

ElasticSearch 内置了分词器&#xff0c;如标准分词器、简单分词器、空白词器等。但这些分词器对我们最常使用的中文并不友好&#xff0c;不能按我们的语言习惯进行分词。 ik分词器就是一个标准的中文分词器。它可以根据定义的字典对域进行分词&#xff0c;并且支持用户配置自…...

Java 中的异常处理机制是什么?如何使用它来处理程序中的异常?(七)

Java 中的异常处理机制是一种重要的编程技术&#xff0c;它能够帮助程序员更好地管理程序中出现的异常情况。本文将详细介绍 Java 中的异常处理机制&#xff0c;并提供示例来说明如何使用异常处理机制来捕获和处理程序中的异常。 什么是异常&#xff1f; 在程序运行过程中&am…...

基于UDQ的并网单相逆变器控制【同步参考系下单相并网全桥正弦PWM逆变器闭环控制】(Simulink)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

JAVA开发——常用的注解

目录 spring spring MVC Spring Boot AOP MyBatis MyBatis-Plus JavaWeb开发 spring Autowired&#xff1a;自动安装&#xff0c;通过类匹配自动注册相应的Bean。 Component&#xff1a;将一个通用的 Java 类标记为 Bean&#xff0c;由 Spring 容器管理。 Controller&…...

【Java笔试强训 24】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、选择题 二、编程题 &#x1f525;年终奖 …...

SpringCloud详解

SpringCloud是一个基于SpringBoot的分布式系统开发框架&#xff0c;它能够帮助我们快速、稳定地构建分布式系统。本篇博客将对SpringCloud进行详细解析&#xff0c;介绍SpringCloud的主要组件和相关应用场景&#xff0c;同时提供代码示例以帮助读者更好地掌握SpringCloud的实际…...

最高级网站建设/百度企业官网

albert1017Linux下压缩某个文件夹&#xff08;文件夹打包&#xff09; tar -zcvf /home/xahot.tar.gz /xahottar -zcvf 打包后生成的文件名全路径 要打包的目录例子&#xff1a;把/xahot文件夹打包后生成一个/home/xahot.tar.gz的文件。 # tar -xf all.tar 这条命令是解出all.t…...

网站建设如何不被忽悠/各大网站收录查询

1.Intent&#xff1a;一个Intent就是对一次将要执行的操作的抽象描述 2.拨打电话&#xff0c;发送短信 3.启动新的Activity&#xff0c;传递参数和返回参数...

wordpress 搜索结果分类/谷歌sem推广

大数据是什么&#xff1f;在维克托迈尔-舍恩伯格及肯尼斯库克耶编写的《大数据时代》中提出&#xff1a;大数据指不用随机分析法&#xff08;抽样调查&#xff09;这样捷径&#xff0c;而采用所有数据进行分析处理。那么究竟多大的数据算是大数据&#xff0c;这个其实并没有明确…...

找外包公司做网站的好处和坏处/手机百度app

信息技术期末教学质量分析报告纵观这次的考试总得都考得不错&#xff0c;对这次考试总结如下&#xff1a;一、情况分析&#xff1a;本次质量检测&#xff0c;考查了三、四年级的教学情况&#xff0c;三年级成绩分析年级人数测试人数合格率优秀率平均分172172100%80%91分四年级成…...

免费做任务赚钱的网站有哪些/网络推广外包公司排名

1、左下角应用程序 2、找这个Ubuntu软件 3、找添加附件 4、编解码器 5、按照我的第二行第一个点开&#xff08;自己按照名字或简介找自己的&#xff09; 6、点安装&#xff0c;可能要等好一会&#xff0c;才1.6M............. 7、然后在去试试能不能播放&#xff0c;不行的&…...

苏州网站开发的企业/百度竞价价格查询

原作AlunE, 链接&#xff1a;https://blog.51cto.com/alun51cto/2423000在django项目中, 一个工程中存在多个APP应用很常见&#xff1b;有时候希望不同的APP连接不同的数据库&#xff0c;这个时候需要建立多个数据库连接。默认数据库设置在Django的setting中使用DATABASES设置定…...