[slf4j+log] 源码解析

slf4j: The Simple Logging Facade for java即 java简单的日志门面。
统一定义了一系列的日志接口,使用户可以使用统一的接口来记录日志。logback,log4j等框架都实现了这些接口,启动时动态地决定真正的日志框架。
本文以slf4j+logback的源码来讲解整个绑定和打印日志的流程。

手动阅读目录如下:

  • 绑定日志框架
  • 解析配置文件获取LoggerFactory:对于logback而言就是LoggerContext
  • 获取Logger:在LoggerAction的begin方法中会将创建的logger对象缓存到loggerCache中and logger的childList结构中;后续的AppenderRefAction中,会将appender添加进来
  • 打印日志:分为同步的Appender和异步的Appender

可用的日志框架依赖如下:

 1         <dependency>
 2             <groupId>org.slf4j</groupId>
 3             <artifactId>slf4j-simple</artifactId>
 4             <version>1.7.5</version>
 5         </dependency>
 6         <dependency>
 7             <groupId>org.slf4j</groupId>
 8             <artifactId>slf4j-log4j12</artifactId>
 9             <version>1.7.25</version>
10         </dependency>
11
12         <dependency>
13             <groupId>org.slf4j</groupId>
14             <artifactId>slf4j-jdk14</artifactId>
15             <version>1.7.25</version>
16         </dependency>
17         <dependency>
18             <groupId>ch.qos.logback</groupId>
19             <artifactId>logback-classic</artifactId>
20             <scope>runtime</scope>
21         </dependency>

如何使用slf4j的示例代码如下(配合logback的配置文件):

 1 import org.slf4j.Logger;
 2 import org.slf4j.LoggerFactory;
 3
 4
 5 public class LoggerTest {
 6
 7     private static final Logger logger = LoggerFactory.getLogger(LoggerTest.class);
 8
 9     public static void main(String[] args){
10         logger.info("hello world!");
11     }
12
13 }

本文以[示例]logback.xml的配置文件为例:

 1  <!-- DEBUG级别日志 appender -->
 2     <appender name="DEBUG_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender" additivity="true">
 3         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
 4             <!-- 按天回滚 daily -->
 5             <fileNamePattern>/Users/tanling/saveDir/logs/%d{yyyy-MM-dd}/debug-log1.log</fileNamePattern>
 6             <!-- 日志最大的历史 60天 -->
 7             <maxHistory>60</maxHistory>
 8         </rollingPolicy>
 9         <encoder>
10             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
11         </encoder>
12     </appender>
13
14
15     <appender name="DEBUG_LOG_ASYNC" class= "ch.qos.logback.classic.AsyncAppender">
16         <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
17         <discardingThreshold >0</discardingThreshold>
18         <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
19         <queueSize>2048</queueSize>
20         <neverBlock>true</neverBlock>
21         <includeCallerData>true</includeCallerData>
22         <maxFlushTime>3000</maxFlushTime>
23         <!-- 添加附加的appender,最多只能添加一个 -->
24         <appender-ref ref ="DEBUG_LOG"/>
25     </appender>
26
27     <!-- root级别   DEBUG -->
28     <root level="INFO">
29         <!-- 控制台输出 -->
30         <appender-ref ref="STDOUT" />
31         <!-- 文件输出 -->
32         <appender-ref ref="DEBUG_LOG" />
33     </root>
34
35     <logger name="org.foo.demo" additivity="true" >
36         <level value="INFO"/>
37         <appender-ref ref="DEBUG_LOG"/>
38     </logger>

绑定日志框架

