IntelliJ IDEA/Android Studio插件开发指南

前言

目前在为安卓手机QQ做自动化的相关工作,包括UI自动化,逻辑层自动化等。使用到的uiautomator等框架,需要在Android Studio进行编码工作。
其中很多工作如果做到插件化的话,可以有效地节省时间成本,提升大家的自动化效率。
比如运行自动化的时候,需要用到我们自定义的shell命令。我们可以通过插件来实现一键运行。
在运行adb shell am instrument命令的时候,需要编译出test APKtarget APK。手Q整体的git仓库很大,编译耗时很久。我们想着通过一些方法来优化这个耗时。其中一个步骤就是,把我们代码目录下的变更,同步到一个编译目录下。
这个小功能的最合适的形态,自然就是Android Studio上的一个插件。点击一个按钮,一键同步,那可真是在米奇妙妙屋吃妙脆角——妙到家了!
Android Studio是基于Intellij IDEA开发的,所以开发Android Studio的插件,其实就是开发IDEA的插件。
根据官方推荐,使用IDEA IDE来开发IDEA插件。

插件开发的基本流程

1. 环境配置

1.1 安装PDK

正如Java开发需要安装Java DevKit,IDEA插件开发也需要安装Plugin DevKit。PDK的作用是为插件提供IDEA内建支持以及相关库函数。
打开Intellij IDEA --> Preferences --> Plugins,如果没有安装,可以在marketplace里面搜索,并安装。
PDK

1.2 配置插件开发SDK

配置开发 IntelliJ 平台插件的SDK也就是IntelliJ Platform Plugin SDK,基于 JDK 之上运行,类似于开发 Android 应用需要 Android SDK。
切换到 File --> Project Structure,选择左侧栏 Platform Settings 下的 SDKs,点击+按钮,先选择 Add JDK,指定 JDK 的路径;再选择Add IntelliJ Platform Plugin SDK,指定上面添加的JDK为插件需要的JDK。需要注意的是,从IDEA 2020.3开始,不能再使用Java1.8版本。因为IDEA 2020.3版本基于Java11构建,所以如果想要开发2020.3及以后版本的IDEA的插件,需要选择Java11版本。
配置插件SDK

2. 新建插件工程

File --> New --> Project,在弹出的窗口中选择Gradle,然后选择Java(这表明我们使用Java语言开发)和Intellij Platform Plugin,点击Next,然后设置项目的名称和位置,点击Finish完成创建。

3. Action

我们在IntelliJ自定义的插件可以添加到菜单项目(如右键菜单中)或者是放在工具栏中。当用户点击时触发一个动作事件,IntelliJ则会回调AnAction子类的actionPerformed函数。因此我们只需重写actionPerformed函数即可。我们可以认为Action是插件的触发入口。我们可以直接右键New --> Plugin DevKit --> Action新建action,这个action是AnAction的子类。

在接下来的弹出窗口中,我们可以创建一个Action。

  • Action ID:这个action的唯一标识
  • Class Name:action的类名
  • Name:action的名称
  • Description: action的描述信息
  • Groups:这个标签指定我们自定义的插件应该放入到哪个菜单下面。 在IntelliJ IDEA菜单栏中有很多菜单如File、Edit、View、Navigate、Code、……、Help等。他们的ID一般是菜单名+Menu的方式。比如,我们想将我们自定义的插件放到Help菜单中,作为Help菜单的子选项。那么在Groups中就指定HelpMenu。左侧的Anchor属性用于描述位置,主要有四个选项:first、last、before、after。他们的含义如下:

first:放在最前面
last:放在最后
before:放在relative-to-action属性指定的ID的前面
after:放在relative-to-action属性指定的ID的后面

  • Keyboard Shortcuts:可以为这个action指定快捷键

public class TestAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        NotificationGroup notificationGroup = new NotificationGroup("testid", NotificationDisplayType.BALLOON, false);
        /**
         * desc: 这是一个IDEA的通知,会通知到idea右下角的悬浮小窗
         * content :  通知内容
         * type  :通知的类型,warning,info,error
         */
        Notification notification = notificationGroup.createNotification("测试通知", MessageType.INFO);
        Notifications.Bus.notify(notification);
    }
}

创建完之后,我们也可以在src/resources/META-INF/plugin.xml中,看到我们之前写入的action信息,如果想要修改,可以在这个配置文件中直接修改。

    <actions>
        <!-- Add your actions here -->
        <action id="testId" class="com.example.yuguo.TestAction" text="通知" description="测试通知的功能">
            <add-to-group group-id="ToolsMenu" anchor="first"/>
        </action>
    </actions>

