EventBus 开源库学习(二)

整体流程阅读

EventBus在使用的时候基本分为以下几步:

1、注册订阅者

java 复制代码
EventBus.getDefault().register(this);

2、订阅者解注册,否者会导致内存泄漏

java 复制代码
EventBus.getDefault().unregister(this);

3、在订阅者中编写注解为Subscribe的事件处理函数

java 复制代码
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true, priority = 1)
public void onMsgEventReceived(MsgEvent event) {
     Toast.makeText(this, event.getMsg(), Toast.LENGTH_LONG).show();
}

4、事件发送

java 复制代码
EventBus.getDefault().post("msg1 - coming!!!");

我们先按使用的流程大体看下源码逻辑,源码版本3.3.1:

注册源码逻辑

java 复制代码
    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;
    }

EventBus 使用了双重校验锁的单例设计模式,保证用到的对象是唯一的,首次使用对象为空的时候通过下面构造创建一个。

java 复制代码
    public EventBus() {
        this(DEFAULT_BUILDER);
    }

DEFAULT_BUILDER是一个final常量,在加载的时候就进行初始化,赋一个EventBusBuilder对象如下面代码所示。

java 复制代码
private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();

EventBusBuilderEventBus的建造类,里面参数在加载的时候进行了初始化。

java 复制代码
public class EventBusBuilder {
    private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();

    boolean logSubscriberExceptions = true;
    boolean logNoSubscriberMessages = true;
    boolean sendSubscriberExceptionEvent = true;
    boolean sendNoSubscriberEvent = true;
    boolean throwSubscriberException;
    boolean eventInheritance = true;
    boolean ignoreGeneratedIndex;
    boolean strictMethodVerification;
    ExecutorService executorService = DEFAULT_EXECUTOR_SERVICE;
    List<Class<?>> skipMethodVerificationForClasses;
    List<SubscriberInfoIndex> subscriberInfoIndexes;
    Logger logger;
    MainThreadSupport mainThreadSupport;
    ...
}

如果有需要的话,我们也可以通过配置EventBusBuilder来更改EventBus的属性,在EventBus中有一个静态方法直接返回一直新的EventBusBuilder对象,设置完参数后调用build()来以新的配置来新建一个EventBus对象。

java 复制代码
#EventBus
    public static EventBusBuilder builder() {
        return new EventBusBuilder();
    }

#EventBusBuilder
    /** Builds an EventBus based on the current configuration. */
    public EventBus build() {
        return new EventBus(this);
    }

然后通过下面的调用来设置:

java 复制代码
EventBus.builder()
        .eventInheritance(false)
        .logSubscriberExceptions(false)
        .build()
        .register(this);

拿到EventBus对象以后,我们可以调用其register方法进行订阅者注册了。

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

首先获取订阅者类subscriber,然后通过findSubscriberMethods方法获取该类中以@Subscribe注解的函数,由于一个类中可能监听多个事件,因此获取的方法可能是多个,所有的方法赋值到一个List列表中,然后遍历这个列表进行注册。

java 复制代码
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } 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;
            }
        }

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

        ....
    }

上面就是注册最主要的代码,步骤解析如下:

  • 根据注解方法获取监听事件的类型eventType,并对订阅者subscriber和订阅函数subscriberMethod建立一个订阅关系对象newSubscription

  • 根据eventTypesubscriptionsByEventType (HashMap)中获取所有该事件类型的订阅关系列表subscriptions

  • 如果订阅关系列表subscriptions为空就新建一个,然后以keyeventTypevaluenewSubscription添加进去;

  • 如果订阅关系列表subscriptions不为空,判断是否存在newSubscription,如果存在,说明之前已经注册过,抛出异常;

  • 如果订阅关系列表subscriptions不为空,列表页没有订阅关系newSubscription,我们遍历添加进去,这里通过订阅函数的priority 来决定存放在列表中的位置,从这里也能看出priority 越大,存放位置越靠前,和上一篇中分析的:值越大,优先级越高,越优先接收到事件。我们可以猜出是通过遍历这个表来进行事件发送的,在表里的位置越靠前,越先收到事件。

  • 然后通过订阅者subscriber在另一个HashMap - typesBySubscriber 中获取该订阅者订阅的所有事件,因为一个订阅者可以订阅多个不同的事件,因此获取的是个List列表subscribedEvents

  • 首先判断subscribedEvents列表是不是空的,如果是空说明以前没有订阅过任何事件,新建一个List,然后以keysubscribervaluesubscribedEvents添加到typesBySubscriber

  • 然后在新建的subscribedEvents中添加我们订阅的事件eventType

这里出现了两个HashMap:subscriptionsByEventTypetypesBySubscriber,通过上面的解析可以知道:
subscriptionsByEventType:一个事件可能有多个订阅者,key是事件,value是所有订阅该事件的所有的订阅者;
typesBySubscriber:一个订阅者可能订阅多个事件,key是订阅者,value是订阅者订阅的所有事件;