点开LoggerFactory.getLogger的源码:

 1  //LoggerFactory
 2  public static Logger getLogger(String name) {
 3    ILoggerFactory iLoggerFactory = getILoggerFactory();
 4    return iLoggerFactory.getLogger(name);
 5  }
 6  public static ILoggerFactory getILoggerFactory() {
 7    if (INITIALIZATION_STATE == UNINITIALIZED) {
 8      INITIALIZATION_STATE = ONGOING_INITIALIZATION;
 9      // 初始化 这里是后续分析代码的核心
10      performInitialization();
11    }
12    switch (INITIALIZATION_STATE) {
13      case SUCCESSFUL_INITIALIZATION:
14        // 初始化成功以后,返回StaticLoggerBinder的单例的loggerfactory
15        return StaticLoggerBinder.getSingleton().getLoggerFactory();
16      case NOP_FALLBACK_INITIALIZATION:
17        return NOP_FALLBACK_FACTORY;
18      case FAILED_INITIALIZATION:
19        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
20      case ONGOING_INITIALIZATION:
21        // support re-entrant behavior.
22        // See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
23        return TEMP_FACTORY;
24    }
25    throw new IllegalStateException("Unreachable code");
26  }
27
28  private final static void performInitialization() {
29    // 绑定,这里是绑定真正的日志框架
30    bind();
31    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
32      versionSanityCheck();
33    }
34  }
35
36  // ?? 绑定是重点
37  private final static void bind() {
38    try {
39      // 通过类加载器加载 org/slf4j/impl/StaticLoggerBinder.class
40      Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
41      reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
42      // the next line does the binding
43      // ?? 解析logback配置文件,获取真正的LoggerFactory
44      StaticLoggerBinder.getSingleton();
45      INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
46      reportActualBinding(staticLoggerBinderPathSet);
47      fixSubstitutedLoggers();
48    } catch (NoClassDefFoundError ncde) {
49     // 代码略去
50    } catch (java.lang.NoSuchMethodError nsme) {
51     // 代码略去
52    }
53  }

源码解析:
当运行getILoggerFactory方法的时候,首先会执行初始化获取到真正的LoggerFactory,然后返回。bind是重点。
bind里面做了以下几件事情:

  • 通过类加载器加载classpath中的org/slf4j/impl/StaticLoggerBinder.class
  • StaticLoggerBinder.getSingleton 解析logback配置文件,获取真正的LoggerFactory
  • 检查classpath中是否导入了多个日志框架

slf4j api旨在一次只与一个基础日志框架绑定。如果类路径上存在多个绑定,则slf4j将发出警告,列出这些绑定的位置。如果类路径上有多个绑定,请选择一个且只能使用一个绑定,并删除其他绑定。即使存在多个绑定,slf4j也会选择一个日志框架实现并与之绑定。https://www.slf4j.org/codes.html#multiple_bindings

获取LoggerContext

下面来介绍StaticLoggerBinder的过程(以logback为例)

 1    void init() {
 2        try {
 3            try {
 4               // 通过ContextInitializer来解析logback的配置文件,解析的内容记录到LoggerContext中
 5                new ContextInitializer(defaultLoggerContext).autoConfig();
 6            } catch (JoranException je) {
 7                Util.report("Failed to auto configure default logger context", je);
 8            }
 9            // logback-292
10            if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
11                StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
12            }
13            // 将LoggerContext存储到 contextSelectorBinder.getContextSelector()中
14            contextSelectorBinder.init(defaultLoggerContext, KEY);
15            initialized = true;
16        } catch (Throwable t) {
17            // we should never get here
18            Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
19        }
20    }
21
22public ILoggerFactory getLoggerFactory() {
23        if (!initialized) {
24            return defaultLoggerContext;
25        }
26
27        if (contextSelectorBinder.getContextSelector() == null) {
28            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
29        }
30        // 这里获取到的LoggerFactroy实际上就是LoggerContext
31        return contextSelectorBinder.getContextSelector().getLoggerContext();
32    }
 

代码解析:
上面两个方法,我们结合起来看。

  • init()方法中,通过ContextInitializer来解析logback的配置文件,解析的内容入LoggerContext中。并且将该内容存储到了contextSelectorBinder.getContextSelector()中。
  • getLoggerFactory实际返回的就是LoggerContext。

