SGsonFormat——Android Studio插件二次開發(fā)

Studio主流插件

我們知道Android Studio是基于Intellij的一套IDE環(huán)境,Intellij本身為開發(fā)者提供了插件式的開發(fā)環(huán)境,大大提高了開發(fā)效率和IDE可配置化。目前studio的成熟插件已有很多。
我們這里先來看看目前已有的主流Studio插件有哪些,幾乎已經(jīng)涵蓋了你所有的需求:
https://juejin.im/entry/5998090ff265da248a7a6bde

基本可以把插件的功能分為以下幾類:
1、解決重復(fù)性工作:

把studio工作中技術(shù)含量低,重復(fù)性高的工作,用插件形式代替。

插件名稱 插件功能
GsonFormat (jsonString自動生成JavaBean類)
ButterKnife Zelezny (xml自動生成butterknife的注解代碼)
Code Generator (xml自動生成activity fragment)
AndroidProguardPlugin (根據(jù)依賴的第三方庫,生成proguard文件)
Exynap (更加擴(kuò)展,把成型、固定的代碼段,自動生成)
MVPHelper (自動生成 M V P 到不同文件夾)
2、集成studio不包含的功能:

為了開發(fā)的方便,將studio本身不具備的功能引入,擴(kuò)展IDE的功能,避免studio和第三方來回切換和數(shù)據(jù)傳輸?shù)穆闊?/p>

插件名稱 插件功能
EventBus3 Intellij (輔助 索引eventbus 從subscribe到post,提高eventbus可讀性)
GradleDependenciesHelperPlugin (gradle依賴自動補(bǔ)全)
SQLScout (調(diào)試sqlite)
FindBugs-IDEA (findbugs插件)
Android Methods Count (預(yù)覽依賴庫中方法數(shù),提前判斷方法數(shù)超限)

各插件的開發(fā)和使用成熟度很高,大部分是免費(fèi)并且開源的,活躍度也很高。因?yàn)閟tudio的使用率極高,而且IntelliJ IDE本身的插件資源豐富,直接借鑒的插件也有很多,使得插件開發(fā)的門檻大大降低。
那么我們自己再遇到重復(fù)性高的工作,或者第三方功能需要嵌入時,也建議考慮插件的方式。

插件的安裝方法

1、Preferences - plugins - Browse repositories 查找jetBrains遠(yuǎn)程倉庫上的插件
很多插件是免費(fèi)且開源的(github),遠(yuǎn)程repositories上對應(yīng)的plugins都是最新的release版本
我們可以使用beta版本,或者自己對開源插件進(jìn)行二次開發(fā),這時就需要安裝本地插件:
2、Preferences - plugins - Install plugin from disk 查找本地plugin的jar包

本文會以一個我自己二次開發(fā)的plugin為例,記錄下plugin開發(fā)的基本流程和值得注意的坑。


插件開發(fā)

studio是基于IntelliJ的二次開發(fā)的IDE,所以plugins其實(shí)是IntelliJ的插件,IntelliJ這個IDE本身就可以開發(fā)plugins,IntelliJ下載免費(fèi)版即可,官網(wǎng)下載,community版本夠用。不再贅述。

新建及import工程

新建project很多文章都有講,不贅述,可以參考:http://www.lxweimin.com/p/336a07b9d98a
基本是配置IntelliJ sdk、創(chuàng)建plugin project、然后在plugin.xml中配置此插件即可

