首頁 > 軟體

一鍵移除ButterKnife並替換為ViewBinding的舊專案拯救

2023-02-03 18:03:25

前言

眾所周知,黃油刀 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 去繼承,那這個外掛就不適用了


IT145.com E-mail:sddin#qq.com