接着来分析是如何解析logback配置文件

 1// ContextInitializer
 2    public void autoConfig() throws JoranException {
 3        StatusListenerConfigHelper.installIfAsked(loggerContext);
 4        // 获取到logback的配置文件
 5        URL url = findURLOfDefaultConfigurationFile(true);
 6        if (url != null) {
 7            // 存在配置文件的时候解析
 8            configureByResource(url);
 9        } else {
10            // 不存在配置文件的时候的处理
11            Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
12            if (c != null) {
13                try {
14                    c.setContext(loggerContext);
15                    c.configure(loggerContext);
16                } catch (Exception e) {
17                    throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
18                                    .getCanonicalName() : "null"), e);
19                }
20            } else {
21                BasicConfigurator basicConfigurator = new BasicConfigurator();
22                basicConfigurator.setContext(loggerContext);
23                basicConfigurator.configure(loggerContext);
24            }
25        }
26    }

代码解析:
获取logback配置文件,获取到则进行配置文件的解析。获取的顺序是这样的:

1    // ②
2    final public static String GROOVY_AUTOCONFIG_FILE = "logback.groovy";
3    // ④
4    final public static String AUTOCONFIG_FILE = "logback.xml";
5    // ③
6    final public static String TEST_AUTOCONFIG_FILE = "logback-test.xml";
7    // ①
8    final public static String CONFIG_FILE_PROPERTY = "logback.configurationFile";

最终落到代码EventPlayer上

1// ContextInitializer
2else if (urlString.endsWith("xml")) {
3            JoranConfigurator configurator = new JoranConfigurator();
4            configurator.setContext(loggerContext);
5            configurator.doConfigure(url);
6        }
 1    public void play(List<SaxEvent> aSaxEventList) {
 2        eventList = aSaxEventList;
 3        SaxEvent se;
 4        for (currentIndex = 0; currentIndex < eventList.size(); currentIndex++) {
 5            se = eventList.get(currentIndex);
 6
 7            if (se instanceof StartEvent) {
 8                interpreter.startElement((StartEvent) se);
 9                // invoke fireInPlay after startElement processing
10                interpreter.getInterpretationContext().fireInPlay(se);
11            }
12            if (se instanceof BodyEvent) {
13                // invoke fireInPlay before characters processing
14                interpreter.getInterpretationContext().fireInPlay(se);
15                interpreter.characters((BodyEvent) se);
16            }
17            if (se instanceof EndEvent) {
18                // invoke fireInPlay before endElement processing
19                interpreter.getInterpretationContext().fireInPlay(se);
20                interpreter.endElement((EndEvent) se);
21            }
22
23        }
24    }

代码解析:
将logback配置文件中的标签会被解析成List。是如何解析的:
标签以"<"为开头的,会解析成StartEven事件。
标签以"<杠"为开头的,会被解析成EndEvent事件。
在"<"xx>"body"<杠xxx>"标签中的body内容,会被解析成BodyEvent事件。
如图参见debug的结果,每个<的标签都是成对出现的,中间是BodyEvent。

对于不同的标签,均会有StartEvent和EndEvent事件。每个标签都有一个对应的Action,其中有begin和end方法,分别对应上面StartEvent和EndEvent事件的处理。
其中BodyEvent对应于Action中的body方法。

标签 处理标签的StartEvent和EndEvent的Action
appender AppenderAction
root RootLoggerAction
logger LoggerAction
appender-ref AppenderRefAction
属性标签(比如:rollingPolicy) NestedComplexPropertyIA
属性标签(比如:fileNamePattern) NestedBasicPropertyIA

下面主要来介绍下各标签的处理:
其中以示例logback.xml为例:

红色标的数字代表appender=DEBUG_LOG的执行路径。青色数字顺序代表appender=DEBUG_LOG_ASYNC的执行顺序。黄色的数字代表logger=org.foo.demo的执行顺序。紫色的数字代表root的执行顺序。
核心思想就是利用栈的特性,标签开始的是将对象压入栈,结束的时候将对象从栈中出栈,处理内部的标签时只需要从栈中peek一个对象即是其父对象,处理完了后作为属性设置进去。最终所有key=loggerName,value=logger对象都会缓存到LoggerContext中。

