2024年Android最新聊一聊 EventBus 源码和设计之禅,安卓面试宝典

推荐学习资料


  • 脑图
    360°全方位性能调优

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
// 根据 priority 大小放入 List 中
subscriptions.add(i, newSubscription);
break;
}
}

List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
subscribedEvents.add(eventType);

// 省略 sticky 事件
}

subscriptionsByEventType 根据 Event 事件类类型获取订阅信息链表,当然,如果没有的话那就 new 一个并放入其中。接着根据订阅方法的优先级塞入该链表中。最后 typesBySubscriber 获取该 subsciber 的所有 Event 事件类型链表,并添加当前 Event 事件类型。关于 sticky 事件的具体内容在 sticky 中会具体讲解。

至此 EventBus#register(Object) 方法算是结束了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

post()

EventBus#post(Object) 源码精简如下:

public void post(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
List eventQueue = postingState.eventQueue;
eventQueue.add(event);

// 确保不会被调用多次
if (!postingState.isPosting) {
postingState.isMainThread = isMainThread();
postingState.isPosting = true;
try {
while (!eventQueue.isEmpty()) {
// 分发 Event 事件
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
// 最后要 reset flag
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}

currentPostingThreadState 是一个 ThreadLocal 类,通过它获取到 PostingThreadState 对象,再根据该对象获取到 event 链表(有没有联想到 Android 中的消息机制?),并将传入的 event 塞入该链表。为了控制 Event 出队列不会被调用多次,PostingThreadState 对象有一个 isPosting 来标记当前链表是否已经开始进行回调操作,通过源码可以看到,每次分发完一个 Event 事件,该事件也会被从链表中 remove 出去。

postSingleEvent()

具体 postSingleEvent() 源码精简如下:

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class<?> eventClass = event.getClass();
postSingleEventForEventType(event, postingState, eventClass);
}

追溯 EventBus#postSingleEventForEventType() 源码精简如下:

private void postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
CopyOnWriteArrayList subscriptions;
synchronized (this) {
subscriptions = subscriptionsByEventType.get(eventClass);
}
if (subscriptions != null && !subscriptions.isEmpty()) {
for (Subscription subscription : subscriptions) {
postingState.event = event;
postingState.subscription = subscription;
try {
postToSubscription(subscription, event, postingState.isMainThread);
} finally {
postingState.event = null;
postingState.subscription = null;
postingState.canceled = false;
}
}
}
}

通过 subscriptionsByEventType 获取该 Event 事件对应的订阅信息链表,然后将该订阅信息Event 和当前线程信息传给了 postToSubscription() 方法,该方法戳进去一看就知道是用来去回调所有订阅方法的,该方法的具体分析在 threadMode 中。实际上到这里 post() 流程就算是结束了。所以实际上核心方法 post() 的源码是十分简单的,也可以看得到,核心字段也仅有 subscriptionsByEventType 一个而已。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

unregister()

EventBus#unregister(Object) 方法源码精简如下:

public synchronized void unregister(Object subscriber) {
List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber); if (subscribedTypes != null) { for (Class<?> eventType : subscribedTypes) {
unsubscribeByEventType(subscriber, eventType);
}
typesBySubscriber.remove(subscriber);
}
}

整体看来分两步走,一步是移除注册对象和其所有 Event 事件链表,即 typesBySubscriber 移除相关键值对的;再就是在 unsubscribeByEventType() 方法中对 subscriptionsByEventType 移除了该 subscriber 的所有订阅信息(可以看到实际上没有对 METHOD_CACHE 进行相关移除操作,便于下一次注册的时候可以很方便拿到之前的信息,这便是缓存的作用所在)。

threadMode

在 EventBus 中,共有四种 threadMode,如下:

public enum ThreadMode {
POSTING,

MAIN,

MAIN_ORDERED,

BACKGROUND,

ASYNC
}

  • POSTING:接收事件方法应执行在发射事件方法所在的线程(由于发射事件方法线程可能是主线程,这意味着接收方法不能执行耗时操作,否则会阻塞主线程)
  • MAIN:在 Android 中则接收事件方法应执行在主线程,否则(在 Java 项目中)等同于 POSTING。如果发射事件方法已位于主线程,那么接收事件方法会被「立即」调用(这意味着接收事件方法不能执行耗时操作,否则会阻塞主线程;同时,由于是「立即」调用,所以发射事件方法此时是会被接收事件方法所阻塞的),否则等同于 MAIN_ORDERED
  • MAIN_ORDERED:在 Android 中则接收事件方法会被扔进 MessageQueue 中等待执行(这意味着发射事件方法是不会被阻塞的),否则(在 Java 项目中)等同于 POSTING
  • BACKGROUND
  • 在 Android 中
  • 发射事件方法在主线程中执行,则接收事件方法应执行在子线程执行,但该子线程是 EventBus 维护的单一子线程,所以为了避免影响到其他接收事件方法的执行,该方法不应太耗时避免该子线程阻塞。
  • 发射事件方法在子线程中执行,则接收事件方法应执行在发射事件方法所在的线程。
  • 在 Java 项目中,接收事件方法会始终执行在 EventBus 维护的单一子线程中。
  • ASYNC:接收方法应执行在不同于发射事件方法所在的另一个线程。常用于耗时操作,例如网络访问。当然,尽量避免在同一个时间大量触发此类型方法,尽管 EventBus 为此专门创建了线程池来管理回收利用这些线程。

