自動規避程式碼陷阱——自定義Lint規則
阿新 • • 發佈:2019-02-04
一、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 classLogDetector的作用是檢查程式碼中是否使用Log類,建議使用封裝過的"LogUtils"類。 其中程式碼的意義和功能我們稍後再細說,目前只需要知道繼承Detector來實現一個規則就可以了。 然後我們還需要另外一個類,繼承IssueRegistry,這個類的作用是將定義規則註冊上,程式碼很簡單,如下: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`"); } } }
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中引入lintaardependencies { 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型別
@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:問題描述