获取Logger

 1    public final Logger getLogger(final String name) {
 2
 3        if (name == null) {
 4            throw new IllegalArgumentException("name argument cannot be null");
 5        }
 6
 7        // if we are asking for the root logger, then let us return it without
 8        // wasting time
 9        if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
10            return root;
11        }
12
13        int i = 0;
14        Logger logger = root;
15
16        // check if the desired logger exists, if it does, return it
17        // without further ado.
18        // 最终所有的key=loggerName,value=logger实例都会缓存在LoggerContext中的loggerCache中
19        Logger childLogger = (Logger) loggerCache.get(name);
20        // if we have the child, then let us return it without wasting time
21        if (childLogger != null) {
22            return childLogger;
23        }
24
25        // if the desired logger does not exist, them create all the loggers
26        // in between as well (if they don‘t already exist)
27        String childName;
28        // 获取Logger的逻辑
29        while (true) {
30            int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
31            if (h == -1) {
32                childName = name;
33            } else {
34                childName = name.substring(0, h);
35            }
36            // move i left of the last point
37            i = h + 1;
38            synchronized (logger) {
39                childLogger = logger.getChildByName(childName);
40                if (childLogger == null) {
41                    childLogger = logger.createChildByName(childName);
42                    loggerCache.put(childName, childLogger);
43                    incSize();
44                }
45            }
46            logger = childLogger;
47            if (h == -1) {
48                return childLogger;
49            }
50        }
51    }

代码解析:
当指定的loggerName中没有点号的时候,从rootLogger中获取childLogger,没有的场合,则新建一个Logger对象,其parentLogger为rootLogger。
指定一个类名的时候,loggerName为该类的全类名。
比如org.foo.demo.LoggerTest,
先查找loggerName=org的logger对象;再查找loggerName=org.foo的logger实例;直至创建loggerName=org.foo.demo.LoggerTest的logger对象。
有的场合直接从logger的chidlist结构中获取,这是在解析LoggerAction的时候缓存起来的。
剧透一下,执行打印的时候,会追溯logger的父logger执行appender。

利用Logger打印日志(同步&异步)

 1    private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
 2                    final Throwable t) {
 3        // 会将方法的调用参数level 消息 params 异常封装成LoggerEvent
 4        LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
 5        le.setMarker(marker);
 6        callAppenders(le);
 7    }
 8
 9    public void callAppenders(ILoggingEvent event) {
10        int writes = 0;
11        // 这里是上述的剧透内容,会遍历执行logger.parent的appender
12        // additive为配置文件中配置的内容,默认为true,当配置为false的时候,会终止追溯其父logger
13        for (Loogger l = this; l != null; l = l.parent) {
14            writes += l.appendLoopOnAppenders(event);
15            if (!l.additive) {
16                break;
17            }
18        }
19        // No appenders in hierarchy
20        if (writes == 0) {
21            loggerContext.noAppenderDefinedWarning(this);
22        }
23    }
24    // logger的aai是AppenderAttachableImpl对象,其中持有多个Appender的list
25    private int appendLoopOnAppenders(ILoggingEvent event) {
26        if (aai != null) {
27            return aai.appendLoopOnAppenders(event);
28        } else {
29            return 0;
30        }
31    }
32    public int appendLoopOnAppenders(E e) {
33        int size = 0;
34        // 找到logger对应的Appender,执行器logger方法
35        for (Appender<E> appender : appenderList) {
36            appender.doAppend(e);
37            size++;
38        }
39        return size;
40    }

代码解析:
将方法的参数封装成LoggingEvent对象,从指定名称的loggery追溯logger的parentLogger,通过logger中持有的appender来执行。

AppenderAction的end方法中会执行启动:

 1    public void end(InterpretationContext ec, String name) {
 2        if (inError) {
 3            return;
 4        }
 5        // 这里启动appender
 6        if (appender instanceof LifeCycle) {
 7            ((LifeCycle) appender).start();
 8        }
 9
10        Object o = ec.peekObject();
11
12        if (o != appender) {
13            addWarn("The object at the of the stack is not the appender named [" + appender.getName() + "] pushed earlier.");
14        } else {
15            ec.popObject();
16        }
17    }

