Lint增量掃描

前言

先來講我爲何要作增量掃描這個事情,畢竟代碼掃描已經老生常談了,業界方案一搜一大堆,有什麼好講的,大部人看到這篇文章的時候確定這麼想吧,可是注意今天我要分享的不是全量掃描,我分享的是從無到有實現增量掃描的過程,有的時候實現一個方案歷來不是重點,咱們對於方案的認知程度纔是咱們本身最重要的收穫 ̄▽ ̄ 。html

再來講說怎麼樣的代碼掃描纔算是高效的,我是這麼理解的:java

不能增量檢查的代碼掃描都是耍流氓,之前的代碼一大堆問題,誰有耐心所有去解決
不能自動化的代碼掃描都是欺騙咱們感情,不是每一個人都有良好的意識每次都去檢查的
不能撤銷提交的代碼掃描都是本身騙本身,檢查出來問題不改,這樣的代碼掃描要來何用
不能持續集成的代碼掃描都是不專業的,問題要快上線了才發現,這樣的代碼掃描風險多高
開發缺的歷來就不是工具,咱們缺的是無縫嵌入的自動化流程、自我Code Review的意識,意識比工具重要。android

這裏扯了一些大道理,你們諒解,口號喊得響,你們纔有興趣看嘛。後面全是乾貨,你們放心,嘿嘿。git

方案介紹

OkLint做爲一個Gradle插件,使用起來超簡單,他能在你提交時發現增量問題,撤銷提交併給你發郵件。github

在根目錄下的build.gradle寫web

allprojects {
    apply plugin: 'oklint'
}複製代碼

方案思考

在講具體實現以前,先來說講我對於高效的代碼掃描是怎麼想的。shell

高效的代碼掃描我以爲有五個方案:apache

  1. 方案一是Android Studio自帶的錯誤提示功能,他有個好處就是實時發現問題,缺點就是有些問題隱藏在花花綠綠的代碼裏,你要指定你想檢查的問題爲error才能暴露出來,這樣就須要在每臺電腦上都改動一下,太麻煩了。api

  2. 方案二是Android Studio的增量代碼掃描功能,缺點就是不能自動化,不能在團隊內很好落實,不利於統計問題和持續集成。
    android-studio

    image.png
    image.png

  3. 方案三是用Sonar持續集成,可是他有個問題是不能增量,咱們團隊用過,最後由於之前問題太多根本推行不起來,相信好多團隊都是這樣吧。

  4. 方案四是用Android Gradle插件2.3.0之後提供的新功能 baseline,他也是全量掃描,可是他能增量顯示問題,這個方案後期和Sonar持續集成,能夠做爲Plan B。

  5. 方案五是我如今用的方案,增量代碼掃描和git hooks搭配使用,味道更好。剛開始的思路是在git commit以前掃描增量代碼,結果發現lint掃描比較慢(我嘗試改了,改了之後確實快了可是有些問題就掃描不到了,畢竟掃描代碼仍是須要整個項目的代碼才能更好的找到問題)。後面我聽取了同事峯哥的意見,採用另一個思路,偷偷在git commit以後去掃描。有些人要問了爲何不在gitlab上的webhook裏面執行,嗯你很機智,這樣實現也有很大的優勢,可是我更想及時檢查每一次改動,越早發現越好解決問題。

我的以爲上面五個方案,方案四和方案五左右開弓效果更好。方案五負責及時檢查每一次改動,方案四負責發現全量代碼潛在的問題。

方案對比

方案作出來了,要是不對比一下,就沒辦法愉快地吹NB了。

功能 OkLint Lint命令行 Android Gradle 插件 Android Studio
增量 能夠 不行 不行,2.3.0後支持全量掃描增量顯示 能夠
自動化 能夠 不行 不行 不行
持續集成 能夠 不行 不行 不行
代碼回滾 能夠 不行 不行 能夠
只掃描優先級高的問題 能夠 配置麻煩 配置麻煩 配置麻煩

