1. 程式人生 > >自動規避程式碼陷阱——自定義Lint規則

自動規避程式碼陷阱——自定義Lint規則

一、Lint是什麼?

Lint 是一款靜態程式碼分析工具,能檢查安卓專案的原始檔,從而查詢潛在的程式錯誤以及優化提升的方案。 當你忘記在Toast上呼叫show()時,Lint 就會提醒你。它也會確保你的ImageView中添加了contentDescription,以支援可用性。類似的例子還有成千上萬個。誠然,Lint 能在諸多方面提供幫助,包括:正確性,安全,效能,易用性,可用性,國際化等等。 這是引用網上的一段描述,簡單來說lint可以對程式碼進行檢查分析,查詢各類潛在問題。

二、Lint的使用

在Android Studio中選擇Analyze -> Inspect Code

然後在彈出窗中選擇Whole project,點選確定開始檢查
檢查結束就可以在下面看到結果了,如下圖:
關於lint的使用不是本文的重點,這裡只是簡單介紹一下。

三、為什麼要使用自定義Lint規則?

由於專案的架構,有時候專案中會有一些非正式的程式碼規則,比如不使用系統自帶的日誌工具Log而使用第三方或二次封裝過的工具類。這種情況預設的lint就無法檢查了,這時候自定義lint規則就派上用場了。 自定義lint規則可以幫助團隊規避一些因架構、業務、歷史等原因出現的程式碼陷阱,避免一些問題頻繁重複的產生,同時可以讓團隊新成員快速被動的瞭解一些開發規則。 下面開始一步步介紹如何自定義lint規則。

四、新建module

想要使用自定義lint規則,一種做法是將定義規則的程式碼打成jar包,然後放在“%UserHome%/.android/lint/”目錄下。 這種做法有兩個缺點:一是對所有的專案都產生作用,無法實現不同專案使用不同規則;二是需要每個人都下載並拷貝到目錄下。 另外一種做法將定義的規則打包成aar的形式,依賴到專案中。 這種做法需要建立兩個module,一個java-lib,一個android-lib,如下圖:
在lintjar中編寫規則程式碼,而lintaar沒有任何程式碼,它的作用是將lintjar的jar包打包成aar以便引用。

五、在lintjar中定義規則

在lintjar中新建一個類,繼承Detector,實現一個規則,如下:
public class 
LogDetector extends Detector implements Detector.ClassScanner { public static final Issue ISSUE = Issue.create("LogUtilsNotUsed", "You must use our `LogUtils`", "Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.", Category.MESSAGES, 9, Severity.ERROR, new Implementation(LogDetector.class, Scope.CLASS_FILE_SCOPE)); @Override public List<String> getApplicableCallNames() { return Arrays.asList("v", "d", "i", "w", "e", "wtf"); } @Override public List<String> getApplicableMethodNames() { return Arrays.asList("v", "d", "i", "w", "e", "wtf"); } @Override public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, @NonNull MethodNode method, @NonNull MethodInsnNode call) { String owner = call.owner; if (owner.startsWith("android/util/Log")) { context.report(ISSUE, method, call, context.getLocation(call), "You must use our `LogUtils`"); } } }
LogDetector的作用是檢查程式碼中是否使用Log類,建議使用封裝過的"LogUtils"類。 其中程式碼的意義和功能我們稍後再細說,目前只需要知道繼承Detector來實現一個規則就可以了。 然後我們還需要另外一個類,繼承IssueRegistry,這個類的作用是將定義規則註冊上,程式碼很簡單,如下:
public class LintRegistry extends IssueRegistry {
    @Override
public List<Issue> getIssues() {
        return Arrays.asList(InitCallDetector.ISSUE, LogDetector.ISSUE);
}
}
這樣還沒有完成註冊,要完成註冊我們還需要在gradle中進行配置。 

六、配置lintjar中gradle

在lintjar的gradle中引入lint的兩個庫
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.tools.lint:lint-api:24.3.1'
compile 'com.android.tools.lint:lint-checks:24.3.1'
}
然後,註冊我們之前定義好的Registry類 /**
 * Lint-Registry是lint的註冊類
 */
jar {
    manifest {
        attributes("Lint-Registry""com.bennu.lintjar.LintRegistry")
    }
} 最後定義一個打包方法,這個會在lintaar中使用
//定義lintJarOutput方法,在lintaar中被呼叫
configurations {
    lintJarOutput
}

dependencies {
    //lintJarOutput方法,打jar包
lintJarOutput files(jar)
}
這樣lintJar這個module就可以了,下面開始配置lintAar這個module。

七、配置LintAar的gradle

LintAar這個module中不需要寫任何程式碼,它的作用是將lintJar生成的jar包再打包成aar即可。 在LintAar的gradle中新增如下:
// 定義lintJarImport方法,在copyLintJar任務中被呼叫
configurations {
    lintJarImport
}

dependencies {
    // 呼叫lintjar的lintJarOutput方法,獲得jar包
lintJarImport project(path: ':lintjar', configuration: 'lintJarOutput')
}