重點(diǎn)說下import工程,如果你是二次開發(fā)一個插件,那么import一個github已有的工程是必須的。以GsonFormat為例(https://github.com/zzz40500/GsonFormat/
github工程下,分為兩個分支master和dev_1.2.2其中dev開發(fā)分支可以直接用于二次開發(fā)。(master分支直接import作為project,需要IDE配置很多東西)

dev分支工程配置步驟

  1. import project from existing code
    注意我們的工程是plugin,選擇的sdk不是jdk1.8(此處同new project),而是IntelliJ IDEA Community,一路‘下一步’這個過程中,有一步已經(jīng)把src下代碼作為module放入了project,生成了GsonFormat.iml,
  2. 只是此時GsonFormat.iml中module type是JAVA_MODULE,而不是PLUGIN_MODULE,需要修改。
    (project的module設(shè)置很重要,決定了project是否可以正常編譯)
    此處配置成功的標(biāo)志就是 IDE出現(xiàn)了run 和 debug兩個按鈕。如果還不正常,可以進(jìn)入IDE右上角的按鈕進(jìn)入project structure進(jìn)行配置


    配置sdk
  3. 如果想正常編譯插件,還需要一步,在 Edit Configuration中配置project的屬性,見圖二,新建一個Plugin Configuration,在右側(cè)的Use classpath of module中選擇剛剛的GsonFormat(由于剛剛我們成功配置了GsonFormat為PLUGIN_MODULE,否則此處找不到哦)


    配置plugin的Module
  4. 到了這一步,無論是run debug 還是Prepare Plugin Module For Deployment(產(chǎn)出本地plugin jar)都可以了。
分析plugin工程

import成功后,我們看一下plugin工程是怎樣的?
plugin工程中常見以下三類文件,也是plugin工程較為特有的文件類型:

  1. Action
    作為整個插件的入口類,其入口方式和name等定義在plugin.xml,Action中actionPerformed作為入口方法,初始化當(dāng)前類,包,傳入到dialog中

  2. Dialog 類似于android中的activity,綁定了Form類,用于view的databinding和邏輯
    JsonDialog是入口dialog,F(xiàn)ieldsDialog是解析jsonstring后展示的dialog,SettingDialog是配置dialog

  3. GUI Form 類似于android中xml布局文件,只不過此處是swing的拖拽控件,F(xiàn)orm與Dialog是配對出現(xiàn),其對應(yīng)關(guān)系在Form配置。

GsonFormat代碼架構(gòu)

以GsonFormat plugin為例,具體講清楚plugin工程的組成和實(shí)現(xiàn)原理。
(GsonFormat插件是把jsonString轉(zhuǎn)變?yōu)閖avaBean的前端插件,寫業(yè)務(wù)代碼的朋友們應(yīng)該非常熟悉,這款插件的使用過程是這樣子的:)
第一步:彈窗:輸入你要轉(zhuǎn)換的jsonString,此處也可以Setting進(jìn)行配置


image

第二步:彈窗:展示轉(zhuǎn)換成功的field class,你可以在此基礎(chǔ)上自定義。


image

最后:我們得到了我們想要的javaBean
image

這個插件的基本功能如上,下面我們簡單分析下源碼:
代碼(類)的組織方式

主Action是MainAction,作為插件的入口可以看到他啟動了彈窗JsonDialog。工程中維護(hù)了幾個dialog(包括java文件和form表單文件),分別對應(yīng)插件工作中所有的彈窗,被放入了ui文件夾。
[圖片上傳失敗...(image-108cd0-1542190836830)]
再來看其他文件夾:
[圖片上傳失敗...(image-4be0b0-1542190836830)]

  1. DataWriter類負(fù)責(zé)GsonFormat最后一步寫入class文件,
  2. config文件夾中類維護(hù)了插件的settings屬性(屬性用戶可以在SettingsDialog配置),
  3. entity文件夾內(nèi)是實(shí)體類,classEntity fieldEntity等類都是維護(hù)最終生成class中的field及innerclass的實(shí)體類。
  4. process文件夾內(nèi)是處理類,jsonstring的解析,javaBean封裝等具體的操作都是在這些類中完成的,是插件的核心類。
處理流程

處理流程的代碼邏輯是流式的,從MainAction入口開始看起,在JsonDialog中點(diǎn)擊確定后,開始解析jsonString。
類JsonUtilsDialog中,點(diǎn)擊事件的響應(yīng)函數(shù)作為入口:

editTP.addKeyListener(new KeyAdapter() {
    @Override
    public void keyReleased(KeyEvent keyEvent) {
        super.keyReleased(keyEvent);
        if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER) {
            onOK();
        }
    }
});

1.解析的過程,主要在ConvertBridge類完成,由run方法作入口,開始解析jsonSTR
onOK()方法的實(shí)現(xiàn):

private void onOK() {
    //省略部分:get PsiClass generateClass:
    new ConvertBridge(
            this, errorLB, jsonSTR, mFile, mProject, generateClass,
            mClass, generateClassName).run();
}

