歡迎你們前往騰訊雲社區,獲取更多騰訊海量技術實踐乾貨哦~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...