亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專(zhuān)欄INFORMATION COLUMN

代碼自動(dòng)生成在重構(gòu)中的一次探索

ztyzz / 793人閱讀

摘要:事件只能攜帶一個(gè)的。例如在上述代碼示例中的將所有使用發(fā)布事件的地方,全部修改為使用的方法。是否能夠編寫(xiě)腳本或者自動(dòng)化工具,自動(dòng)化的完成重構(gòu)工作。實(shí)施方案使用注解解析自動(dòng)生成文件我們都知道,是通過(guò)注解來(lái)實(shí)現(xiàn)的。

歡迎大家前往騰訊云社區(qū),獲取更多騰訊海量技術(shù)實(shí)踐干貨哦~

作者:吳濤
導(dǎo)語(yǔ):EventBus 已經(jīng)火了很長(zhǎng)一段時(shí)間了。最近我們項(xiàng)目決定引入EventBus,替換我們播放器現(xiàn)在的事件總線框架,以解決我們存在的一些問(wèn)題。

自研事件機(jī)制介紹

騰訊視頻的播放器架構(gòu)是基于總線設(shè)計(jì)的,不同的功能模塊被抽象成一個(gè)個(gè)插件管理器,掛載在總線上,收聽(tīng)、發(fā)布事件,完成業(yè)務(wù)邏輯處理。

圖 1

上圖是播放器的總線示意圖,每個(gè)節(jié)點(diǎn)表示一個(gè)邏輯插件,紅色的線條代表總線。插件可以有子插件,父插件要負(fù)責(zé)將事件派發(fā)給它的子插件。

圖 2

上面三個(gè)類(lèi)圖中,Event是描述事件的類(lèi),不同的事件通過(guò)不同的id值來(lái)區(qū)分。IEventProxy即是播放器的總線,publish(Event event)方法負(fù)責(zé)將事件拋到總線上。Plugin即是插件的抽象類(lèi),當(dāng)總線上有新事件到達(dá)時(shí),插件的onEvent(Event event)方法會(huì)被調(diào)用,onEvent方法內(nèi)部根具事件的id值辨識(shí)不同的事件,做相應(yīng)的業(yè)務(wù)邏輯處理。擁有子插件的插件,還需要循環(huán)調(diào)用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);
        }
    }

一個(gè)插件將事件發(fā)布到總線上的代碼示例:

   @Override
 public void onClick(View v) { 
mEventProxy.publishEvent(Event.makeEvent(Event.UIEvent.ON_AUDIO_PLAY_ICON_CLICKED));
}
自研總線的缺陷

通過(guò)之前對(duì)播放器架構(gòu)的介紹,我們可以發(fā)現(xiàn),我們的事件機(jī)制還是比較簡(jiǎn)陋。主要存在以下幾點(diǎn)缺陷:
1、 插件代碼結(jié)構(gòu)不夠松散,所有事件響應(yīng)處理都在onEvent方法中處理。
2、 事件過(guò)度廣播。當(dāng)一個(gè)事件發(fā)生時(shí),所有插件的onEvent方法都會(huì)被調(diào)用執(zhí)行,浪費(fèi)了cpu時(shí)間片,程序執(zhí)行效率不高。
3、 事件類(lèi)型不安全。每個(gè)事件只能攜帶一個(gè)Object的對(duì)象message,事件收聽(tīng)者如果要解析message,收聽(tīng)者只能靠“猜”,是否猜中取決于發(fā)布該事件的人是否按照收聽(tīng)者的意愿攜帶指定類(lèi)型的message。如果沒(méi)有通過(guò)instanceof校驗(yàn)而直接強(qiáng)轉(zhuǎn),極有可能發(fā)生強(qiáng)轉(zhuǎn)失敗。
4、 事件參數(shù)不可拓展。事件只能攜帶一個(gè)Object的message。一旦某事件攜帶某種類(lèi)型的message,該事件攜帶的message類(lèi)型不能再變更,一旦變更,所有收聽(tīng)該事件的插件也必須要修改代碼。

基于此,我們決定引入EventBus開(kāi)源庫(kù)來(lái)重構(gòu)我們的事件機(jī)制。

EventBus介紹

了解過(guò)EventBus的同學(xué)都知道,EventBus的核心是使用反射。不同的事件用不同的類(lèi)型來(lái)表示,插件類(lèi)要收聽(tīng)某一事件,就要聲明一個(gè)相應(yīng)的方法來(lái)接收事件。例如,已知有AEvent,BEvent,CEvent三種事件,有X、Y、Z三個(gè)插件,假設(shè)X插件收聽(tīng)AEvent,Y插件收聽(tīng)BEvent,Z插件收聽(tīng)CEvent,則X、Y、Z三個(gè)插件類(lèi)中需如下聲明:

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();
}
}