RollingFileAppender(同步)

1    public void start() {
2        // 部分代码略
3        // 进行一堆check,然后appender的文件名创建文件,然后打开文件流,将日志以append的方式追加到文件中
4        currentlyActiveFile = new File(getFile());
5        addInfo("Active log file name: " + getFile());
6        super.start();
7    }

AsyncAppender(异步)

 1// AsyncAppenderBase
 2    @Override
 3    public void start() {
 4        if (isStarted())
 5            return;
 6        if (appenderCount == 0) {
 7            addError("No attached appenders found.");
 8            return;
 9        }
10        if (queueSize < 1) {
11            addError("Invalid queue size [" + queueSize + "]");
12            return;
13        }
14        blockingQueue = new ArrayBlockingQueue<E>(queueSize);
15
16        if (discardingThreshold == UNDEFINED)
17            discardingThreshold = queueSize / 5;
18        addInfo("Setting discardingThreshold to " + discardingThreshold);
19        worker.setDaemon(true);
20        worker.setName("AsyncAppender-Worker-" + getName());
21        // make sure this instance is marked as "started" before staring the worker Thread
22        super.start();
23        worker.start();
24    }
25
26    @Override
27    protected void append(E eventObject) {
28        // 当队列空间所剩无几的时候 & 日志级别(INFO,DEBU,TRACE)<=INFO的场合,消息直接丢弃
29        if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
30            return;
31        }
32        preprocess(eventObject);
33        put(eventObject);
34    }
35
36    private void put(E eventObject) {
37        if (neverBlock) {
38            blockingQueue.offer(eventObject);
39        } else {
40            try {
41                blockingQueue.put(eventObject);
42            } catch (InterruptedException e) {
43                // Interruption of current thread when in doAppend method should not be consumed
44                // by AsyncAppender
45                Thread.currentThread().interrupt();
46            }
47        }
48    }

代码解析:

  • 创建一个ArrayBlockingQueue队列,用于存放创建的LoggingEvent,其中neverBlock为配置文件中指定,true的场合,满了也不阻塞直接返回; false的场合,满了则阻塞,直至event被消费。
  • 创建了一个工作线程Worker
 1    class Worker extends Thread {
 2
 3        public void run() {
 4            AsyncAppenderBase<E> parent = AsyncAppenderBase.this;
 5            AppenderAttachableImpl<E> aai = parent.aai;
 6
 7            // loop while the parent is started
 8            while (parent.isStarted()) {
 9                try {
10                    E e = parent.blockingQueue.take();
11                    aai.appendLoopOnAppenders(e);
12                } catch (InterruptedException ie) {
13                    break;
14                }
15            }
16
17            addInfo("Worker thread will flush remaining events before exiting. ");
18
19            for (E e : parent.blockingQueue) {
20                aai.appendLoopOnAppenders(e);
21                parent.blockingQueue.remove(e);
22            }
23
24            aai.detachAndStopAllAppenders();
25        }
26    }

代码解析:
asyncAppender中的aai就是同步的appender,这里只要appender开启中,会从blockingQueue队列中取出LoggingEvent交给普通的appender来执行。

以上。

更多源码分析,关注公众号??????

原文地址:https://www.cnblogs.com/mianteno/p/10692445.html

时间: 2024-10-06 23:10:56

[slf4j+log] 源码解析的相关文章

t-io 集群解决方案以及源码解析

t-io 集群解决方案以及源码解析 0x01 概要说明 本博客是基于老谭t-io showcase中的tio-websocket-showcase 示例来实现集群.看showcase 入门还是挺容易的,入坑(入门)请看老谭写的用t-io来写一个网页聊天室或客服是个怎样的体验. 要深入理解具体实现原理后续的业务扩展,把t-io玩6起来还需要耐心看看源码,看了之后我相信你一定会有收获的,祝你好运. 其实t-io 在2.4的版本中已加入的集群实现的逻辑代码,只是官方没有写文档以及完整的示例而已,在此不