Android有本身的Code Lint,可是他只能全量掃描,並且無法只掃描優先級高的。當然Android Studio能夠在提交前面執行code analysis,可是做爲一個團隊你很難落實讓每一個人每次提交代碼都去執行,就算執行了你也不能保證他必定去改正這個問題,就算他改了這個問題,你也不能保證多個分支合併的代碼沒有問題,因此一個能自動在git commit時掃描增量代碼的工具仍是頗有必要的。

方案實現


思路其實很簡單的,流程很簡單

gradle插件copy git hooks------> git hooks自動執行增量掃描的任務------> git diff找到增量代碼------> lint-api.jar調用project.addfile() 掃描增量代碼------>javamail發送問題郵件------>git reset回滾代碼

好了如今你已經獲得個人大乘佛法了,你能夠屁顛屁顛地回大唐娶妻生子走向人生巔峯了,我保證我不阻止你。

找到增量代碼

這個命令感謝個人另外一個同事馬老闆,他坐爲旁邊,我每次急躁的時候他都耐心幫我找答案。

private List<String> getPostCommitChange() {
        ArrayList<String> filterList = new ArrayList<String>()
        try {
            String projectDir = getProject().getProjectDir()
            String commond = "git diff --name-only --diff-filter=ACMRTUXB HEAD~1 HEAD~0 $projectDir"
            String changeInfo = commond.execute(null, project.getRootDir()).text.trim()
            if (changeInfo == null || changeInfo.empty) {
                return filterList
            }
            String[] lines = changeInfo.split("\\n")
            return lines.toList()
        } catch (Exception e) {
            return filterList
        }
    }複製代碼

用git diff命令找到剛提交的commit都改動了哪些文件,我講一下他的每一個參數的意思

  • git diff 比較兩個commit
  • HEAD~1是前一個commit,HEAD~0是當前的commit,有個注意點HEAD~1 HEAD~0 的前後順序,剛開始寫反了,增長的文件變成了刪除的文件
  • diff-filter是篩選文件類型,沒寫D用來去除刪除的文件
  • name-only用來只列出文件名
  • projectDir必定要寫,否則git不知道要找哪一個項目,並且注意我這裏寫的是當前module dir,確保每一個module只檢查本身的改動,用來加快掃描速度和防止掃描出來重複的問題。

這裏着重說一下在gralde裏寫命令的一個注意點,要執行帶有單引號的命令會執行爲空的問題
譬如

git status  -s  | grep -v '^D'//列出當前要提交的commit變更了哪些文件並排除刪除的文件複製代碼

你覺得"git status -s | grep -v '^D'".execute就好了嗎,太天真了,執行結果爲空,剛開始我覺得只要加上轉義符就行,結果仍是不行。後面反覆實驗發現要這麼寫

["/bin/bash", "-c", "git status -s | grep -v '^D'"].execute()複製代碼

增量代碼掃描具體實現

原理比較長,怕你們看的似懂非懂,我先給結果,這樣比較好。看到一些不明白的名詞能夠先忽略掉,後面原理裏面會提,我儘可能講的淺顯易懂。
我寫了一個增量掃描的task,而後寫了一個LintClinet,這個LintClient會掃描代碼,它繼承android gradle的LintGradleClient,task會調用這個client的run方法,run方法就是掃描方法。
而增量掃描的關鍵性代碼是修改LintGradleClientcreateLintRequest方法,往project加入要掃描的文件

@Override
    protected LintRequest createLintRequest(@NonNull List<File> files) {
//注意這個project是com.android.tools.lint.detector.api.project
  LintRequest lintRequest = super.createLintRequest(files);
        for (Project project : lintRequest.getProjects()) {
                 project.addFile(changefile);//加入要掃描的文件
                addChangeFiles(project);
        }
     return lintRequest;
    }複製代碼

有個注意點我要提一下
LintGradleClient構造函數須要參數,除了variant能夠爲空,其餘都不能爲空。由於不在android gradle插件內部,因此有些參數獲取須要動一些腦筋。