當(dāng)我們需要發(fā)布某AEvent時(shí),需要調(diào)用EventBus的post方法:

    mEventBus.post(new AEvent());

更多如何使用EventBus及EventBus原理的知識(shí),這篇文章不作講解,您可以搜索其它文章或者在GitHub上了解。

工作量評(píng)估

通過(guò)以上分析,我們這次重構(gòu)的主要工作內(nèi)容就明確了:

1、 將Event類(lèi)中所有預(yù)定義的事件全部映射成具體的類(lèi),即有多少Event id就有多少Event類(lèi)的原則。比如,我們需要將Event.PageEvent.UPDATE_VIDEO轉(zhuǎn)換成UpdateVideoEvent.java。

2、 將插件的onEvent方法中switch語(yǔ)句中的每一條case語(yǔ)句映射為一個(gè)方法聲明,即有多少case就有多少方法原則。例如在上述代碼示例中的case Event.PageEvent.UPDATE_VIDEO:

@Subscribe
public void onUpdateVideoEvent(UpdateVideoEvent event){
     mVideoInfo = event.getVIdeoInfo();
}

3、 將所有使用IEventProxy發(fā)布事件的地方,全部修改為使用EventBus的post方法。比如有:

mEventProxy.publish(Event.makeEvent(Event.PageEvent.UPDATE_VIDEO, videoInfo));
要替換為:
mEventBus.post(new UpdateVideoEvent(videoInfo));

如果耐心把這篇文章看到這里的話,大家可能會(huì)覺(jué)得,你要做的工作很簡(jiǎn)單嘛,無(wú)壓力,so easy。

開(kāi)始工作之前,老大都要求我們先把工作量評(píng)估出來(lái)。由于代碼中有多少事件,有多少個(gè)插件,每個(gè)插件具體收聽(tīng)處理了多少種事件,這是很難統(tǒng)計(jì)出來(lái)的,特別是最后一點(diǎn)。不過(guò),工作量肯定和插件的個(gè)數(shù),以及插件的代碼規(guī)??隙ㄊ浅烧鹊模抑恍枰堰@兩點(diǎn)統(tǒng)計(jì)出來(lái),估計(jì)一個(gè)大概的工作量還是可以的。于是,有下面的統(tǒng)計(jì)表:

圖 3

橫坐標(biāo)是代碼行數(shù),縱坐標(biāo)是在插件個(gè)數(shù)。插件總個(gè)數(shù)有151個(gè),總代碼行數(shù)47000多行。按照每200行代碼1個(gè)小時(shí)的工作速度,每天8小時(shí)不停寫(xiě)代碼,一個(gè)人也要整整30個(gè)工作日,還不包括自測(cè),代碼審核等等其它工作量。我拿著這個(gè)表就去找老大說(shuō),兩個(gè)人需要三周的工作量。結(jié)果老大直接跟我說(shuō),幫手沒(méi)有,你一個(gè)人先搞,看看進(jìn)度咋樣(好吧,其實(shí)老大是對(duì)這個(gè)評(píng)估不滿意)。

就這樣,兩眼一抹黑,踏上了EventBus重構(gòu)之路。

第一天,我先入手了幾個(gè)插件類(lèi)。遇到需要映射的XXX事件,就手動(dòng)創(chuàng)建其對(duì)應(yīng)于的XXXEvent.java文件,此操作大概需要近一分鐘。將switch中的語(yǔ)句寫(xiě)成對(duì)應(yīng)的方法,然后把case中的語(yǔ)句復(fù)制到方法體中,此操作視語(yǔ)句長(zhǎng)度及case分支的多少,耗時(shí)不等。最后將onEvent方法刪除。就這樣一天工作下來(lái),不斷重復(fù)著這樣的工作,一個(gè)八百多行的插件竟耗費(fèi)了我半天工作時(shí)間,極其煩躁,而且人工修改還特別容易出錯(cuò),比如拼寫(xiě)錯(cuò)誤,漏掉case分支等等,帶來(lái)的后果直接表現(xiàn)在代碼運(yùn)行不正確,而后續(xù)卻難以排查。

于是,我有一個(gè)大膽的想法。程序員是腦力勞動(dòng)者,任何時(shí)候,都不應(yīng)該成為搬運(yùn)工。是否能夠編寫(xiě)腳本或者自動(dòng)化工具,自動(dòng)化的完成重構(gòu)工作。