4. 配置描述

src/resources/META-INF/plugin.xml是整个插件的配置文件,里面定义了插件的名称,描述信息,支持的IDEA版本号,作者信息,action的相关信息等。

<idea-plugin>
    <!--插件的id,属于全局唯一标识-->
    <id>plugin.test</id>
    <!--插件的名称-->
    <name>PluginTest</name>
    <vendor email="[email protected]" url="">author_name</vendor>

    <!--插件的描述信息,支持html-->
    <description><![CDATA[
    Plugin Test<br>
    <em>v1.0</em>
    ]]></description>
    
    <extensions defaultExtensionNs="com.intellij">
        <!-- Add your extensions here -->
    </extensions>

    <actions>
        <!-- 这里是刚刚定义的插件信息 -->
        <action id="testId" class="com.example.yuguo.TestAction" text="通知" description="测试通知的功能">
            <add-to-group group-id="ToolsMenu" anchor="first"/>
        </action>
    </actions>
</idea-plugin>

5. 调试、打包

调试

等到配置完成后,在IDEA右侧的Gradle一栏中,有Intellij的集合。点击里面的runIde,可以打开一个沙盒,里面运行包含着该插件的IDEA实例。也可以右键选择debug模式运行。

打包

点击上图的buildPlugin,就可以在build/distributions/目录下面生成插件zip包,这个包就是我们需要的最终产物。在IDEA设置Preferences --> Plugins,点击installed旁边的设置按钮,选择Install Plugin from Disk,然后选择这个zip,就可以安装到IDEA中了。

插件的组件

GUI

ToolWindow

工具视窗(ToolWindow)的功能主要是进行信息的显示,同时用户还可以直接在toolwindow中进行操作调用工具,比如IDE下方默认的terminal、Git等。作为IDE侧边栏中较大的一部分,toolwindow与用户的交互在整个ui中非常重要。

实现toolwindow主要分为两步,第一步创建类实现ToolWindowFactory接口,编写需求的toolWindowFactory实例,第二步在plugin.xml中注册该ToolWindow。

当用户单击工具窗口按钮时,将调用工厂类的方法createToolWindowContent(),并初始化工具窗口的UI。此过程可确保未使用的工具窗口不会在启动时间或内存使用上造成任何开销:如果用户不与插件的工具窗口进行交互,则不会加载或执行任何插件代码。

public class ToolFactoryCompute implements ToolWindowFactory {

    private ToolWindow myToolWindow;
    private JPanel myPanel;
    private JTextArea textContent;
    private JScrollPane myScrollPane;

    /**
     * @param project 项目
     * @param toolWindow 窗口
     */
    @Override
    public void createToolWindowContent(@NotNull Project project, 
                                        @NotNull ToolWindow toolWindow) {
        myToolWindow = toolWindow;

        // 将显示面板添加到显示区 
        ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
        Content content = contentFactory.createContent(mPanel, "Control", false);
        toolWindow.getContentManager().addContent(content);
    }

}

在plugin.xml中注册toolwindow。

<extensions defaultExtensionNs="com.intellij">
  <!-- canCloseContents表示是否可以关闭这个toolwindow, anchor表示toolwindow的位置, id是toolwindow的名字, factoryClass表示toolwindow的工厂类 -->
  <toolWindow canCloseContents="false" anchor="bottom"
              id="Compute Code Lines" icon="/myToolWindow/test.png"
              factoryClass="tools.ToolFactoryCompute">
  </toolWindow>
  
</extensions>

Dialog

会话框(Dialog)可以与用户交互,获取用户自定义输入的内容,也可以作为提示弹窗,告诉用户信息。会话框的实现需要定义一个继承了IDEA的DialogWrapper抽象类的子类,这个子类就是自定义的会话框实现,所有的样式定义、功能触发都是放到这个子类里的,比如以下实现:

public class FormTestDialog extends DialogWrapper {
 
    private String projectName; //假如需要获取到项目名,作为该类的属性放进来
 
    // DialogWrapper没有默认的无参构造方法,所以需要重写构造方法,它提供了很多重载构造方法,
    // 这里使用传project类型参数的构造方法,通过Project对象可以获取当前IDEA内打开的项目的一些属性,
    // 比如项目名,项目路径等
    public FormTestDialog(@Nullable Project project) {
        super(project);
        setTitle("表单测试"); // 设置会话框标题
        this.projectName = project.getName();
    }
 