看命名就知道By后面的是key值,前面的是value值,一个好的命名就是这样吧。

解注册源码逻辑

在使用EventBus时,注册完后我习惯接着去写解注册的代码,怕后面会忘,因此按照写代码习惯在讲下解注册的源码实现。

java 复制代码
    /** Unregisters the given subscriber from all event classes. */
    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);
        } else {
            logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

如果是我们自己写这部分代码,根据前面注册的过程,我们肯定是要把添加到两个HashMap中的值移除掉。好了,看上面源码,首先在typesBySubscriber中获取该订阅者订阅的事件列表subscribedTypes

如果为空说明该订阅者没有订阅任何事件,无任何操作;如果不为空,遍历所有事件调用unsubscribeByEventType方法进行解注册,然后在typesBySubscriber中移除这个订阅者subscriber

java 复制代码
    /** Only updates subscriptionsByEventType, not typesBySubscriber! Caller must update typesBySubscriber. */
    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) {
                    subscription.active = false;
                    subscriptions.remove(i);
                    i--;
                    size--;
                }
            }
        }
    }

在上一个方法中,已经把typesBySubscriber这个订阅者移除了,那么unsubscribeByEventType函数就是遍历事件所有的订阅者,然后把解注册的订阅者在subscriptionsByEventType中给移除掉。上面的逻辑就是干这个事情。不过一边遍历一边移除是有风险的,这个大家要注意,index需要也跟着进行减少。

总结下:注册和解注册就是往两个HashMap添加和移除数据的过程。

事件发送post源码逻辑

java 复制代码
    /** Posts the given event to the event bus. */
    public void post(Object event) {
        PostingThreadState postingState = currentPostingThreadState.get();
        List<Object> eventQueue = postingState.eventQueue;
        eventQueue.add(event);
        ····
    }

上面是部分post代码,第一行先解释这个变量currentPostingThreadState及内部类PostingThreadState

java 复制代码
    private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
        @Override
        protected PostingThreadState initialValue() {
            return new PostingThreadState();
        }
    };


    /** For ThreadLocal, much faster to set (and get multiple values). */
    final static class PostingThreadState {
        final List<Object> eventQueue = new ArrayList<>();
        boolean isPosting;
        boolean isMainThread;
        Subscription subscription;
        Object event;
        boolean canceled;
    }

currentPostingThreadState是一个ThreadLocal,相当于是线程的私有财产,里面维护的变量只属于当前线程,线程间不会共享。维护的变量是一个自定义类PostingThreadState,用来保存发送线程的发送状态信息:当前线程是否为主线程,是否在发送事件,发送的事件列表、接收事件的订阅者等。

java 复制代码
    /** Posts the given event to the event bus. */
    public void post(Object event) {
        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;
            }
        }
    }

现在继续看post逻辑。首先,获取到当前线程的发送状态postingState,然后拿到事件列表,并把需要post的事件加入到列表中。判断是否启动了事件发送流程,如果已经启动了,不在做处理,加入列表中的事件会轮到处理。如果没有启动就启动处理流程,并将isPosting赋值为true。循环从事件列表中获取事件,通过postSingleEvent进行处理。

java 复制代码
    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));
            }
        }
    }

postSingleEvent中,有一个变量的判断eventInheritance,这个变量表示是否查找发送事件的父类或接口的订阅者,默认是true。例如,发送事件MsgEvent这个对象,MsgEvent继承至Event这个抽象类,发送后,不光所有订阅MsgEvent的订阅者可以收到事件,所有订阅Event的这个事件的订阅者也会收到事件。前面有分析这个字段的值是可以重新配置的,代码如下。

java 复制代码
EventBus.builder()
        .eventInheritance(false)   //发送的时候不考虑事件父类
        .logSubscriberExceptions(false)
        .build()
        .register(this);

eventInheritancetrue,通过lookupAllEventTypes,向上找到所有父类事件类,然后遍历找到所有事件的订阅者,并发送事件,如果为false,直接将当前事件发送给订阅者。

postSingleEvent函数主要是找到需要post的所有相关事件,然后进一步调用postSingleEventForEventType发送给订阅者,因此函数后面的逻辑是判断postSingleEventForEventType的返回值subscriptionFound,即判断当前事件有没有订阅者进行处理,如果没有处理的,会发送一个NoSubscriberEvent。例如:如果我在上一节的例子中post一个新的对象student,但是没有订阅者和接收函数,如果监听了NoSubscriberEvent,会收到一个NoSubscriberEvent的事件,告知调用者你post的对象没有订阅者。

java 复制代码
@Subscribe()
public void onMsgEventReceived(NoSubscriberEvent event) {
    Log.i(TAG, "NoSubscriberEvent : " + event);
}

Student student = new Student(1,"jane");
EventBus.getDefault().post(student);
java 复制代码
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;
    }

从前面分析可知,最后都走到了postSingleEventForEventType这个函数里面,首先在subscriptionsByEventType中找到所有订阅该事件的订阅者subscriptionssubscriptions为空或者个数是0则返回false。有订阅者的话,遍历订阅者,然后通过postToSubscription进行发送,并返回true