實(shí)施方案

使用注解解析自動(dòng)生成文件

我們都知道,EventBus是通過(guò)注解來(lái)實(shí)現(xiàn)的。通過(guò)注解解析,在編譯階段生成了一個(gè)java文件,這個(gè)文件被稱(chēng)作SubscribeInfoIndex,其硬編碼了每個(gè)使用了Subscribe注解的類(lèi)的信息。

受到EventBus的啟發(fā),我們的事件類(lèi)是否也能通過(guò)注解解析的方式生成呢?答案是肯定的。關(guān)于注解解析相關(guān)的知識(shí)可參看我的另一篇KM《apt與JavaPoet 自動(dòng)生成代碼》,由于篇幅限制,這里不做講解。

首先,自定義一個(gè)注解:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface OldEvent {
    String packageName();
}

packageName 屬性指明該Event 類(lèi)對(duì)應(yīng)生成的新Event文件的包路徑。

然后在Event.java中使用該注解:

圖 4

圖 5

(注:PlayerEvent 和UIEvent是Event中定義的內(nèi)部類(lèi),事件Id定義在內(nèi)部類(lèi)中。除此之外,還有AudioEvent、PageEvent等)。
編寫(xiě)注解解析器,注解解析器的邏輯也比較簡(jiǎn)單:

圖 6

例如,PlayerEvent.INIT對(duì)應(yīng)生成的文件如下:

圖 7

語(yǔ)法解析修改代碼

現(xiàn)在,我們剩下的工作是如何完成代碼自動(dòng)替換,將publish替換為post,將case替換為方法。

我首先想到的是使用正則表達(dá)式,通過(guò)對(duì)源文件進(jìn)行掃描,將匹配的代碼行替換為指定代碼。比如,我們使用正則表達(dá)式^sw+.publishs(s(.+)s(,s(w+)s)?)來(lái)匹配代碼中的mEventProxy.publish()方法調(diào)用,然后將其替換為相應(yīng)的post。但是,我們僅僅通過(guò)正則匹配,沒(méi)有辦法確定匹配到的就是IEventProxy類(lèi)中com.tencent.qqlive.ona.player.event.IEventProxy.publishEvent(com.tencent.qqlive.ona.player.event.Event)的方法調(diào)用。例如,完全有可能有一個(gè)類(lèi)A,它內(nèi)部也聲明了一個(gè)public void publish(SomeKind params)方法,我們的正則也會(huì)匹配,導(dǎo)致錯(cuò)誤替換。另外,case語(yǔ)句的替換也是更加的困難。首先,哪些類(lèi)中的onEvent方法的switch case需要被替換?只有那些繼承自Plugin的類(lèi)才需要替換,如何判斷一個(gè)類(lèi)是否繼承自Plugin也是很難判斷的,不但有直接繼承,還有間接的繼承。

因此,正則匹配這條路是走不通了,有太多語(yǔ)法、語(yǔ)義上的信息我們需要知道后才能處理。

那么,如何去做語(yǔ)法解析呢?寫(xiě)一個(gè)java語(yǔ)法解析器吧。但是我最多只有一個(gè)月的時(shí)間,好像不太現(xiàn)實(shí)。

不能自己寫(xiě)就只能搜索下是否有現(xiàn)成的語(yǔ)法解析庫(kù),還真有!

JavaSymbolResolver介紹

JavaSymbolResolver是一個(gè)用于Java語(yǔ)法語(yǔ)義解析的庫(kù),其實(shí)現(xiàn)基礎(chǔ)是JavaParser庫(kù)。比如,有下面代碼:

int a = 0;

void foo() {
    while (true) {
        String a = "hello!";
        Object foo = a + 1;
    }
}

對(duì)于表達(dá)式a + 1中的a,JavaParser只能告訴我們a是一個(gè)變量,而JavaSymbolResolver則能識(shí)別出這里的a是一個(gè)變量,其類(lèi)型是String。

又例如,有如下A、B兩個(gè)類(lèi):

    import static B.b;
public class A{

private int a;
void foo(){
        a = b + 1;
}
}

public class B {
    public static int b = 2;
}

JavaSymbolResolver能夠識(shí)別出,b + 1表達(dá)式中的b即是B類(lèi)中的b, 而且其初始值為2。