LintGradleClient(
             IssueRegistry registry,//掃描規則
            LintCliFlags flags,
            org.gradle.api.Project gradleProject,//gradle 項目
            AndroidProject modelProject,// android項目
            File sdkHome,// android sdk目錄
            Variant variant,//編譯的Variant
            BuildToolInfo buildToolInfo) {//編譯工具包複製代碼

篇幅有限,參數講太多反而把你們搞糊塗,我就講一個參數,如何獲取AndroidProject

private AndroidProject getAndroidProject() {
        GradleConnector gradleConn = GradleConnector.newConnector()
        gradleConn.forProjectDirectory(getProject().getProjectDir())
        AndroidProject modelProject = gradleConn.connect().getModel(AndroidProject.class)
        return modelProject
    }複製代碼

增量代碼掃描原理分析

剛開始想的很簡單呀,命令行 Lint不是也能掃描代碼嗎,那裏面確定有指定掃描文件和目錄的參數吧,別說還真有, --sources <dir> ,結果一試,發現是有結果,可是掃描出來的問題根本不是那個文件的問題呀,而後我同事說在他電腦卻提示不能掃描gradle項目,一會兒就矇蔽了,無從下手的感受,剛開始我覺得命令沒用對,可是改來改去都不對,後面我嘗試去除裏面的gradle project判斷限制,而後指定掃描文件,仍是掃描不出該有的問題,我就先暫停這個方案的研究。

既然上面這條路走不通,我就去找android studio的源碼看他是怎麼實現增量掃描的,結果在Android Studio源碼裏面,搜索lint根本沒有找到任何相關的代碼,後面發現實際上是在另外的Plugin源碼裏。不過他依賴於Intellij Module,Module會找到每一個類,那我又沒有Module這個上下文,這麼說這個方案仍是走不通。

那就再換一個思路,Android Gradle插件不是也能夠實現Lint掃描,那我改一改不就能夠增量掃描,結果一拿到他的代碼就感受無從下手,改來改去都不對呀,不知道哪一行代碼能夠實現增量掃描,就算後面完成了增量掃碼,掃描也很慢。

帶着上面的幾個坑,我研究了Lint內部的實現原理找到了增量代碼掃描的實現方法

  1. 爲何命令行Lint 掃描不出增量代碼的問題
  2. android studio是怎麼實現lint增量掃描的

我先講一下關於Lint的預備知識,而後再來說上面幾個問題,方便你們更好理解

###Lint掃描內部原理

其實不管是Lint命令行、android gradle插件、android studio都依賴了兩個jar

  • lint-api.jar:lint-api是代碼掃描的具體實現
  • lint-check.jar:lint-check是默認的掃描規則

    lint-api.jar內部實現原理:
    LintDriver調用analyze()分析LintRequest中的文件------>checkProject----->runFileDetectors----->check對應文件的Visitor,譬如JavaPsiVisitor分析java文件,AsmVisitor分析class文件等

下面講講三種方式分別怎麼實現的

Lint命令行:
lint.sh------>lint.jar------>LintCliClient 的run(IssueRegistry registry, List<File> files)------>LintDriver analyze分析 project

Lint Gradle Task:
Lint.groovy------>LintGradleClient的run(IssueRegistry registry)------>LintDriver analyze分析 LintGradleProject

Android Studio:
AndroidLintGlobalInspectionContext------> performPreRunActivities-----> LintDriver analyze分析IntellijLintProject

明白了原理,咱們回到上面兩個問題

  1. 爲何命令行Lint 掃描不出增量代碼的問題
    我舉個例子:
    譬若有個TestActivity裏面寫了靜態的activity變量,LeakDetector會去檢查這個狀況,可是直接lint --sources app/src/com/demo/TestActivity.java .你會發現掃描不出這個錯誤或者提示'app' is a Gradle project. To correctly analyze Gradle projects, you should run "gradlew :lint" instead. [LintError],其實這兩個問題都是同一個緣由。
    LeakDetector會去判斷靜態變量是否是Activity類,可是變量的PsiField倒是com.demo.TestActivity不是'android'開頭,這樣就掃描不出問題了。

    @Override
         public void visitField(PsiField field) {
          String fqn= field.getType().getCanonicalText();
            if (fqn.startsWith("android.")) {//fqn變量是com.demo.TestActivity
                 if (isLeakCandidate(cls, mContext.getEvaluator())
                         && !isAppContextName(cls, field)) {
                     String message = "Do not place Android context classes in static fields; "
                             + "this is a memory leak (and also breaks Instant Run)";
                     report(field, modifierList, message);
                 }
             }
    }複製代碼

    那爲何fqn不是android.app.activity呢,由於lint命令行會把lib目錄下面jar的class加入掃描造成抽象語法樹,可是gradle項目是compile jar的,不在lib目錄下面,這就是爲何高版本的lint裏面提示不能掃描gradle項目。這也側面說明了命令行lint走不通

  2. android studio是怎麼實現lint增量掃描的
    android studio內部會掃描IntellijLintProject中的文件,IntellijLintProject是由
    create(IntellijLintClient client, List<VirtualFile> files,Module... modules)生成的,那就只要找到文件加入project的代碼就能找到增量代碼掃描的方案了。

    if (project != null) {
       project.setDirectLibraries(Collections.<Project>emptyList());
       if (file != null) {
         project.addFile(VfsUtilCore.virtualToIoFile(file));
       }
    }複製代碼

    那爲何addfile之後LintDriver會增量掃描呢,拿java文件掃描舉個例子,LintDriver會判斷subset是否是爲空,不爲空就不掃描JavaSourceFolders,只掃描增量文件。

    List<File> files = project.getSubset();
                 if (files != null) {//判斷是否是要增量掃描
                     checkIndividualJavaFiles(project, main, checks, files);
                 } else {
                     List<File> sourceFolders = project.getJavaSourceFolders();
                     List<File> testFolders = scope.contains(Scope.TEST_SOURCES)
                             ? project.getTestSourceFolders() : Collections.emptyList();
                     checkJava(project, main, sourceFolders, testFolders, checks);
                 }複製代碼

只掃描優先級高的問題

雖然Lint支持配置lint.xml去忽略Issue,可是隻能一個個忽略,個人方案是設置優先級低的規則爲Severity.IGNORE,LintDirver會忽略Severity.IGNORE的規則

@Override
            public Severity getSeverity(Issue issue) {
                Severity severity = super.getSeverity(issue);
                if (onlyHighPriority) {
                    if (issue.getCategory().compareTo(Category.USABILITY) < 0 && issue.getPriority() > 4) {//只掃描優先級比較高的規則
                        return severity;
                    }
                    return Severity.IGNORE;
                }
                return severity;
            }複製代碼

自動執行代碼掃描

Git Hooks提供了post-commit實現commit以後自動執行任務,可是你會發如今post-commit裏寫 ./gradlew Lint,仍是要等lint任務執行完了才commit成功。我發現只要在shell腳本里加入&>/dev/null就能夠後臺執行了。

nohup ./gradlew  LintIncrement  &>/dev/null &複製代碼

自動同步Git Hooks

若是Git Hooks腳本須要每臺電腦本身去複製,這明顯不利於團隊合做,並且不方便後面更新腳本,我選擇用Gradle命令複製到指定目錄,可是這裏有個問題,gradle插件能帶資源文件嗎,若是沒有專門學過gradle說不定一時無從下手,還好我恰好之前看過fastdex裏面是怎麼解決的,經過getResourceAsStream能夠複製Gradle插件resources下面的文件

public static void copyResourceFile(String name, File dest) throws IOException {
        FileOutputStream os = null;
        File parent = dest.getParentFile();
        if (parent != null && (!parent.exists())) {
            parent.mkdirs();
        }
        InputStream is = null;

        try {
            is = FileUtils.class.getResourceAsStream("/" + name);
            os = new FileOutputStream(dest, false);

            byte[] buffer = new byte[BUFFER_SIZE];
            int length;
            while ((length = is.read(buffer)) > 0) {
                os.write(buffer, 0, length);
            }
        } finally {
            if (is != null) {
                is.close();
            }
            if (os != null) {
                os.close();
            }
        }
    }複製代碼

複製腳本installGitHooks是這樣實現的,finalizedBy保證它在build任務後面自動執行,它會把/resource/post-commit文件複製到工程.git/hooks/post-commit。chmod -R +x .git/hooks/必定要寫,否則沒有權限

private void createGitHooksTask(Project project) {
        def preBuild = project.tasks.findByName("preBuild")

        if (preBuild == null) {
            throw new GradleException("lint need depend on preBuild and clean task")
            return
        }

        def installGitHooks = project.getTasks().create("installGitHooks")
                .doLast {

                    File postCommitFile = new File(project.rootProject.rootDir, PATH_POST_COMMIT)
                    if (lintIncrementExtension.isCheckPostCommit()) {
                      FileUtils.copyResourceFile("post-commit", postCommitFile)
                    } else {
                         if (preCommitDestFile.exists()) {
                             preCommitDestFile.delete()
                            }
                    }
                    Runtime.getRuntime().exec("chmod -R +x .git/hooks/")
                }

        preBuild.finalizedBy installGitHooks
    }複製代碼

Gradle插件實現發送郵件

image.png
image.png

原來打算直接用shell腳本里面的sendmail去發送郵件的,可是聽同事說若是mac上沒有登陸郵箱是無法發送成功的,我就用了javamail,網上的方案大多數是在java裏面實現javamail,在gradle裏面發送郵件的方案比較少,我嘗試了屢次才解決。

首先在gradle插件的build.gradle裏面加入javamail的依賴,剛開始我是直接compile了,可是運行之後提示我沒找到javamail的類,原來是要ant能找到javamail的類才行

configurations {
   antClasspath
}
dependencies {
   antClasspath 'ant:ant-javamail:1.+'
   antClasspath 'javax.activation:activation:1.1.1'
   antClasspath 'javax.mail:mail:1.+'
}
ClassLoader antClassLoader = org.apache.tools.ant.Project.class.classLoader
configurations.antClasspath.each { File jar ->
   antClassLoader.addURL( jar.toURI().toURL() )
}複製代碼

而後在gralde裏面執行發送任務

void send(File file) {
       getProject().ant.mail(
 from: fromMail,// 發件方
 tolist: toList,//收件方
 ccList: ccList,//抄送方
 message: message,//消息內容
 subject: subject,//標題
 mailhost: mailhost,//SMTP轉發服務器
 messagemimetype: "text/html",//消息格式
 files: file.getAbsolutePath()//發送文件目錄
        )
    }複製代碼

這裏有幾個注意點

  1. mailhost填入不須要SSL 認證的smtp服務器,否則你就須要輸入帳號和密碼才能發送郵件
  2. message裏面換行,不能用\n,由於messagemimetype是html格式,要使用<br>

發現問題回滾代碼

if (lintClient.haveErrors() ) {
          "git reset HEAD~1".execute(null, project.getRootDir())
        }複製代碼

如何調試gradle 插件

我原來看了幾篇Lint原理分析就打算去實現增量掃描,而後發現看和作仍是不同的,中間遇到好多問題,還好gradle插件能夠調試。

第一步 點擊edit configurations

image.png
image.png

第二步 建立remote,默認選項就能夠
image.png
image.png

第三步 在你要運行的gradle任務裏面加入
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
image.png
image.png

第四步,先點擊運行你要運行的gradle任務,gradle會等待你點擊remote,而後就能夠調試了

Lint版本變更

發現android gradle最新的幾個版本對於lint作了一些優化,我順便提一下。

  1. 2.3.0之後運行./gradlew lint會更快,Google實現了LintCharSequence來完成數據的存儲和傳參,實現了內存中只有一份拷貝
  2. 2.3.0之後lint-report.html是material design,更好看、更方便查問題
  3. 2.3.0之後支持baseline增量顯示bug
  4. 3.0.0之後自定義lint規則就不用像原來美團的方法)同樣麻煩了,官方支持
  5. 掃描會更快,uast語法樹替換了如今的psi和lombok語法樹

尾聲

回過頭來看,其實增量掃描也很簡單,就一行關鍵性代碼project.addfile(file)

最後講一下你們關心的開源問題吧,那要等在公司內部穩定運行之後在公司Github地址開源,畢竟咱們是一款嚴肅的產品嘛。

相關文章
相關標籤/搜索