SGsonFormat——Android Studio插件二次开发

Studio主流插件

我们知道Android Studio是基于Intellij的一套IDE环境,Intellij本身为开发者提供了插件式的开发环境,大大提高了开发效率和IDE可配置化。目前studio的成熟插件已有很多。
我们这里先来看看目前已有的主流Studio插件有哪些,几乎已经涵盖了你所有的需求:
https://juejin.im/entry/5998090ff265da248a7a6bde

基本可以把插件的功能分为以下几类:
1、解决重复性工作:

把studio工作中技术含量低,重复性高的工作,用插件形式代替。

插件名称 插件功能
GsonFormat (jsonString自动生成JavaBean类)
ButterKnife Zelezny (xml自动生成butterknife的注解代码)
Code Generator (xml自动生成activity fragment)
AndroidProguardPlugin (根据依赖的第三方库,生成proguard文件)
Exynap (更加扩展,把成型、固定的代码段,自动生成)
MVPHelper (自动生成 M V P 到不同文件夹)
2、集成studio不包含的功能:

为了开发的方便,将studio本身不具备的功能引入,扩展IDE的功能,避免studio和第三方来回切换和数据传输的麻烦。

插件名称 插件功能
EventBus3 Intellij (辅助 索引eventbus 从subscribe到post,提高eventbus可读性)
GradleDependenciesHelperPlugin (gradle依赖自动补全)
SQLScout (调试sqlite)
FindBugs-IDEA (findbugs插件)
Android Methods Count (预览依赖库中方法数,提前判断方法数超限)

各插件的开发和使用成熟度很高,大部分是免费并且开源的,活跃度也很高。因为studio的使用率极高,而且IntelliJ IDE本身的插件资源丰富,直接借鉴的插件也有很多,使得插件开发的门槛大大降低。
那么我们自己再遇到重复性高的工作,或者第三方功能需要嵌入时,也建议考虑插件的方式。

插件的安装方法

1、Preferences - plugins - Browse repositories 查找jetBrains远程仓库上的插件
很多插件是免费且开源的(github),远程repositories上对应的plugins都是最新的release版本
我们可以使用beta版本,或者自己对开源插件进行二次开发,这时就需要安装本地插件:
2、Preferences - plugins - Install plugin from disk 查找本地plugin的jar包

本文会以一个我自己二次开发的plugin为例,记录下plugin开发的基本流程和值得注意的坑。


插件开发

studio是基于IntelliJ的二次开发的IDE,所以plugins其实是IntelliJ的插件,IntelliJ这个IDE本身就可以开发plugins,IntelliJ下载免费版即可,官网下载,community版本够用。不再赘述。

新建及import工程

新建project很多文章都有讲,不赘述,可以参考:https://www.jianshu.com/p/336a07b9d98a
基本是配置IntelliJ sdk、创建plugin project、然后在plugin.xml中配置此插件即可