    // 重写下面的方法,返回一个自定义的swing样式,该样式会展示在会话框的最上方的位置
    @Override
    protected JComponent createNorthPanel() {
        return null;
    }
 
    // 重写下面的方法,返回一个自定义的swing样式,该样式会展示在会话框的最下方的位置
    @Override
    protected JComponent createSouthPanel() {
        return null;
    }
 
    // 重写下面的方法,返回一个自定义的swing样式,该样式会展示在会话框的中央位置
    @Override
    protected JComponent createCenterPanel() {
        return null;
    }
}

业务实践

获取文件差异

方案一:自建Diff工具

为了获得代码目录与编译目录的文件差异,必然要使用到Diff工具,这其中涉及到很多自定义的规则,比如差异文件是否要忽略等。优点是可以完全自定义灵活的识别差异的规则。缺点是耗时较久,毕竟要编写一套Diff系统。时间比较紧,所以这个方案pass了。

方案二:使用JGit

JGit是Java编写的一套Git工具,通过Java代码就可以调用到Git的所有指令,可以完美解决获得文件差异的需求。但是经过实际测试发现,在调用git.status.call()方法时 ,由于它需要初始化Git,包括建立diff,filetree等操作,对于大仓库,一次运行就要十几秒,不能接受,故放弃。

Git git = Git.open(new File("~/source-code.temp-1/git"));
    Status status = git.status().call();        //返回的值都是相对工作区的路径,而不是绝对路径
    status.getAdded().forEach(it -> System.out.println("Add File :" + it));      //git add命令后会看到变化
    status.getRemoved().forEach(it -> System.out.println("Remove File :" + it));  ///git rm命令会看到变化,从暂存区删除的文件列表
    status.getModified().forEach(it -> System.out.println("Modified File :" + it));  //修改的文件列表
    status.getUntracked().forEach(it -> System.out.println("Untracked File :" + it)); //工作区新增的文件列表
    status.getConflicting().forEach(it -> System.out.println("Conflicting File :" + it)); //冲突的文件列表
    status.getMissing().forEach(it -> System.out.println("Missing File :" + it));    //工作区删除的文件列表

方案三:利用内存Git

经过方案二,我们发现git是符合我们要求的,但是因为JGit要初始化,所以耗时较久。但是我们在运行IDEA的时候,在终端使用git status非常快,是毫秒级,那我们完全可以利用内存中的git,直接执行git status命令,在返回结果中去匹配文件差异。
通过让Java执行git命令,可以达到毫秒级相应。

Java执行shell命令并返回执行结果

 /**
     * 执行shellCommand命令,获取命令的返回结果。在返回结果中,把符合条件的文件名放置到文件集合中
     *
     * @param cmd shell命令
     * @return 命令的输出结果
     */
    public static String executeCommand(String[] cmd) throws IOException {
        String resultStr = "";
        // 利用runtime去执行shell命令
        Process ps = Runtime.getRuntime().exec(cmd);
        // 获取process对象的正常流和异常流
        try (BufferedReader brInfo = new BufferedReader(new InputStreamReader(ps.getInputStream()));
             BufferedReader brError = new BufferedReader(new InputStreamReader(ps.getErrorStream()))) {
            StringBuilder stringBuffer = new StringBuilder();
            String line;
            // 读取输出结果,按照每行读取
            if (brInfo.readLine() != null) {
                while ((line = brInfo.readLine()) != null) {
                    stringBuffer.append(line).append("\n");
                    // 处理文件差异
                    filterFiles(line);
                }
            } else {
                // 如果正常输出流为null,那么就获取异常流
                while ((line = brError.readLine()) != null) {
                    stringBuffer.append(line).append("\n");
                }
            }
            // 等待shell命令执行完成
            ps.waitFor();
            resultStr = stringBuffer.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // shell命令的返回结果
        return resultStr;
    }
    
    // 在main函数中测试
     public static void main(String[] args) {
        String cmd = "git status";
        String resultStr = executeCommand(new String[]{"/bin/sh", "-c", cmd});
        System.out.println(resultStr);
    }
    

参考

//blog.csdn.net/huachao1001/article/details/53856916

//blog.csdn.net/huachao1001/article/details/53883500

//plugins.jetbrains.com/docs/intellij/welcome.html?from=jetbrains.org