欢迎你们前往腾讯云社区,获取更多腾讯海量技术实践干货哦~java
做者:吴涛
导语:EventBus 已经火了很长一段时间了。最近咱们项目决定引入EventBus,替换咱们播放器如今的事件总线框架,以解决咱们存在的一些问题。git
腾讯视频的播放器架构是基于总线设计的,不一样的功能模块被抽象成一个个插件管理器,挂载在总线上,收听、发布事件,完成业务逻辑处理。程序员
图 1github
上图是播放器的总线示意图,每一个节点表示一个逻辑插件,红色的线条表明总线。插件能够有子插件,父插件要负责将事件派发给它的子插件。正则表达式
图 2安全
上面三个类图中,Event是描述事件的类,不一样的事件经过不一样的id值来区分。IEventProxy便是播放器的总线,publish(Event event)方法负责将事件抛到总线上。Plugin便是插件的抽象类,当总线上有新事件到达时,插件的onEvent(Event event)方法会被调用,onEvent方法内部根具事件的id值辨识不一样的事件,作相应的业务逻辑处理。拥有子插件的插件,还须要循环调用mChildPlugins的onEvent(Event event)方法,将事件传递给子插件处理。架构
下面是典型的插件onEvent方法代码片断:框架
@Override public void onEvent(Event event) { switch (event.getId()) { case Event.PageEvent.UPDATE_VIDEO: mVideoInfo = (VideoInfo) event.getMessage(); break; case Event.PlayerEvent.DEFINITION_FETCHED: updateIcon(); break; case Event.PluginEvent.BULLET_CLOSE: updateIcon(); break; default: break; } for (Plugins plugin : mChildPlugins){ plugin.onEvent(event); } }
一个插件将事件发布到总线上的代码示例:ide
@Override public void onClick(View v) { mEventProxy.publishEvent(Event.makeEvent(Event.UIEvent.ON_AUDIO_PLAY_ICON_CLICKED)); }
经过以前对播放器架构的介绍,咱们能够发现,咱们的事件机制仍是比较简陋。主要存在如下几点缺陷:
一、 插件代码结构不够松散,全部事件响应处理都在onEvent方法中处理。
二、 事件过分广播。当一个事件发生时,全部插件的onEvent方法都会被调用执行,浪费了cpu时间片,程序执行效率不高。
三、 事件类型不安全。每一个事件只能携带一个Object的对象message,事件收听者若是要解析message,收听者只能靠“猜”,是否猜中取决于发布该事件的人是否按照收听者的意愿携带指定类型的message。若是没有经过instanceof校验而直接强转,极有可能发生强转失败。
四、 事件参数不可拓展。事件只能携带一个Object的message。一旦某事件携带某种类型的message,该事件携带的message类型不能再变动,一旦变动,全部收听该事件的插件也必需要修改代码。工具
基于此,咱们决定引入EventBus开源库来重构咱们的事件机制。
了解过EventBus的同窗都知道,EventBus的核心是使用反射。不一样的事件用不一样的类型来表示,插件类要收听某一事件,就要声明一个相应的方法来接收事件。例如,已知有AEvent,BEvent,CEvent三种事件,有X、Y、Z三个插件,假设X插件收听AEvent,Y插件收听BEvent,Z插件收听CEvent,则X、Y、Z三个插件类中需以下声明:
X.java: public class X{ @Subscribe public void onAEvent(AEvent event){ doSomeThing(); } } Y.java: public class Y{ @Subscribe public void onBEvent(BEvent event){ doSomeThing(); } } Z.java: public class Z{ @Subscribe public void onCEvent(CEvent event){ doSomeThing(); } }
当咱们须要发布某AEvent时,须要调用EventBus的post方法:
mEventBus.post(new AEvent());
更多如何使用EventBus及EventBus原理的知识,这篇文章不做讲解,您能够搜索其它文章或者在GitHub上了解。
经过以上分析,咱们此次重构的主要工做内容就明确了:
一、 将Event类中全部预约义的事件所有映射成具体的类,即有多少Event id就有多少Event类的原则。好比,咱们须要将Event.PageEvent.UPDATE_VIDEO转换成UpdateVideoEvent.java。
二、 将插件的onEvent方法中switch语句中的每一条case语句映射为一个方法声明,即有多少case就有多少方法原则。例如在上述代码示例中的case Event.PageEvent.UPDATE_VIDEO:
@Subscribe public void onUpdateVideoEvent(UpdateVideoEvent event){ mVideoInfo = event.getVIdeoInfo(); }
三、 将全部使用IEventProxy发布事件的地方,所有修改成使用EventBus的post方法。好比有:
mEventProxy.publish(Event.makeEvent(Event.PageEvent.UPDATE_VIDEO, videoInfo)); 要替换为: mEventBus.post(new UpdateVideoEvent(videoInfo));
若是耐心把这篇文章看到这里的话,你们可能会以为,你要作的工做很简单嘛,无压力,so easy。
开始工做以前,老大都要求咱们先把工做量评估出来。因为代码中有多少事件,有多少个插件,每一个插件具体收听处理了多少种事件,这是很难统计出来的,特别是最后一点。不过,工做量确定和插件的个数,以及插件的代码规模确定是成正比的,我只须要把这两点统计出来,估计一个大概的工做量仍是能够的。因而,有下面的统计表:
图 3
横坐标是代码行数,纵坐标是在插件个数。插件总个数有151个,总代码行数47000多行。按照每200行代码1个小时的工做速度,天天8小时不停写代码,一我的也要整整30个工做日,还不包括自测,代码审核等等其它工做量。我拿着这个表就去找老大说,两我的须要三周的工做量。结果老大直接跟我说,帮手没有,你一我的先搞,看看进度咋样(好吧,其实老大是对这个评估不满意)。
就这样,两眼一抹黑,踏上了EventBus重构之路。
第一天,我先入手了几个插件类。遇到须要映射的XXX事件,就手动建立其对应于的XXXEvent.java文件,此操做大概须要近一分钟。将switch中的语句写成对应的方法,而后把case中的语句复制到方法体中,此操做视语句长度及case分支的多少,耗时不等。最后将onEvent方法删除。就这样一天工做下来,不断重复着这样的工做,一个八百多行的插件竟耗费了我半天工做时间,极其烦躁,并且人工修改还特别容易出错,好比拼写错误,漏掉case分支等等,带来的后果直接表如今代码运行不正确,然后续却难以排查。
因而,我有一个大胆的想法。程序员是脑力劳动者,任什么时候候,都不该该成为搬运工。是否可以编写脚本或者自动化工具,自动化的完成重构工做。
使用注解解析自动生成文件
咱们都知道,EventBus是经过注解来实现的。经过注解解析,在编译阶段生成了一个java文件,这个文件被称做SubscribeInfoIndex,其硬编码了每一个使用了Subscribe注解的类的信息。
受到EventBus的启发,咱们的事件类是否也能经过注解解析的方式生成呢?答案是确定的。关于注解解析相关的知识可参看个人另外一篇KM《apt与JavaPoet 自动生成代码》,因为篇幅限制,这里不作讲解。
首先,自定义一个注解:
@Retention(RetentionPolicy.SOURCE) @Target(ElementType.TYPE) public @interface OldEvent { String packageName(); }
packageName 属性指明该Event 类对应生成的新Event文件的包路径。
而后在Event.java中使用该注解:
图 4
图 5
(注:PlayerEvent 和UIEvent是Event中定义的内部类,事件Id定义在内部类中。除此以外,还有AudioEvent、PageEvent等)。
编写注解解析器,注解解析器的逻辑也比较简单:
图 6
例如,PlayerEvent.INIT对应生成的文件以下:
图 7
如今,咱们剩下的工做是如何完成代码自动替换,将publish替换为post,将case替换为方法。
我首先想到的是使用正则表达式,经过对源文件进行扫描,将匹配的代码行替换为指定代码。好比,咱们使用正则表达式^\s\w+\.publish\s\(\s(.+)\s(,\s(\w+)\s)?\)来匹配代码中的mEventProxy.publish()方法调用,而后将其替换为相应的post。可是,咱们仅仅经过正则匹配,没有办法肯定匹配到的就是IEventProxy类中com.tencent.qqlive.ona.player.event.IEventProxy.publishEvent(com.tencent.qqlive.ona.player.event.Event)的方法调用。例如,彻底有可能有一个类A,它内部也声明了一个public void publish(SomeKind params)方法,咱们的正则也会匹配,致使错误替换。另外,case语句的替换也是更加的困难。首先,哪些类中的onEvent方法的switch case须要被替换?只有那些继承自Plugin的类才须要替换,如何判断一个类是否继承自Plugin也是很难判断的,不但有直接继承,还有间接的继承。
所以,正则匹配这条路是走不通了,有太多语法、语义上的信息咱们须要知道后才能处理。
那么,如何去作语法解析呢?写一个java语法解析器吧。可是我最多只有一个月的时间,好像不太现实。
不能本身写就只能搜索下是否有现成的语法解析库,还真有!
JavaSymbolResolver是一个用于Java语法语义解析的库,其实现基础是JavaParser库。好比,有下面代码:
int a = 0; void foo() { while (true) { String a = "hello!"; Object foo = a + 1; } }
对于表达式a + 1中的a,JavaParser只能告诉咱们a是一个变量,而JavaSymbolResolver则能识别出这里的a是一个变量,其类型是String。
又例如,有以下A、B两个类:
import static B.b; public class A{ private int a; void foo(){ a = b + 1; } } public class B { public static int b = 2; }
JavaSymbolResolver可以识别出,b + 1表达式中的b便是B类中的b, 并且其初始值为2。
JavaSymbolResolver的这些强大的符号解析能力要基于JavaParser的语法解析。JavaParser接受一个java文件(或者代码片断),而后输出一个叫CompliationUnit的对象,叫编译单元,其内部结构是一个树形结构,被称做抽象语法树Abstract Syntax Tree(AST)。JavaParser 将源代码中的一个类定义、一个方法声明、一句方法调用语句,甚至一个break语句,都抽象为AST上的一个节点(Node),而ComplationUnit则是树的根节点,AST完整的描述了一个java文件。
图 8
例如,有以下代码:
package com.github.javaparser; import java.time.LocalDateTime; public class TimePrinter { public static void main(String args[]){ System.out.print(LocalDateTime.now()); } }
经过JavaParser处理后,输出以下语法树:
图 9
上图中展现了输出的ComplationUnit中包含了三个子节点,一个package申明,一个import申明,一个类定义。上图并无完整的描述整个语法数,绿色三角形的部分被省略了,下图展现了省略的MethodDeclatation部分:
图 10
经过其四个节点,咱们可看出其返回类型是void,方法名是main,方法参数是String args,以及其方法体:
图 11
能够看到,即便是System.out.print(LocalDateTime.now());这么一句代码,也能够完整的描述成一颗树。
有了AST后,咱们如何遍历这棵树呢?JavaPaser已经为咱们把遍历树的代码封装好了,而且提供了Visitor类,基于访问者模式,你只须要实现不一样的Visitor类来处理具体的节点,而不是将精力放在编写如何遍历树的代码上。
前面咱们已经说过,JavaSymbolResolver是创建在JavaParser上的,JavaSymbolResolver借助JavaParser的AST树,即可实现其符号解析。好比,当判断一个MethodCallExpr是不是对com.tencent.qqlive.ona.player.event.IEventProxy.publishEvent(com.tencent.qqlive.ona.player
.event.Event)的调用时,JavaSymbolResolver提供的solve方法,不断回溯当前节点的父节点,以找到这个MethodCallExpr方法调用声明的原型MethodDeclaration,MethodDeclaration记录了方法声明的全限定名,经过将全限定名与com.tencent.qqlive.ona.player.event.IEventProxy.publishEvent(com.tencent.qqlive.ona.player
.event.Event)比较是否相等,咱们即可得出结果。
一开始,我是经过新建工程,而后在工程build.gradle文件中,引入JavaSymbolResolver库的:
dependencies { compile group: 'com.github.javaparser', name: 'java-symbol-solver-core', version: '0.6.1'}
在开发过程当中,我发现这个库如今还很不稳定,有许多bug。例如,使用Lexical-Preserving Printing模式解析的AST,JavaSymbolResolver根本没有办法解析,会直接crash,因此致使我只能使用Pretty Printing模式解析java文件。有一些内部接口,JavaSymbolResolver也不能正确解析,好比,有以下代码:
public class BaseClass{ public interface AnInterface{ void doSomething(); } } public class ClassA extends BaseClass{ } public class ClassB implements ClassA.AnInterface{ public void doSomething(){ } }
遗憾的JavaSolverResolver 没法解析出ClassB的类型,由于ClassA.AnInterface没法解析出来,由于AnInterface没有定义在ClassA中,可是,咱们都知道,从java语法的角度,ClassB这么写是彻底正确的!
因为JavaSymbolResolver目前存在一些气人bug,因此我不得不下载他的源码,以修复这些阻碍个人bug,但愿JavaSymbolResolver尽快修复这些bug。
下面两张图是我用beyong compare将处理后的文件和处理以前的文件进行的对比,左边是处理后的文件,右边是原始文件。第一张图能够看出onEvent整个被删除了,第二张图能够看处处理后的文件末尾添加了不少@Subscrbe注解的方法,第三张图看到原始文件中的mEventProxy.publish()方法已经被替换成了对应的mEventBus.post()。
图 12
图 13
图 14
本文主要记述了我如何经过编写工具自动生成代码的方式,提升代码重构的效率。本来计划须要共计60人日的工做量,实际一我的只用了不到三周的时间便完成了任务。另外,本文还对注解解析,JavaSymbolResolver及JavaParser的基础知识进行了讲解。
因为文章已经比较长了,篇幅限制,本文并未对实现自动化工具的代码实现细节进行过多的讲解,这部份内容待到之后来分享了。
一站式知足电商节云计算需求的秘诀
重构代码的Tricks
Es2017 将会给咱们带来什么?
此文已由做者受权腾讯云技术社区发布,转载请注明文章出处
原文连接:
https://cloud.tencent.com/com...