java 复制代码
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);
        }
    }

这段代码比较简单就是根据订阅者中注解@SubscribethreadMode值来分别进行处理,上一节也讲过,POSTING表示在哪个线程发送就在哪个线程接收处理,因此直接调用invokeSubscriber通过反射来调用订阅者中的接收事件的方法。

java 复制代码
    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);
        }
    }

如果threadModeMAIN,而当前线程是子线程,通过 mainThreadPoster.enqueue(subscription, event)将事件加入主线程队列。

java 复制代码
mainThreadSupport = builder.getMainThreadSupport();
mainThreadPoster = mainThreadSupport != null ? mainThreadSupport.createPoster(this) : null;

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

public class DefaultAndroidMainThreadSupport implements MainThreadSupport {
    public boolean isMainThread() {
        return Looper.getMainLooper() == Looper.myLooper();
    }

    @Override
    public Poster createPoster(EventBus eventBus) {
        return new HandlerPoster(eventBus, Looper.getMainLooper(), 10);
    }
}

mainThreadPoster是由mainThreadSupport 创建的,mainThreadSupportMainThreadSupport实例,而MainThreadSupport是一个接口,实现类为DefaultAndroidMainThreadSupport,因此,最终是调用到的DefaultAndroidMainThreadSupport中的createPoster,新建了一个HandlerPoster

java 复制代码
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");
                }
            }
        }
    }

    @Override
    public void handleMessage(Message msg) {
      ...
    }
}

从上面的源码可以看到,HandlerPoster就是一个Handler,当执行mainThreadPoster.enqueue(subscription, event)时,会将订阅者和事件封装成一个PendingPost,然后加入到PendingPostQueue这个队列中,如果handlerActivetrue表示当前Handler正常处理事件,将入队列的事件等着被处理即可。如果为false则启动处理,调用sendMessage发送消息。

java 复制代码
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);
        ...
    }

    public void enqueue(Subscription subscription, Object event) {
       ...
    }

    @Override
    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 synchronized
                        pendingPost = 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;
        }
    }
}

handleMessage()方法将中将PendingPost对象循环出队列,交给invokeSubscriber()方法进一步处理。这样就把线程通过Handler切回了主线程。

backgroundPoster.enqueue()asyncPoster.enqueue也类似,内部都是先将事件入队列,然后再出队列,但是会通过线程池去进一步处理事件。

粘性事件发送postSticky源码逻辑

java 复制代码
    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列中中,然后调用了post,也就是上面我们解析过的流程。让我们再来回顾下注册的代码:

java 复制代码
// Must be called in synchronized block
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        ...
        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);
            }
        }
    }

注册源码分析的时候,就分析到subscriberMethod.sticky这个句上面,现在让我们看看下面的逻辑,同样先判断eventInheritance的值,然后将之前放在stickyEvents中的事件拿出来,执行checkPostStickyEventToSubscription

java 复制代码
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());
        }
    }

checkPostStickyEventToSubscription中的代码比较水,又调用了postToSubscription,这个方法上面贴出来了,这里方面看再贴一遍,熟悉的味道:

java 复制代码
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);
        }
    }

所以,在注册的时候,如果你订阅者的接收方法加了sticky注解,那么在注册的时候就会看下订阅的事件之前有没有通过postSticky发送过,如果有就会立马收到这个事件。

以上是我们平时使用过程的源码解析,码字不易,喜欢就点赞收藏啊。

参考文章:
EventBus 原理解析

相关推荐
统计学小王子2 分钟前
《数学少年-从正负号到几何原本》(第一章:“刻度之上,零为分界“)--1.2 电梯与账单里的相反数字
学习·初中数学·初中数学科普·数学科普
vipjx210 分钟前
百度网盘不限速原理解析:如何合法合规提升你的网盘下载速度
开源
流云鹤13 分钟前
03Java学习day(3)
java·学习
我命由我123451 小时前
棘轮效应极简理解
经验分享·学习·职场和发展·产品运营·求职招聘·职场发展·学习方法
a1117761 小时前
中文优先的企业 RAG 知识库 开源项目
开发语言·开源·kotlin
tt5555555555551 小时前
电工证学习笔记(二)
笔记·学习·电力
wc881 小时前
微软EDGE浏览器功能学习
前端·学习·edge
hPw0eKIqD1 小时前
一步一步学习使用FireMonkey动画() 使用动画组件为窗体添加动态效果
学习
TunerT_TQ2 小时前
Valhalla 静态工程审阅 #009|Continue 源码证据驱动评测【大厂开源基础设施特辑】
vscode·测试工具·开源·llm·github·jetbrains·ai编程助手
TunerT_TQ2 小时前
Valhalla 静态工程审阅 #008|RisingWave 源码证据驱动评测【大厂开源基础设施特辑】
rust·开源·github·实时数据处理·流式数据库·apacheflink·streamingsql