关于以上 threadMode 哪几种应避免耗时操作,耗时时阻塞的是哪条线程,希望各位读者能够仔细阅读。

说完几种 threadMode 之后,再来看看前文遗留下来的问题——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 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);
}
}

细看源码,其实可以发现只用到了两种方法,一种是 invokeSubscriber 意味着立即调用该方法,另一种是 xxxPoster.enqueue() 意味着需要使用其他线程来执行该方法。

invokeSubscriber()

源码如下:

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

实在是简单粗暴直接通俗易懂,笔者佩服。

那么那些情况会使用 invokeSubscriber() 方法呢?

  • POSTING:不用说,既然和发射事件线程同一条线程执行,那么当然直接调用 invokeSubscriber() 即可。
  • MAIN:在确保发射事件线程是主线程的情况下,直接调用 invokeSubscriber()
  • MAIN_ORDERED:如果当前项目不是 Android 项目情况下(纯 Java 项目),将会直接调用 invokeSubscriber()
  • BACKGROUND:前面提到如果发射事件线程不是主线程的话,接收事件将会执行于发射事件所在的线程,所以也会直接调用 invokeSubscriber()

文中已多次提到 Android 项目和纯 Java 项目,是由于在 Java 项目中大部分情况下不需要特地区分主线程和子线程(这一点笔者也得到了女票的证实)。其实不仅是 EventBus,RxJava 也是如此,RxJava 中是没有 Schedulers.mainThread() 一说的,仅有 Schedulers.trampoline() 表当前线程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Poster#enqueue()

根据源码可以看出来分为以下三种:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

HandlerPoster 源码不在此扩展了,熟悉 Android 的读者们应该都猜得到 HandlerPoster 底层实现肯定是通过 Handler 机制来实现的,HandlerPoster#enqueue() 方法的实现离不开 Hanlder#sendMessage(),而处理方式肯定就是在 Hanlder#handleMessage() 中去调用 invokeSubscriber()

BackgroundPoster 源码也不在此扩展了,前面提到 EventBus 会维护单一线程去执行接收事件方法,所以肯定会在 Runnable#run() 中去调用 invokeSubscriber()

AsyncPoster 的底层实现实际上与 BackgroundPoster 大同小异,但是有读者会疑惑了,BackgroundPoster 底层维护的是「单一」线程,而 AsyncPoster 肯定不是这样的啊。这里的细节留到设计技巧一节再来细说。

sticky

什么叫做 sticky 事件笔者此处就不做扩展了。项目中如果想要发射 sticky 事件需要通过 EventBus#postSticky() 方式,源码如下:

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
post(event);
}

可以看到第一步是将该事件放入 stickyEvents 中,第二步则是正常 post()。为避免多线程操作 postSticky(Object)removeStickyEvent(Class<?>) 引发的冲突,所以对 stickyEvents 对象添加了 synchronized 关键字,不得不说 EventBus 作者的设计实在是缜密啊。前文提到 EventBus#register() 中关于 sticky 事件的代码简化如下:

if (subscriberMethod.sticky) {
Object stickyEvent = stickyEvents.get(eventType);
if (stickyEvent != null) {
postToSubscription(newSubscription, stickyEvent, isMainThread());
}
}

可以看到,没有什么特殊的地方,判断当前事件是否 sticky,如果 sticky 则从 stickyEvents 拿出该事件并执行 postToSubscription() 方法。

优化操作

eventInheritance

不知道各位读者在日常使用 EventBus 中会不会在 Event 之间存在继承关系,反正笔者是没这样用过。也正是存在笔者这种不会这样使用 Event 和会使用 Event 继承的开发者之间的矛盾才会有这个字段出现。全局搜索该字段仅用于发射事件的时候判断是否需要发射父类事件,由于该字段默认为 true,所以如果各位读者和笔者一样在项目开发中 Event 不存在继承关系的话,可以将该字段设为 false 以提高性能。

APT

EventBus 内部使用了大量的反射去寻找接收事件方法,实际上有经验的小伙伴知道可以使用 APT 来优化。这也就是 EventBus 3.0 引入的技术,此处的使用便不在此处扩展了,代码中通过 ignoreGeneratedIndex 来判断是否使用生成的 APT 代码去优化寻找接收事件的过程,如果开启了的话,那么将会通过 subscriberInfoIndexes 来快速得到接收事件方法的相关信息。所以各位读者如果没有在项目中接入 EventBus 的 APT,那么可以将 ignoreGeneratedIndex 设为 false 提高性能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

设计技巧

反射方法

EventBus 在获取接收事件方法的信息中,通过 getDeclaredMethods() 来获取类中所有方法而并不是通过 getMethods(),由于前者只反射当前类的方法(不包括隐式继承的父类方法),所以前者的效率较后者更高些。

FindState

以下代码是 FindState 的获取:

private FindState prepareFindState() {
synchronized (FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
FindState state = FIND_STATE_POOL[i];
if (state != null) {
FIND_STATE_POOL[i] = null;
return state;
}
}
}
return new FindState();
}

以下代码是 FindState 的回收复用:

private List getMethodsAndRelease(FindState findState) {
List subscriberMethods = new ArrayList<>(findState.subscriberMethods);
findState.recycle();
synchronized (FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
if (FIND_STATE_POOL[i] == null) {
FIND_STATE_POOL[i] = findState;
break;
}
}

写在最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

[外链图片转存中…(img-PrDV3dob-1714978741538)]

【延伸Android必备知识点】

[外链图片转存中…(img-M7Av9B2s-1714978741538)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值