JavaSymbolResolver的這些強(qiáng)大的符號(hào)解析能力要基于JavaParser的語(yǔ)法解析。JavaParser接受一個(gè)java文件(或者代碼片段),然后輸出一個(gè)叫CompliationUnit的對(duì)象,叫編譯單元,其內(nèi)部結(jié)構(gòu)是一個(gè)樹(shù)形結(jié)構(gòu),被稱(chēng)作抽象語(yǔ)法樹(shù)Abstract Syntax Tree(AST)。JavaParser 將源代碼中的一個(gè)類(lèi)定義、一個(gè)方法聲明、一句方法調(diào)用語(yǔ)句,甚至一個(gè)break語(yǔ)句,都抽象為AST上的一個(gè)節(jié)點(diǎn)(Node),而ComplationUnit則是樹(shù)的根節(jié)點(diǎn),AST完整的描述了一個(gè)java文件。

圖 8

例如,有如下代碼:

package com.github.javaparser;
import java.time.LocalDateTime;
public class TimePrinter {
public static void main(String args[]){
System.out.print(LocalDateTime.now());
}
 }

通過(guò)JavaParser處理后,輸出如下語(yǔ)法樹(shù):

圖 9

上圖中展示了輸出的ComplationUnit中包含了三個(gè)子節(jié)點(diǎn),一個(gè)package申明,一個(gè)import申明,一個(gè)類(lèi)定義。上圖并沒(méi)有完整的描述整個(gè)語(yǔ)法數(shù),綠色三角形的部分被省略了,下圖展示了省略的MethodDeclatation部分:

圖 10

通過(guò)其四個(gè)節(jié)點(diǎn),我們可看出其返回類(lèi)型是void,方法名是main,方法參數(shù)是String args,以及其方法體:

圖 11

可以看到,即使是System.out.print(LocalDateTime.now());這么一句代碼,也可以完整的描述成一顆樹(shù)。

有了AST后,我們?nèi)绾伪闅v這棵樹(shù)呢?JavaPaser已經(jīng)為我們把遍歷樹(shù)的代碼封裝好了,并且提供了Visitor類(lèi),基于訪問(wèn)者模式,你只需要實(shí)現(xiàn)不同的Visitor類(lèi)來(lái)處理具體的節(jié)點(diǎn),而不是將精力放在編寫(xiě)如何遍歷樹(shù)的代碼上。

前面我們已經(jīng)說(shuō)過(guò),JavaSymbolResolver是建立在JavaParser上的,JavaSymbolResolver借助JavaParser的AST樹(shù),便可實(shí)現(xiàn)其符號(hào)解析。比如,當(dāng)判斷一個(gè)MethodCallExpr是否是對(duì)com.tencent.qqlive.ona.player.event.IEventProxy.publishEvent(com.tencent.qqlive.ona.player
.event.Event)的調(diào)用時(shí),JavaSymbolResolver提供的solve方法,不斷回溯當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn),以找到這個(gè)MethodCallExpr方法調(diào)用聲明的原型MethodDeclaration,MethodDeclaration記錄了方法聲明的全限定名,通過(guò)將全限定名與com.tencent.qqlive.ona.player.event.IEventProxy.publishEvent(com.tencent.qqlive.ona.player
.event.Event)比較是否相等,我們便可得出結(jié)果。

使用JavaSymbolResolver進(jìn)行重構(gòu)

一開(kāi)始,我是通過(guò)新建工程,然后在工程build.gradle文件中,引入JavaSymbolResolver庫(kù)的:

dependencies {
compile group: "com.github.javaparser", name: "java-symbol-solver-core", version: "0.6.1"}

在開(kāi)發(fā)過(guò)程中,我發(fā)現(xiàn)這個(gè)庫(kù)現(xiàn)在還很不穩(wěn)定,有許多bug。例如,使用Lexical-Preserving Printing模式解析的AST,JavaSymbolResolver根本沒(méi)有辦法解析,會(huì)直接crash,所以導(dǎo)致我只能使用Pretty Printing模式解析java文件。有一些內(nèi)部接口,JavaSymbolResolver也不能正確解析,比如,有如下代碼:

public class BaseClass{
  public interface AnInterface{
        void doSomething();
}
}

public class ClassA extends BaseClass{

}

public class ClassB implements ClassA.AnInterface{
    public void doSomething(){

    }
}

遺憾的JavaSolverResolver 無(wú)法解析出ClassB的類(lèi)型,因?yàn)镃lassA.AnInterface無(wú)法解析出來(lái),因?yàn)锳nInterface沒(méi)有定義在ClassA中,但是,我們都知道,從java語(yǔ)法的角度,ClassB這么寫(xiě)是完全正確的!