重点说下import工程,如果你是二次开发一个插件,那么import一个github已有的工程是必须的。以GsonFormat为例(https://github.com/zzz40500/GsonFormat/
github工程下,分为两个分支master和dev_1.2.2其中dev开发分支可以直接用于二次开发。(master分支直接import作为project,需要IDE配置很多东西)

dev分支工程配置步骤

  1. import project from existing code
    注意我们的工程是plugin,选择的sdk不是jdk1.8(此处同new project),而是IntelliJ IDEA Community,一路‘下一步’这个过程中,有一步已经把src下代码作为module放入了project,生成了GsonFormat.iml,
  2. 只是此时GsonFormat.iml中module type是JAVA_MODULE,而不是PLUGIN_MODULE,需要修改。
    (project的module设置很重要,决定了project是否可以正常编译)
    此处配置成功的标志就是 IDE出现了run 和 debug两个按钮。如果还不正常,可以进入IDE右上角的按钮进入project structure进行配置


    配置sdk
  3. 如果想正常编译插件,还需要一步,在 Edit Configuration中配置project的属性,见图二,新建一个Plugin Configuration,在右侧的Use classpath of module中选择刚刚的GsonFormat(由于刚刚我们成功配置了GsonFormat为PLUGIN_MODULE,否则此处找不到哦)


    配置plugin的Module
  4. 到了这一步,无论是run debug 还是Prepare Plugin Module For Deployment(产出本地plugin jar)都可以了。
分析plugin工程

import成功后,我们看一下plugin工程是怎样的?
plugin工程中常见以下三类文件,也是plugin工程较为特有的文件类型:

  1. Action
    作为整个插件的入口类,其入口方式和name等定义在plugin.xml,Action中actionPerformed作为入口方法,初始化当前类,包,传入到dialog中

  2. Dialog 类似于android中的activity,绑定了Form类,用于view的databinding和逻辑
    JsonDialog是入口dialog,FieldsDialog是解析jsonstring后展示的dialog,SettingDialog是配置dialog

  3. GUI Form 类似于android中xml布局文件,只不过此处是swing的拖拽控件,Form与Dialog是配对出现,其对应关系在Form配置。

GsonFormat代码架构

以GsonFormat plugin为例,具体讲清楚plugin工程的组成和实现原理。
(GsonFormat插件是把jsonString转变为javaBean的前端插件,写业务代码的朋友们应该非常熟悉,这款插件的使用过程是这样子的:)
第一步:弹窗:输入你要转换的jsonString,此处也可以Setting进行配置


image

第二步:弹窗:展示转换成功的field class,你可以在此基础上自定义。


image

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

这个插件的基本功能如上,下面我们简单分析下源码:
代码(类)的组织方式

主Action是MainAction,作为插件的入口可以看到他启动了弹窗JsonDialog。工程中维护了几个dialog(包括java文件和form表单文件),分别对应插件工作中所有的弹窗,被放入了ui文件夹。
[图片上传失败...(image-108cd0-1542190836830)]
再来看其他文件夹:
[图片上传失败...(image-4be0b0-1542190836830)]

  1. DataWriter类负责GsonFormat最后一步写入class文件,
  2. config文件夹中类维护了插件的settings属性(属性用户可以在SettingsDialog配置),
  3. entity文件夹内是实体类,classEntity fieldEntity等类都是维护最终生成class中的field及innerclass的实体类。
  4. process文件夹内是处理类,jsonstring的解析,javaBean封装等具体的操作都是在这些类中完成的,是插件的核心类。
处理流程

处理流程的代码逻辑是流式的,从MainAction入口开始看起,在JsonDialog中点击确定后,开始解析jsonString。
类JsonUtilsDialog中,点击事件的响应函数作为入口:

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()方法的实现:

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模式(默认virgo模式):virgo模式,就是启动FieldsDialog,就是我们见到的第二个窗口,用户自行修改fields的定义,非virgo比较简单,跳过dialog直接写入fields到class里。除非settings自己定义,否则我们一般都使用virgo模式。可以看到无论是否virgo模式与否,都会调用createFields方法,区别只是是否显示FieldsDialog。

3.详细看下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中数据
    String key = listEntityList.get(i);
    Object type = json.get(key);

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

通过createFields方法把field放入FieldEntity,DataWriter再根据FieldEntity的内容写入class中,

4.以上的解析过程,只涉及了一层JavaBean的情况,JavaBean大部分情况下,是要嵌套Bean内部类的,就是JSONObject内部是嵌套jsonobject的,我们继续来看:

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依次调用createField方法,createFiled方法中调用了typeByValue方法,在createInnnerClass中 对子json再次进行createFields方法,如此依次递归。完成了一层层javabean的解析工作。
可以说Class中包括FieldEntry,而FieldEntry本身也是包含多个子FieldEntry的。FieldEntry可以设置基本类型,也可以设置ClassEntity。

5.最终通过WriterUtil类将FieldEntry写入到class文件中,完成了整个的插件功能。

二次开发的部分

开发中遇到的问题

由于代码混淆的原因,开发中经常遇到debug下正常的代码,在release包情况下无法正常解析网路数据,因为javabean类中的field混淆后已经不是原来定义的名称了。而这个问题在提测关口最容易出现。想解决这个问题必须保证javaBean在打包中不被混淆。
如何不被混淆,不同厂商有不同的解决策略(规范):

  1. 统一放到一个文件夹里(或者含固定名称的文件夹),混淆时ignore 这些文件夹。
    但是这办法操作起来不完美,一个是重构后文件夹容易变名字,还有团队开发时无法保证所有的人都遵守。处理代码时都要绷着文件夹名称这个弦。
  2. 所有JavaBean extends Serializable(统一基类)
    这样proguard文件中保证所有Seriallizable的子类不被混淆即可。而且Bundle传递参数时javaBean可以直接被用。但是这个办法也有一个缺点:需要保证所有人遵守这个约定,无法规范这个步骤。
我们的解决方式:

一般我们的接口管理系统中,都可以产生mock的jsonString,客户端开发会直接利用SGsonFormat插件将jsonString直接转为JavaBean,所以基于GsonFormat功能二次开发,让所有的JavaBean class统一继承Serializable,这样兼顾了易用性和统一性。

GsonFormat的二次开发:

统一继承Serializable的逻辑,应该放入DataWriter写入的流程中,分析可得:在ClassProcessor中process方法,实际上将classContent的String内容通过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 {
            //根据classContent创建class
            String classContent =
                    "public static class " + classEntity.getClassName() + " implements Serializable" + "{}";
            generateClass = factory.createClassFromText(classContent, null).getInnerClasses()[0];
        }

        if (generateClass != null) {
            //递归调用,创建内部类
            for (ClassEntity innerClass : classEntity.getInnerClasss()) {
                generateClass(factory, innerClass, generateClass, visitor);
            }
            if (!Config.getInstant().isSplitGenerate()) {
                generateClass = (PsiClass) parentClass.add(generateClass);
            }
            //创建内部变量
            for (FieldEntity fieldEntity : classEntity.getFields()) {
                generateField(factory, fieldEntity, generateClass, classEntity);
            }
            //创建内部变量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即可使用

总结

二次开发的改动并不大,但是把Studio的plugin开发环境和流程算是熟悉了一遍,plugin插件的开发可以说你会用java就能上手,只不过他自定义的文件类型和组织方式需要熟悉。如果有需要的话,做个新的plugin提高工作效率,是个很好的方式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,138评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,656评论 2 59
  • 原地址:https://ydmmocoo.github.io/2016/06/28/Android-Studio%...
    菜菜编程阅读 589评论 1 3
  • 现在Android的开发者基本上都使用Android Studio进行开发(如果你还在使用eclipse那也行,毕...
    零宽度接合阅读 1,623评论 0 11
  • 因为临时决定出门旅游,又是第一次全家泡温泉,玩水上公园,早饭后迅速开车直奔商场买了泳装和泳裤。小外甥点着柜台里...
    娟1216阅读 437评论 0 0