Flume-ng源码解析之Channel组件

如果还没看过Flume-ng源码解析之启动流程,可以点击Flume-ng源码解析之启动流程 查看 1 接口介绍 组件的分析顺序是按照上一篇中启动顺序来分析的,首先是Channel,然后是Sink,最后是Source,在开始看组件源码之前我们先来看一下两个重要的接口,一个是LifecycleAware ,另一个是NamedComponent 1.1 LifecycleAware @[email protected] interface LifecycleAware {  public void s

Handler机制(四)---Handler源码解析

Handler的主要用途有两个:(1).在将来的某个时刻执行消息或一个runnable,(2)把消息发送到消息队列. 主要依靠post(Runnable).postAtTime(Runnable, long).postDelayed(Runnable, long).sendEmptyMessage(int).sendMessage(Message).sendMessageAtTime(Message).sendMessageDelayed(Message, long)这些方法来来完成消息调度.p

Android EventBus源码解析, 带你深入理解EventBus

上一篇带大家初步了解了EventBus的使用方式,详见:Android EventBus实战 没听过你就out了,本篇博客将解析EventBus的源码,相信能够让大家深入理解该框架的实现,也能解决很多在使用中的疑问:为什么可以这么做?为什么这么做不好呢? 1.概述 一般使用EventBus的组件类,类似下面这种方式: [java] view plain copy public class SampleComponent extends Fragment { @Override public vo

【特征匹配】RANSAC算法原理与源码解析

转载请注明出处:http://blog.csdn.net/luoshixian099/article/details/50217655 随机抽样一致性(RANSAC)算法,可以在一组包含"外点"的数据集中,采用不断迭代的方法,寻找最优参数模型,不符合最优模型的点,被定义为"外点".在图像配准以及拼接上得到广泛的应用,本文将对RANSAC算法在OpenCV中角点误匹配对的检测中进行解析. 1.RANSAC原理 OpenCV中滤除误匹配对采用RANSAC算法寻找一个最佳

Dialog与FragmentDialog源码解析

<代码里的世界> -UI篇 用文字札记描绘自己 android学习之路 转载请保留出处 by Qiao http://blog.csdn.net/qiaoidea/article/details/46402845 [导航] - 弹出式对话框各种方案 从仿QQ消息提示框来谈弹出式对话框的实现方式 (Dialog,PopupWind,自定义View,Activity,FragmentDialog) - Dialog源码解析 从源码上看Dialog与DialogFragment 1.概述 前一篇写了

【转】Java 集合系列12之 TreeMap详细介绍(源码解析)和使用示例

概要 这一章,我们对TreeMap进行学习.我们先对TreeMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用TreeMap.内容包括:第1部分 TreeMap介绍第2部分 TreeMap数据结构第3部分 TreeMap源码解析(基于JDK1.6.0_45)第4部分 TreeMap遍历方式第5部分 TreeMap示例 转载请注明出处:http://www.cnblogs.com/skywang12345/admin/EditPosts.aspx?postid=3310928 第1部

消息中间件 RocketMQ源码解析:事务消息

关注微信公众号:[芋艿的后端小屋]有福利: RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表 RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址 您对于源码的疑问每条留言都将得到认真回复.甚至不知道如何读源码也可以请教噢. 新的源码解析文章实时收到通知.每周更新一篇左右. 1. 概述 2. 事务消息发送 2.1 Producer 发送事务消息 2.2 Broker 处理结束事务请求 2.3 Broker 生成

Android EventBus3.0使用及源码解析

叨了个叨 最近因为换工作的一些琐事搞的我一个头两个大,也没怎么去学新东西,实在是有些愧疚.新项目用到了EventBus3.0,原来只是听说EventBus的鼎鼎大名,一直没仔细研究过.趁着周末有些时间,研究下代码,也算没有虚度光阴. EventBus GitHub : https://github.com/greenrobot/EventBus EventBus3.0简介 EventBus是greenrobot出品的一个用于Android中事件发布/订阅的库.以前传递对象可能通过接口.广播.文件