// 呼叫lintJarImport得到jar包,拷貝到指定目錄
task copyLintJar(type: Copy) {
    from (configurations.lintJarImport) {
        rename {
            String fileName ->
                'lint.jar'
}
    }
    into 'build/intermediates/lint/'
}

// 當專案執行到prepareLintJar這一步時執行copyLintJar方法(注意:這個時機需要根據專案具體情況改變)
project.afterEvaluate {
    def compileLintTask = project.tasks.find{ it.name == 'prepareLintJar'}
    compileLintTask.dependsOn(copyLintJar)
}
定義一個lintJarImport方法,這個方法會呼叫lintJar中的lintJarOutput方法得到jar包。 新建一個copyLintJar的任務task,目的是將前面得到的jar包拷貝到指定的目錄。 最後在afterEvaluate中判斷當執行了‘prepareLintJar’這個task時執行copyLintJar這個任務。 注意:‘prepareLintJar’是基於我自己的環境判斷出來的,在不同的gradle版本上可能有所不同,請根據實際情況修改copyLintJar的執行時機。 這樣lintjar和lintaar這兩個module都完成了,下一步將它們依賴進專案。

八、在專案中引入規則

在專案的gradle中引入lintaar
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation project(':lintaar')
}
同時新增如下配置
android {
    
    ...
    
    lintOptions {
        textReport true // 輸出lint報告
textOutput 'stdout'
abortOnError false // 遇到錯誤不停止
}
}
然後,我們在程式碼中隨便寫個Log程式碼,如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e("test", "use log.e");
init();
}
接下來就可以測試自定義的規則是否生效了。

九、執行lint

點開gradle視窗,在專案(如:app)下找到lint的相關task,雙擊執行即可,如下
執行時就可以中Message視窗中看到相關資訊,如下
可以看到,我們定義的規則已經使用了,找到了一處使用Log類的程式碼。

十、如何定義規則

上面我們實現了規則併成功使用了,但是對於規則的定義,即Detector類一筆帶過,這個其實才是重點,下面我們以LogDetector為例詳細說說如何定義自己的規則。 (1)首先建立一個Issue物件,如下:
public static final Issue ISSUE = Issue.create("LogUtilsNotUsed",
"You must use our `LogUtils`",
"Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.",
Category.MESSAGES,
9,
Severity.ERROR,
        new Implementation(LogDetector.class,
Scope.CLASS_FILE_SCOPE));
Issue的create函式有七個引數:
  • id:問題的id
  • briefDescription:問題的簡單描述
  • explanation:問題的解釋,即如何解決問題
  • category:問題的型別,具體間Category類
  • priority:問題的重要程度,從1到10,10是最重要
  • severity:問題的嚴重性,有ERROR、WARNING等
  • implementation:問題的實現,Implementation型別
Implementation類的建構函式中第一個引數是定義問題的類;第二個引數是檔案範圍,即在什麼型別的檔案中掃描這個問題。 (2)然後通過重寫必要的函式來實現一定的查詢規則,程式碼如下:
@Override
public List<String> getApplicableCallNames() {
    return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}

@Override
public List<String> getApplicableMethodNames() {
    return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
重寫了Detector的兩個函式,來查詢呼叫的方法名是“v”、“d”等的那部分程式碼。 Detector有很多Scanner,每個Scanner又有不少函式,這裡就不一個個來說了。具體需要使用那個Scanner的哪些函式,需要大家根據自己的情況,結合Detector原始碼中每個函式的說明來自己判斷。這部分網上的資料不多,後續的文章中,我可能會就幾個例子講解一些函式的使用。 在上面的程式碼中用來兩個函式getApplicableCallNames和getApplicableMethodNames。其中getApplicableCallNames是ClassScanner的函式,而getApplicableMethodNames是JavaScanner的函式,兩個函式作用是一樣的,這兩個函式會返回一個字串列表,檢查時當發現方法呼叫而且呼叫的方法名在列表中時,就會觸發check。 (3)最後重寫check函式實現問題邏輯,程式碼如下:
@Override
public void checkCall(@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull MethodInsnNode call) {
    String owner = call.owner;
    if (owner.startsWith("android/util/Log")) {
        context.report(ISSUE,
method,
call,
context.getLocation(call),
"You must use our `LogUtils`");
}
}
檢查每次函式(已過濾)呼叫,當呼叫主體是Log類時,使用ClassContext的report函式上報一個問題。 report函式有5個引數:
  • issue:上面定義的ISSUE
  • method:MethodNode型別
  • instruction:MethodInsnNode型別
  • location:問題的位置
  • message:問題描述
通過上面這個簡單的事例,一個問題規則的定義基本上就是通過上面三步來完成。

十一、總結

本篇文章主要是講解一下如何在專案中完成一個自定義lint的引入,並且通過一個簡單的例子講解如何建立一個簡單的規則,並且執行檢視結果。