由于JavaSymbolResolver目前存在一些氣人bug,所以我不得不下載他的源碼,以修復(fù)這些阻礙我的bug,希望JavaSymbolResolver盡快修復(fù)這些bug。

下面兩張圖是我用beyong compare將處理后的文件和處理之前的文件進(jìn)行的對(duì)比,左邊是處理后的文件,右邊是原始文件。第一張圖可以看出onEvent整個(gè)被刪除了,第二張圖可以看到處理后的文件末尾添加了很多@Subscrbe注解的方法,第三張圖看到原始文件中的mEventProxy.publish()方法已經(jīng)被替換成了對(duì)應(yīng)的mEventBus.post()。

圖 12

圖 13

圖 14

總結(jié)

本文主要記述了我如何通過(guò)編寫(xiě)工具自動(dòng)生成代碼的方式,提高代碼重構(gòu)的效率。原本計(jì)劃需要共計(jì)60人日的工作量,實(shí)際一個(gè)人只用了不到三周的時(shí)間便完成了任務(wù)。另外,本文還對(duì)注解解析,JavaSymbolResolver及JavaParser的基礎(chǔ)知識(shí)進(jìn)行了講解。

由于文章已經(jīng)比較長(zhǎng)了,篇幅限制,本文并未對(duì)實(shí)現(xiàn)自動(dòng)化工具的代碼實(shí)現(xiàn)細(xì)節(jié)進(jìn)行過(guò)多的講解,這部分內(nèi)容待到以后來(lái)分享了。

閱讀推薦

一站式滿足電商節(jié)云計(jì)算需求的秘訣
重構(gòu)代碼的Tricks
Es2017 將會(huì)給我們帶來(lái)什么?

此文已由作者授權(quán)騰訊云技術(shù)社區(qū)發(fā)布,轉(zhuǎn)載請(qǐng)注明文章出處
原文鏈接:
https://cloud.tencent.com/com...

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/67976.html

相關(guān)文章

  • 我的編程成長(zhǎng)四步曲

    摘要:評(píng)估目標(biāo)并將其拆解成任務(wù)。依據(jù)中心思考我將這篇文章分成了四小節(jié)。為了這個(gè)我們需要分成幾步,或者幾層設(shè)計(jì)。每個(gè)人都可以用不同的方式成長(zhǎng),知道自己的喜歡的然后去計(jì)劃。 這次我決定不耍流氓的寫(xiě)一篇雞湯,這篇是以過(guò)程到結(jié)果的文章——以前老是寫(xiě)結(jié)果,總感覺(jué)不好~~。 Blabla,群聊的時(shí)候,看到一個(gè)網(wǎng)站有一個(gè)Most active GitHub users的排名,發(fā)現(xiàn)我在里面的位置是20——在...

    helloworldcoding 評(píng)論0 收藏0
  • JavaScript是如何工作: 深入探索 websocket 和HTTP/2與SSE +如何選擇正

    摘要:數(shù)據(jù)作為消息通過(guò)傳輸,每個(gè)消息由一個(gè)或多個(gè)幀組成,其中包含正在發(fā)送的數(shù)據(jù)有效負(fù)載。幀數(shù)據(jù)如上所述,數(shù)據(jù)可以被分割成多個(gè)幀。但是,規(guī)范希望能夠處理交錯(cuò)的控制幀。 文章底部分享給大家一套 react + socket 實(shí)戰(zhàn)教程 這是專(zhuān)門(mén)探索 JavaScript 及其所構(gòu)建的組件的系列文章的第5篇。 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來(lái)篇優(yōu)質(zhì)文章等著你! 如果你錯(cuò)過(guò)了前面的章...

    cuieney 評(píng)論0 收藏0
  • 我的 2015 年度小結(jié)(技術(shù)方面)

    摘要:因?yàn)槁酚蓪用媸軜I(yè)務(wù)影響很大,經(jīng)常修改一些功能的行為,所以后來(lái)大部分測(cè)試都是針對(duì)層面的單元測(cè)試。在我了解的過(guò)程中,我發(fā)現(xiàn)中文網(wǎng)絡(luò)上對(duì)的討論非常分散,于是我創(chuàng)建了中文社區(qū),到年末已經(jīng)有個(gè)注冊(cè)用戶和個(gè)帖子了。 https://jysperm.me/2016/02/programming-of-2015/ 從 2014 年末開(kāi)始開(kāi)發(fā)的一個(gè)互聯(lián)網(wǎng)金融項(xiàng)目終于在今年三月份上線了,這是一個(gè) Node...

    宋華 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<