摘要:事件只能攜帶一個的。例如在上述代碼示例中的將所有使用發布事件的地方,全部修改為使用的方法。是否能夠編寫腳本或者自動化工具,自動化的完成重構工作。實施方案使用注解解析自動生成文件我們都知道,是通過注解來實現的。
歡迎大家前往騰訊云社區,獲取更多騰訊海量技術實踐干貨哦~
自研事件機制介紹作者:吳濤
導語:EventBus 已經火了很長一段時間了。最近我們項目決定引入EventBus,替換我們播放器現在的事件總線框架,以解決我們存在的一些問題。
騰訊視頻的播放器架構是基于總線設計的,不同的功能模塊被抽象成一個個插件管理器,掛載在總線上,收聽、發布事件,完成業務邏輯處理。
圖 1
上圖是播放器的總線示意圖,每個節點表示一個邏輯插件,紅色的線條代表總線。插件可以有子插件,父插件要負責將事件派發給它的子插件。
圖 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); } }
一個插件將事件發布到總線上的代碼示例:
@Override public void onClick(View v) { mEventProxy.publishEvent(Event.makeEvent(Event.UIEvent.ON_AUDIO_PLAY_ICON_CLICKED)); }自研總線的缺陷
通過之前對播放器架構的介紹,我們可以發現,我們的事件機制還是比較簡陋。主要存在以下幾點缺陷:
1、 插件代碼結構不夠松散,所有事件響應處理都在onEvent方法中處理。
2、 事件過度廣播。當一個事件發生時,所有插件的onEvent方法都會被調用執行,浪費了cpu時間片,程序執行效率不高。
3、 事件類型不安全。每個事件只能攜帶一個Object的對象message,事件收聽者如果要解析message,收聽者只能靠“猜”,是否猜中取決于發布該事件的人是否按照收聽者的意愿攜帶指定類型的message。如果沒有通過instanceof校驗而直接強轉,極有可能發生強轉失敗。
4、 事件參數不可拓展。事件只能攜帶一個Object的message。一旦某事件攜帶某種類型的message,該事件攜帶的message類型不能再變更,一旦變更,所有收聽該事件的插件也必須要修改代碼。
基于此,我們決定引入EventBus開源庫來重構我們的事件機制。
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上了解。
工作量評估通過以上分析,我們這次重構的主要工作內容就明確了:
1、 將Event類中所有預定義的事件全部映射成具體的類,即有多少Event id就有多少Event類的原則。比如,我們需要將Event.PageEvent.UPDATE_VIDEO轉換成UpdateVideoEvent.java。
2、 將插件的onEvent方法中switch語句中的每一條case語句映射為一個方法聲明,即有多少case就有多少方法原則。例如在上述代碼示例中的case Event.PageEvent.UPDATE_VIDEO:
@Subscribe public void onUpdateVideoEvent(UpdateVideoEvent event){ mVideoInfo = event.getVIdeoInfo(); }
3、 將所有使用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替換為方法。
我首先想到的是使用正則表達式,通過對源文件進行掃描,將匹配的代碼行替換為指定代碼。比如,我們使用正則表達式^sw+.publishs(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介紹
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...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67976.html
摘要:評估目標并將其拆解成任務。依據中心思考我將這篇文章分成了四小節。為了這個我們需要分成幾步,或者幾層設計。每個人都可以用不同的方式成長,知道自己的喜歡的然后去計劃。 這次我決定不耍流氓的寫一篇雞湯,這篇是以過程到結果的文章——以前老是寫結果,總感覺不好~~。 Blabla,群聊的時候,看到一個網站有一個Most active GitHub users的排名,發現我在里面的位置是20——在...
摘要:數據作為消息通過傳輸,每個消息由一個或多個幀組成,其中包含正在發送的數據有效負載。幀數據如上所述,數據可以被分割成多個幀。但是,規范希望能夠處理交錯的控制幀。 文章底部分享給大家一套 react + socket 實戰教程 這是專門探索 JavaScript 及其所構建的組件的系列文章的第5篇。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 如果你錯過了前面的章...
摘要:因為路由層面受業務影響很大,經常修改一些功能的行為,所以后來大部分測試都是針對層面的單元測試。在我了解的過程中,我發現中文網絡上對的討論非常分散,于是我創建了中文社區,到年末已經有個注冊用戶和個帖子了。 https://jysperm.me/2016/02/programming-of-2015/ 從 2014 年末開始開發的一個互聯網金融項目終于在今年三月份上線了,這是一個 Node...
閱讀 2135·2021-10-14 09:43
閱讀 2197·2019-08-30 15:55
閱讀 726·2019-08-30 14:23
閱讀 2019·2019-08-30 13:21
閱讀 1235·2019-08-30 12:50
閱讀 2199·2019-08-29 18:46
閱讀 2280·2019-08-29 17:28
閱讀 2359·2019-08-29 17:21