2.ConvertBridge類中run方法,通過parseJson方法,開始解析jsonString,

    public void parseJson(JSONObject json) {
        if (Config.getInstant().isVirgoMode()) {
          //省略代碼:裝配mGenerateEntity對象
          //createFields 解析jsonString核心方法
          mGenerateEntity.setFields(createFields(json, fieldList, mGenerateEntity));
          FieldsDialog fieldsDialog = new FieldsDialog(mJsonUtilsDialog, mGenerateEntity, mFactory,
                    mGeneratClass, currentClass, mFile, project, generateClassName);
        } else {
          mGenerateEntity.setFields(createFields(json, fieldList, mGenerateEntity));
          WriterUtil writerUtil = new WriterUtil(null, null, mFile, project, mGeneratClass);
          writerUtil.mInnerClassEntity = mGenerateEntity;
          writerUtil.execute();
        }
    }

分為Virgo模式和非Virgo模式(默認(rèn)virgo模式):virgo模式,就是啟動FieldsDialog,就是我們見到的第二個窗口,用戶自行修改fields的定義,非virgo比較簡單,跳過dialog直接寫入fields到class里。除非settings自己定義,否則我們一般都使用virgo模式。可以看到無論是否virgo模式與否,都會調(diào)用createFields方法,區(qū)別只是是否顯示FieldsDialog。

3.詳細(xì)看下createFields方法做了什么。

for (int i = 0; i < list.size(); i++) {
    String key = list.get(i);
    Object type = json.get(key);
    if (type instanceof JSONArray) {
        //將jsonArray放入listEntityList
        listEntityList.add(key);
        continue;
    }
    FieldEntity fieldEntity = createFiled(parentClass, key, type);
    fieldEntityList.add(fieldEntity);
}
for (int i = 0; i < listEntityList.size(); i++) {
    //解析listEntityList中數(shù)據(jù)
    String key = listEntityList.get(i);
    Object type = json.get(key);

    FieldEntity fieldEntity = createFiled(parentClass, key, type);
    fieldEntityList.add(fieldEntity);
}

通過createFields方法把field放入FieldEntity,DataWriter再根據(jù)FieldEntity的內(nèi)容寫入class中,

4.以上的解析過程,只涉及了一層JavaBean的情況,JavaBean大部分情況下,是要嵌套Bean內(nèi)部類的,就是JSONObject內(nèi)部是嵌套jsonobject的,我們繼續(xù)來看:

private FieldEntity typeByValue(InnerClassEntity parentClass, String key, Object type) {
        if (type instanceof JSONObject) {
            InnerClassEntity classEntity = checkInnerClass((JSONObject) type);
            if (classEntity == null) {
                //省略代碼
            } else {
                FieldEntity fieldEntity = new FieldEntity();
                fieldEntity.setKey(key);
                fieldEntity.setTargetClass(classEntity);
                fieldEntity.setType("%s");
                nodeBean = fieldEntity;
            }
        }

createFields方法中對每個fieldEntity依次調(diào)用createField方法,createFiled方法中調(diào)用了typeByValue方法,在createInnnerClass中 對子json再次進(jìn)行createFields方法,如此依次遞歸。完成了一層層javabean的解析工作。
可以說Class中包括FieldEntry,而FieldEntry本身也是包含多個子FieldEntry的。FieldEntry可以設(shè)置基本類型,也可以設(shè)置ClassEntity。

5.最終通過WriterUtil類將FieldEntry寫入到class文件中,完成了整個的插件功能。

二次開發(fā)的部分

開發(fā)中遇到的問題

由于代碼混淆的原因,開發(fā)中經(jīng)常遇到debug下正常的代碼,在release包情況下無法正常解析網(wǎng)路數(shù)據(jù),因?yàn)閖avabean類中的field混淆后已經(jīng)不是原來定義的名稱了。而這個問題在提測關(guān)口最容易出現(xiàn)。想解決這個問題必須保證javaBean在打包中不被混淆。
如何不被混淆,不同廠商有不同的解決策略(規(guī)范):

  1. 統(tǒng)一放到一個文件夾里(或者含固定名稱的文件夾),混淆時ignore 這些文件夾。
    但是這辦法操作起來不完美,一個是重構(gòu)后文件夾容易變名字,還有團(tuán)隊(duì)開發(fā)時無法保證所有的人都遵守。處理代碼時都要繃著文件夾名稱這個弦。
  2. 所有JavaBean extends Serializable(統(tǒng)一基類)
    這樣proguard文件中保證所有Seriallizable的子類不被混淆即可。而且Bundle傳遞參數(shù)時javaBean可以直接被用。但是這個辦法也有一個缺點(diǎn):需要保證所有人遵守這個約定,無法規(guī)范這個步驟。
