当前位置 : 主页 > 手机开发 > android >

一键移除ButterKnife并替换为ViewBinding的旧项目拯救

来源:互联网 收集:自由互联 发布时间:2023-03-22
目录 前言 支持的语言与类 思路讲解 研究代码 捋清思路 代码编写 PSI相关知识 文件处理 编写举例 注意事项 使用步骤 前言 众所周知,黄油刀 ButterKnife 已经废弃了,并且已经不再维护
目录
  • 前言
  • 支持的语言与类
  • 思路讲解
    • 研究代码
    • 捋清思路
  • 代码编写
    • PSI相关知识
    • 文件处理
    • 编写举例
    • 注意事项
  • 使用步骤

    前言

    众所周知,黄油刀 ButterKnife 已经废弃了,并且已经不再维护了,而一些老项目估计还有一堆这样的代码,相信大家多多少少都有过被 @BindView 或者 @OnClick 支配的恐惧,而如果想要一个页面一个页面的移除的话,工作量也是非常大的,而这也是笔者写这个插件的原因了(这里不讲解插件开发的相关知识)。

    注:由于每个项目的封装的多样性、以及 layout 布局的初始化有各种各样的写法,还有涉及到一些语法语义的联系,代码无法做到精准转换(后面会举一些例子),所以插件无法做到百分百转换成功,在转换后建议手动检查一下是否出错。

    本文对于没有插件开发以及 PSI 基础的人可能会看不下去,可以直接 github传送门 跳 github 链接并 clone 代码运行,一键完成 ButterKnife 的移除并替换成 ViewBinding 。

    支持的语言与类

    目前仅支持 Java 语言,因为相信如果项目中使用的是 Kotlin ,那肯定首选 KAE 或者 ViewBinding 了(优选 ViewBinding ,如今 KAE 也已经被移除了)。

    该插件中目前对不同的类有不同的转换方式

    • Activity、Fragment、自定义 View 是移除 ButterKnife 并转换成 ViewBinding
    • ViewHolder、Dialog 是移除 ButterKnife 并转换成 findViewById 形式

    由于 Activity 与 Fragment 对于布局的塞入是比较统一的,所以可以做到比较精准的转换为 ViewBinding,而自定义 View 虽然布局的写法也各式各样,但是笔者也尽量修改统一了,而 ViewHolder 与 Dialog 比较复杂,直接修改成 findViewById 比较不容易出错(如果对自己的项目写法的统一很有信心的,也可以按照自己项目的写法试着修改一下代码,都改成 ViewBinding 会更好),毕竟谁也不希望修改后的代码一团糟是吧~

    思路讲解

    研究代码

    首先我们需要研究一下使用了 ButterKnife 的代码是怎么样的,如果是自己使用过该插件的同学肯定是很了解、它的写法的,而对于笔者这种没使用过,但是公司的老项目中 java 的部分全是使用了 ButterKnife 的就很难受了,然后列出我们需要关心的注解。

    • @BindView:用于标记 xml 里的各种属性
    • @OnClick:用于标记 xml 中属性对应的点击事件
    • @OnLongClick:用于标记 xml 中属性对应的长按事件
    • @OnTouch:用于标记 xml 中属性对应的 touch 事件

    这里不做过多讲解,毕竟又不是教大家怎么用 ButterKnife 是吧~

    捋清思路

    上面说到的相关注解是我们需要移除的,我们要针对我们转换的不同方式对这些注解标记的变量与方法做不同的操作。

    • 对于修改成 findViewById 形式的类,我们只需要记录下来该注解以及注解对应的变量或者方法名称,然后新增 initView() 方法用于初始化记录下来的变量,新增 initListener() 方法用于点击事件的编写。
    • 对于修改成 ViewBinding 形式的类,我们不仅需要记录该注解与对应的变量和方法,并且还需要遍历类中的全部代码,在检索到该标记的变量后,需要把这些变量都修改成 mBinding.xxx 的形式,注意:一般大家xml的id命名喜欢用_下划线,但是ViewBinding使用的使用是需要自动改成驼峰式命名的。

    除此之外,我们需要移除的还有 ButterKnife 的 import 语句、绑定语句 bind()、以及解绑语句 unbind()。我们需要增加的有:layout 对应的 ViewBinding 类的初始化语句、import 语句。

    了解完这些我们就可以开始写插件啦~

    代码编写

    对于代码的编写笔者这里也会分几个步骤去阐述:分别是 PSI 相关知识、文件处理、编写举例、注意事项。

    PSI相关知识

    PSI 的全称是 Program Structure Interface(程序结构接口),我们要分析代码以及修改代码的话,是离不开 PSI 的,文档传送门

    一个 Class 文件结构分别包含字段表、属性表、方法表等,每个字段、方法也都有属性表,但在 PSI 中,总体上只有 PsiFilePsiElement

    • PsiFile 是一个接口,如果文件是一个 java 文件,那么解析生成的 PsiFile 就是 PsiJavaFile 对象,如果是一个 Xml 文件,则解析后生成的是 XmlFile 对象
    • 而对应 Java 文件的 PsiElement 种类有:PsiClass、PsiField、PsiMethod、PsiCodeBlock、PsiStatement、PsiMethodCallExpression 等等

    其中,PsiJavaFile、PsiClass、PsiField、PsiMethod、PsiStatement 是我们本文涉及到的,大家可以先去看看文档了解一下。

    文件处理

    我们在选择多级目录的时候,会有很多的文件,而我们需要在这些文件中筛选出 java 文件,以及筛选出 import 语句中含有 butterknife 的,因为如果该类使用了 ButterKnife ,则肯定需要 import 相关的类。

    筛选 java 文件的这部分代码在这里就不贴出来了,很简单的,大家可以直接去看代码就好。

    判断该类是否需要进行 ButterKnife 移除处理:

    /**
     * 检查是否有import butterknife相关,若没有引入butterknife,则不需要操作
     */
    private fun checkIsNeedModify(): Boolean {
        val importStatement = psiJavaFile.importList?.importStatements?.find {
            it.qualifiedName?.lowercase(Locale.getDefault())?.contains("butterknife") == true
        }
        return importStatement != null
    }
    

    在这里需要先来一些前置知识,我们的插件在获取文件的的时候,拿到的是 VirtualFile,当该文件是java文件时,VirtualFile 可以通过 PSI 提供的api转换成 PsiJavaFile,然后我们可以通过 PsiFile 拿到 PsiClass,其中,importList 是属于 PsiFile 的,而上面说到那些 PsiElement 都是属于 PsiClass 的。

    下面贴一下这部分代码:

    private fun handle(vFile: VirtualFile) {
        if (vFile.isDirectory) {
            handleDirectory(vFile)
        } else {
            // 判断是否是java类型
            if (vFile.fileType is JavaFileType) {
                // 转换成psiFile
                val psiFile = PsiManager.getInstance(project!!).findFile(vFile)
                // 转换成psiClass
                val psiClass = PsiTreeUtil.findChildOfAnyType(psiFile, PsiClass::class.java)
                handleSingleVirtualFile(vFile, psiFile, psiClass)
            }
        }
    }
    

    这里只需要了解的就是添加了注释的那几行代码。

    编写举例

    我们需要对 PsiClass 进行分类,这里目前是只能按照大部分人对类的命名习惯来进行分析,如果有一些特殊的命名习惯的人,可以把代码 clone 下来自行修改一下再运行。

    private fun checkClassType(psiClass: PsiClass) {
        val superType = psiClass.superClassType.toString()
        if (superType.contains("Activity")) {
            ActivityCodeParser(project, vFile, psiJavaFile, psiClass).execute()
        } else if (superType.contains("Fragment")) {
            FragmentCodeParser(project, vFile, psiJavaFile, psiClass).execute()
        } else if (superType.contains("ViewHolder") || superType.contains("Adapter<ViewHolder>")) {
            AdapterCodeParser(project, psiJavaFile, psiClass).execute()
        } else if (superType.contains("Adapter")) {
            // 这里的判断是为了不做处理,因为adapter的xml属性是在viewHolder中初始化的
        } else if (superType.contains("Dialog")) {
            DialogCodeParser(project, psiJavaFile, psiClass).execute()
        } else { 
            // 自定义View
            CustomViewCodeParser(project, vFile, psiJavaFile, psiClass).execute()
        }
    }
    

    我们通过拿到 PsiClass 继承的父类的类型来进行判断,这里的不足是代码中只拿了当前类的上一级继承的父类的类型,并没有去判断父类是否还有父类,因为笔者认为只要命名规范,这就不是什么大问题。举个例子,如果有人喜欢封装一个名为 BaseFragment 的实则是一个 Activity 的基类,然后由 MainActivity 去继承,那这个插件就不适用了

    上一篇:Android动态使用VectorDrawable过程详解
    下一篇:没有了
    网友评论