我們的解決方式:

一般我們的接口管理系統(tǒng)中,都可以產(chǎn)生mock的jsonString,客戶端開發(fā)會直接利用SGsonFormat插件將jsonString直接轉(zhuǎn)為JavaBean,所以基于GsonFormat功能二次開發(fā),讓所有的JavaBean class統(tǒng)一繼承Serializable,這樣兼顧了易用性和統(tǒng)一性。

GsonFormat的二次開發(fā):

統(tǒng)一繼承Serializable的邏輯,應(yīng)該放入DataWriter寫入的流程中,分析可得:在ClassProcessor中process方法,實(shí)際上將classContent的String內(nèi)容通過PsiElementFactory寫入class文件中,所以修改String classContent既可。

protected void generateClass(PsiElementFactory factory, ClassEntity classEntity, PsiClass parentClass, IProcessor visitor) {

    onStartGenerateClass(factory, classEntity, parentClass, visitor);
    PsiClass generateClass = null;
    if (classEntity.isGenerate()) {
        if (Config.getInstant().isSplitGenerate()) {
            try {
                generateClass = PsiClassUtil.getPsiClass(
                        parentClass.getContainingFile(), parentClass.getProject(), classEntity.getQualifiedName());
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } else {
            //根據(jù)classContent創(chuàng)建class
            String classContent =
                    "public static class " + classEntity.getClassName() + " implements Serializable" + "{}";
            generateClass = factory.createClassFromText(classContent, null).getInnerClasses()[0];
        }

        if (generateClass != null) {
            //遞歸調(diào)用,創(chuàng)建內(nèi)部類
            for (ClassEntity innerClass : classEntity.getInnerClasss()) {
                generateClass(factory, innerClass, generateClass, visitor);
            }
            if (!Config.getInstant().isSplitGenerate()) {
                generateClass = (PsiClass) parentClass.add(generateClass);
            }
            //創(chuàng)建內(nèi)部變量
            for (FieldEntity fieldEntity : classEntity.getFields()) {
                generateField(factory, fieldEntity, generateClass, classEntity);
            }
            //創(chuàng)建內(nèi)部變量getter setter方法
            generateGetterAndSetter(factory, generateClass, classEntity);
            generateConvertMethod(factory, generateClass, classEntity);
        }
    }
    onEndGenerateClass(factory, classEntity, parentClass, generateClass, visitor);
    if (Config.getInstant().isSplitGenerate()) {
        formatJavCode(generateClass);
    }
}

插件下載地址:(該插件已提交repository) https://plugins.jetbrains.com/plugin/11100-sgsonformat/update/49532
或者直接搜索SGsonFormat,install即可使用

總結(jié)

二次開發(fā)的改動并不大,但是把Studio的plugin開發(fā)環(huán)境和流程算是熟悉了一遍,plugin插件的開發(fā)可以說你會用java就能上手,只不過他自定義的文件類型和組織方式需要熟悉。如果有需要的話,做個新的plugin提高工作效率,是個很好的方式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,716評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,811評論 2 59
  • 原地址:https://ydmmocoo.github.io/2016/06/28/Android-Studio%...
    菜菜編程閱讀 603評論 1 3
  • 現(xiàn)在Android的開發(fā)者基本上都使用Android Studio進(jìn)行開發(fā)(如果你還在使用eclipse那也行,畢...
    零寬度接合閱讀 1,649評論 0 11
  • 因?yàn)榕R時決定出門旅游,又是第一次全家泡溫泉,玩水上公園,早飯后迅速開車直奔商場買了泳裝和泳褲。小外甥點(diǎn)著柜臺里...
    娟1216閱讀 460評論 0 0