Android 項(xiàng)目代碼質(zhì)量保證實(shí)踐

1 背景

一個(gè)項(xiàng)目開(kāi)發(fā)必然會(huì)涉及團(tuán)隊(duì)協(xié)作,而工程質(zhì)量就需要團(tuán)隊(duì)去保證。一般我們期望的代碼:無(wú)潛在風(fēng)險(xiǎn)、無(wú)重復(fù)邏輯、風(fēng)格無(wú)差異、可閱讀性好、新人上手速度快等。為了達(dá)到上述目標(biāo),一般團(tuán)隊(duì)都會(huì)制定一套自己認(rèn)可的編碼規(guī)范,并且周期性進(jìn)行 code review。然而編碼規(guī)范的制定,那么一套編碼規(guī)范需要包含哪些內(nèi)容,另外編碼規(guī)范僅僅是一套軟規(guī)范,實(shí)際程序員同學(xué)能遵守到什么程序還是一個(gè)未知數(shù),所以需要我們進(jìn)行人肉 code review,而這種人肉排查方式,很容易遺漏部分問(wèn)題,保障性還是有些不足。

為此,我們從編碼前期、編碼中期和編碼后期保證進(jìn)行了初步嘗試。

2 編碼前期 - 編碼規(guī)范

對(duì)于一個(gè) Android 項(xiàng)目,一般需要涉及的編碼規(guī)范有:

  1. 普通 java 編碼風(fēng)格規(guī)范

    如每個(gè)方法最大行數(shù),每個(gè)類文件的最大行數(shù),每個(gè)方法最大參數(shù)數(shù)等

  2. 普通 java 編碼最佳實(shí)踐

    iffortry 等嵌套深度規(guī)范,變量初始化規(guī)范等

  3. 通用 Android 編碼規(guī)范(java 部分和 xml 部分),

    包含 Android java 部分和 Android xml 部分,如避免使用普通內(nèi)部類定義handler,避免 layout xml 中存在無(wú)用結(jié)點(diǎn)等

  4. Gradle 編碼規(guī)范

    如盡量避免 lib 使用 module,獨(dú)立工程可以通過(guò) aar 或 mvn 方式導(dǎo)入

  5. 具體項(xiàng)目相關(guān)的編碼規(guī)范等

    如項(xiàng)目團(tuán)隊(duì)規(guī)定使用自定義 LogUtil 打日志,Activity、Fragment 等重要類的繼承關(guān)系,Activity 對(duì)應(yīng)的 xml 文件必須以 activity_ 開(kāi)頭等

制定了這些內(nèi)容可以一定程度上規(guī)范程序猿的編碼,配合團(tuán)隊(duì)進(jìn)行了周期性的 code review (一般是一個(gè)版本一次,大概 4 個(gè)星期一次),會(huì)有比較好的效果。然而即使這么做,還是存在一定的問(wèn)題,距離我們期望的目標(biāo)還是比較遠(yuǎn)。比如各個(gè)單例類的定義五花八門(mén),使用 LogUtil 代替 Log 的使用,Message.Obtain() 代替 new Message()Activity 部分文件命名,甚至 ActivityFragment 的基類定義規(guī)則還是很容易發(fā)生錯(cuò)誤,并沒(méi)有被發(fā)現(xiàn)。隨著編碼規(guī)范的完善充實(shí),多個(gè)開(kāi)發(fā)的編碼規(guī)范如何保證,就會(huì)成為一個(gè)顯而易見(jiàn)的問(wèn)題。

3 編碼中期 - 編碼模板

為了實(shí)現(xiàn)公用代碼復(fù)用,我們定義了一些 util 工具類,但隨著各個(gè)開(kāi)發(fā)的補(bǔ)充,這套 util 工具類也越來(lái)越多,如 LogUtilKeyboardUtil 等,而這些類一部分是為了統(tǒng)一入口,如統(tǒng)一使用 LogUtil,可以統(tǒng)一做到測(cè)試服打開(kāi)本地日志,線上服關(guān)閉日志;KeyboardUtil 方便使用者控制鍵盤(pán)的彈出隱藏等操作。雖然定義了這些工具類,但終究存在應(yīng)該使用而沒(méi)有使用的情況。當(dāng)然這些工具代碼并不難,開(kāi)發(fā)在自己的模塊也能很容易的實(shí)現(xiàn)和使用,一般也不會(huì)出問(wèn)題。然而上述講的優(yōu)點(diǎn)都會(huì)消失掉。而這些問(wèn)題依賴 code review 也是件頭疼的問(wèn)題。

此外,RecycleView 的編碼方式,單例模式的實(shí)現(xiàn)方式等等,各個(gè)開(kāi)發(fā)可能寫(xiě)出各式代碼,甚至實(shí)現(xiàn)的單例模式并不是線程安全的。

提了這么多,另一方面,要求開(kāi)發(fā)在繁忙的業(yè)務(wù)中嚴(yán)格遵守這些規(guī)范,也有些強(qiáng)人所難。所幸,Android Studio 為我們提供了編碼模板來(lái)解放開(kāi)發(fā)的工作,并一定程度上統(tǒng)一編碼風(fēng)格。

3.1 Live Template

3.1.1 系統(tǒng)模板

查看一個(gè)使用 Android Studio 中很常見(jiàn)的例子,輸入 for,出現(xiàn)下拉列表如下:

image

當(dāng)選擇 fori,出現(xiàn)編碼片段:

image

確認(rèn)循環(huán)變量 i,跳入循環(huán)結(jié)果值的輸入:

image

上例,就是 Android Studio 中系統(tǒng)提供的 Live Template 一個(gè)實(shí)例。這個(gè)類似于 iOS 中的 Code Snippets,提供了代碼片段的能力。

Android Studio (Mac) 進(jìn)入 Settings/Preferences -> Editor -> Live Templates,可以看到已定義的模板組:

image

查看 fori 編碼模板的實(shí)現(xiàn):

image
  • A: 完成模板的快捷鍵 Tab
  • B: 位置分類 iterations
  • C: 編碼模板對(duì)應(yīng)的縮寫(xiě) fori
  • D: 模板的內(nèi)容
  • E: 模板應(yīng)用環(huán)境

3.1.2 自定義模板

  1. 構(gòu)建項(xiàng)目 group

    image

    輸入 group 的名稱

    image
  2. 構(gòu)建具體編碼模板

    image
    • Abbreviation:觸發(fā)編碼模板的縮寫(xiě)
    • Description:模板的具體描述
    • Template Text:具體的模板內(nèi)容
  3. 變量定義

    變量形式為 $<variable_name>$,點(diǎn)擊 Edit variables 可設(shè)置變量具體內(nèi)容:

    image
    • Name:變量名
    • Expression:變量表現(xiàn)
    • Default value:默認(rèn)值
    • Skip if defined:是否跳過(guò)編輯已經(jīng)定義的值
  4. 模板應(yīng)用環(huán)境

    點(diǎn)擊 No application contexts yet. Define,設(shè)置為 java 環(huán)境:

    image
  5. 模板文件

    構(gòu)建了模板 group 后,在 android studio config\templates 目錄下查看到 yanxuan.xml

    windows: C:\Users\\<user>\\.AndroidStudiox.x\config\templates(user 為你的計(jì)算機(jī)用戶名)

    mac:~/Library/Preferences/AndroidStudiox.x/templates

    <templateSet group="test">
      <template name="yxtest" value="testMethod($a$, $b$);" description="這是一個(gè)測(cè)試模板" toReformat="false" toShortenFQNames="true">
        <variable name="a" expression="lineNumber()" defaultValue="2" alwaysStopAt="false" />
        <variable name="b" expression="" defaultValue="" alwaysStopAt="true" />
        <context>
          <option name="JAVA_CODE" value="true" />
          <option name="JAVA_STATEMENT" value="true" />
          <option name="JAVA_EXPRESSION" value="true" />
          <option name="JAVA_DECLARATION" value="true" />
          <option name="JAVA_COMMENT" value="true" />
          <option name="JAVA_STRING" value="true" />
          <option name="COMPLETION" value="true" />
        </context>
      </template>
    </templateSet>
    
  6. 設(shè)置完畢,實(shí)踐查看:

    image

    yxtest

    image

    singleton

3.2 Android Studio Template

  1. 背景

    除了 Live Template 之外,工程項(xiàng)目中很多新建的類也有很多機(jī)械的代碼,如我們定義的 Activity 要么繼承自 BaseBlankActivity,要么繼承自 BaseActionBarActivity,另外項(xiàng)目中采用 MVP 模式,因此一個(gè) Activity 基本上會(huì)有一個(gè)對(duì)應(yīng)的 presenter 類,一個(gè) layout 文件,同時(shí)很多時(shí)候,一個(gè)頁(yè)面中會(huì)有一個(gè)需要支持刷新的 RecycleView 等。除此之外,ViewHolderHttpTask 等代碼也是固定模式的代碼。

    這些都是固定機(jī)械的代碼,而如果是人肉去寫(xiě)的話,難免會(huì)出現(xiàn)代碼風(fēng)格不一致、不規(guī)范的情況,同時(shí)也浪費(fèi)了一部分的時(shí)間。所幸,Android Studio 提供了工程類模板,方便我們實(shí)現(xiàn)這樣的功能。

  2. 系統(tǒng)模板

    查看 Android Studio 系統(tǒng)類模板,我們能發(fā)現(xiàn)有很多定義好的類模板:

    image

    如需要?jiǎng)?chuàng)建一個(gè)空的 Activity 頁(yè)面,可以選擇 Empty Activity,并填寫(xiě)類名,layout 名稱等信息,之后就能出現(xiàn)對(duì)應(yīng)的添加或修改:MainActivity.javaactivity_main.xmlAndroidManifest.xml

    image

    而這些模板定義,可以在相關(guān)路徑文件中找到:

    • Windows: ${Android Studio 的安裝目錄}/plugins/android/lib/templates/
    • Mac: Android Studio.app/Contents/plugins/android/lib/templates/
    image

    針對(duì) EmpytActivity 這里需要定義的文件有:

    • globals.xml.ftl:定義當(dāng)前模板的一些全局變量
    • recipe.xml.ftl:定義模板拷貝的邏輯等
    • template.xml:定義模板對(duì)話框的樣式
    • template_blank_activity.png:定義模板的圖標(biāo)
    • root/src/app_package/SimpleActivity.java.ftl:具體的模板文件
    image16

    圖片來(lái)自:http://www.slideshare.net/murphonic/custom-android-code-templates-15537501

  3. 自定義模板

    而針對(duì)我們需要自定義的模板,可以在模板定義路徑下新建文件夾和文件即可,細(xì)節(jié)內(nèi)容可查看 Tutorial How To Create Custom Android Code Templates

    項(xiàng)目的模板文件內(nèi)容:

    image17

    設(shè)置完模板文件之后,重啟 Android Studio,可以生效模板文件,使用模板文件如下:

    image18

4 編碼后期 - 靜態(tài)代碼檢查

由上,我們定義了編碼規(guī)范,定義了 Live Template 和 Android Studio Template 方便程序猿更好的準(zhǔn)守我們的項(xiàng)目編碼規(guī)范。然后編碼規(guī)范畢竟只是軟規(guī)范,而提供編碼模板更多的解決大量 util 的使用問(wèn)題和便利小伙伴完成機(jī)械編碼,并不能完全保證程序猿嚴(yán)格按照全部的規(guī)范來(lái)編碼。

為此,我們需要一套靜態(tài)代碼檢查機(jī)制能檢查已有的代碼是否遵守規(guī)范。總結(jié)已有的規(guī)范,可以將規(guī)范類型歸納為普通 Java 規(guī)范、普通 Android 規(guī)范、具體項(xiàng)目規(guī)范等。而這些檢查點(diǎn),可以配合不同的檢查工具進(jìn)行檢查。

4.1 檢查 java 代碼風(fēng)格 - CheckStyle

對(duì)于 java 規(guī)范,checkstyle 幫助開(kāi)發(fā)者實(shí)現(xiàn)常用的檢查。這里 CheckStyle 能檢查的內(nèi)容有:

  1. Javadoc 注釋
  2. 命名約定
  3. 標(biāo)題
  4. Import 語(yǔ)句
  5. 體積大小
  6. 空白
  7. 修飾符
  8. 代碼問(wèn)題
  9. 類設(shè)計(jì)
  10. 混合檢查(包活一些有用的比如非必須的System.out和printstackTrace)

檢查內(nèi)容很多,而檢查項(xiàng)需要和具體的項(xiàng)目規(guī)范做結(jié)合。如,每行代碼字符數(shù)控制在 80,單頁(yè)代碼行數(shù)控制在 800 等。因此需要結(jié)合配置文件,來(lái)檢查項(xiàng)目中的 java 代碼。在 Android Studio 上配置 CheckStyle 流程如下:

  1. 在 Android Studio 中添加 gradle Plugin

    apply plugin: 'checkstyle'
    
  2. 設(shè)置 CheckStyle 版本

    checkstyle {
        toolVersion '6.1.1'
        showViolations true
    }
    
  3. 配置 CheckStyle 檢查項(xiàng)

    task checkstyle(type: Checkstyle) {
        configFile file("$configDir/checkstyle/checkstyle.xml")
        configProperties.checkstyleSuppressionsPath = file("$configDir/checkstyle/suppressions.xml").absolutePath
        source 'src'
        include '**/*.java' // 檢查 java 代碼
        exclude '**/gen/**' // 排除生成的代碼
        classpath = files()
        ignoreFailures true  // 忽略檢查失敗的情況,避免gradle命令執(zhí)行中止
    }
    
  4. 配置自定義的檢查項(xiàng):

    checkstyle.xml

    <!--單個(gè)文件方法數(shù)上限最多為 30-->
    <module name="MethodCount">
        <property name="maxTotal" value="30"/>
    </module>
    <!--方法名的首字母是小寫(xiě)-->
    <module name="MethodName">
        <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
    </module>
    <!--靜態(tài)變量名的首字符是 s-->
    <module name="StaticVariableName">
        <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
        <property name="applyToPublic" value="true"/>
        <property name="applyToProtected" value="true"/>
        <property name="applyToPackage" value="true"/>
        <property name="applyToPrivate" value="true"/>
    </module>
    <!--只有私有構(gòu)造函數(shù)的類需要定義成 final 類型-->
    <module name="FinalClass"/>
    
    ...
    

    具體其他的檢查項(xiàng)配置可以查看 檢查配置鏈接

  5. 執(zhí)行 checkstyle 檢查

    ./gradlew checkstyle
    
  6. 查看檢查結(jié)果

    命令執(zhí)行結(jié)束,查看檢查結(jié)果文件:${project}/app/build/reports/checkstyle/checkstyle.html

    image

4.2 檢查 java 代碼質(zhì)量 - FindBugs

CheckStyle 工具不同的是,FindBugs 不注重樣式或者格式,而是試圖尋找出真正的缺陷或者現(xiàn)在的性能問(wèn)題。FindBugs 檢查類和 Jar 文件,不是通過(guò)分析類文件的形式或結(jié)構(gòu)來(lái)分析程序,而是使用 Visitor 模式,將字節(jié)碼與一組缺陷模式進(jìn)行對(duì)比以發(fā)現(xiàn)可能的問(wèn)題。而這些問(wèn)題比如如下:

  1. 忽略返回值

    image

    上述代碼執(zhí)行結(jié)束之后,并沒(méi)有什么意義,變量 a 的值也不會(huì)變成 dddbbbccc。因此,上述代碼很可能是程序猿的 bug。為此 FindBugs 能找出這種問(wèn)題

  2. 空指針示例

    image

    上述最后一行代碼,很明顯在執(zhí)行的時(shí)候會(huì)發(fā)生空指針異常,這里因?yàn)?FindBugs 無(wú)法知道變量 strMaps 是否確實(shí)有 aaa 這個(gè) key,為此這里會(huì)檢查出錯(cuò)誤。

  3. 未初始化的成員變量使用

    image

    這里由于類成員變量 actions 并未初始化,因此當(dāng) actions.add("TEST") 被執(zhí)行的時(shí)候會(huì)發(fā)生異常。

Android Studio 上 FindBugs 的集成如下:

  1. 在 gradle 中引入插件

    apply plugin: 'findbugs'
    
  2. 在 gradle 中配置 findbugs task

    task findbugs(type: FindBugs, dependsOn: "assembleDebug") {
        ignoreFailures = false
        effort = "max"
        reportLevel = "high"
        excludeFilter = new File("$configDir/findbugs/findbugs-filter.xml")
        classes = files("${project.rootDir}/app/build/intermediates/classes")
    
        source 'src'
        include '**/*.java'
        exclude '**/gen/**'
    
        reports {
            xml.enabled = false
            html.enabled = true
            xml {
                destination "$reportsDir/findbugs/findbugs.xml"
            }
            html {
                destination "$reportsDir/findbugs/findbugs.html"
            }
        }
    
        classpath = files()
        ignoreFailures true  // 避免檢查失敗 gradle 執(zhí)行中止
    }
    
  3. 執(zhí)行 findbugs 檢查

    ./gradlew findbugs
    
  4. 查看檢查結(jié)果

    查看檢查結(jié)果文件:${project}/app/build/reports/findbugs/findbugs.html

    image

4.3 檢查 Android 代碼質(zhì)量 - Lint

4.3.1 基本介紹

前面 FindBugs 的檢查實(shí)例(忽略返回值, 未初始化的成員變量使用),可以發(fā)現(xiàn)在 Android Studio IDE 上,已經(jīng)出現(xiàn)了標(biāo)黃提示,我們把光標(biāo)放上去,就能看到具體的提示了:

image

cmd + F1 可以看到具體的錯(cuò)誤提示:

image

這就原生 Lint 給我們提供的錯(cuò)誤提示功能。除了和 FindBugs 重復(fù)的純 java 代碼檢查之外,Lint 能檢查很多其他工具無(wú)法檢查的內(nèi)容,也更貼合 Android:

image

在 Activity 內(nèi)定義非靜態(tài)內(nèi)部類 Handler 的報(bào)警

image

AndroidManifest.xml 中定義 export 為 true 的廣播接受器,但沒(méi)有定義權(quán)限,Lint 檢查認(rèn)為是不安全的

image

build.gradle 文件中引用的 support 包的版本低的提示

Android Lint 是一個(gè)靜態(tài)代碼檢查工具,能夠?qū)撛诘?bug,可能的安全性、性能、可用性、可訪問(wèn)性、國(guó)際化等優(yōu)化內(nèi)容做出監(jiān)測(cè):

image
image

來(lái)自官方文檔 Improve Your Code with Lint

  1. App Source Files:工程中的源文件,包括 java 代碼、資源 xml 代碼、gradle 文件,圖片資源文件以及 progroud 等文件
  2. lint.xml:配置文件,配置哪些 lint 檢查可以排除,自定義問(wèn)題的嚴(yán)重級(jí)別
  3. lint Tool:靜態(tài)代碼檢查工具,可以是命令行或者集成在 Android Studio 上
  4. lint Output:代碼檢查結(jié)果,可以直接顯示在 Console 上,也可以是 lint-result.html

4.3.2 原生 Lint 檢查

在 Android SDK Tools 16 及更高的版本中,Lint 工具會(huì)自動(dòng)安裝。原生 Lint 的檢查項(xiàng)已經(jīng)有 200 多項(xiàng) (包括前面示例的 5 項(xiàng)內(nèi)容),因此使用原生的功能點(diǎn),就能檢查開(kāi)發(fā)中的大部分通用問(wèn)題。

  1. Android Studio IDE 上配置 Lint 檢查偏好設(shè)置

    (Mac 下) PreferencesEditorInspections 進(jìn)入 Android StudioLint 配置界面

    image
    • A:配置 Profile,方便不同項(xiàng)目或者不同情況下使用不同的 Profile 進(jìn)行檢查
    • B:Lint 檢查點(diǎn) Group
    • C:Lint 檢查點(diǎn)具體描述
    • D:Lint 檢查點(diǎn)警告級(jí)別設(shè)置
    • E:Lint 檢查點(diǎn)開(kāi)關(guān)
  2. lint.xml 上配置 Lint

    除了可以通過(guò) IDE 配置 Lint,還可以通過(guò)直接 lint.xml 為單個(gè)項(xiàng)目配置檢查規(guī)則

    <?xml version="1.0" encoding="UTF-8"?>
    <lint>
        <!-- Disable the given check in this project -->
        <issue id="IconMissingDensityFolder" severity="ignore" />
    
        <!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
        <issue id="ObsoleteLayoutParam">
            <ignore path="res/layout/activation.xml" />
            <ignore path="res/layout-xlarge/activation.xml" />
        </issue>
    
        <!-- Ignore the UselessLeaf issue in the specified file -->
        <issue id="UselessLeaf">
            <ignore path="res/layout/main.xml" />
        </issue>
    
        <!-- Change the severity of hardcoded strings to "error" -->
        <issue id="HardcodedText" severity="error" />
    </lint>
    

    來(lái)源 Android Develop 文檔 Improve Your Code with Lint

  3. gradle 中配置 Lint task

    android {
        lintOptions {
            abortOnError false // 配置 lint 過(guò)程中出錯(cuò),不中止 gradle 任務(wù)
            xmlReport false
            htmlReport true
            lintConfig file("$configDir/lint/lint.xml") // 配置 lint 檢查規(guī)則
            htmlOutput file("$reportsDir/lint/lint-result.html") // 配置 lint 輸出文件
            xmlOutput file("$reportsDir/lint/lint-result.xml") // 配置 lint 輸出文件
        }
    }
    
  4. 執(zhí)行檢查

    在工程根目錄執(zhí)行以下命令 (Mac),以執(zhí)行檢查任務(wù)

    ./gradlew lint
    
  5. 檢查結(jié)果

    生成的檢查結(jié)果在 ${項(xiàng)目工程}/app/build/reports/lint/lint-result.html

    image

4.3.3 自定義 Lint 檢查

雖然原生的 Lint 檢查已經(jīng)很強(qiáng)大了,檢查項(xiàng)也已經(jīng)很多,然而還是無(wú)法滿足項(xiàng)目中的特有需求:

  1. log 統(tǒng)一使用 LogUtil
  2. 對(duì)應(yīng) Activity 的 layout 命名為 activity_XXX
  3. 對(duì)應(yīng) Fragment 的 layout 命名為 fragment_XXX
  4. Activity 必須派生自 BaseBlankActivityBaseActionBarActivity

對(duì)于以上這些需求,原生 Lint 檢查(包括 CheckStyleFindBugs)就已經(jīng)無(wú)能為力了,我們必須編碼支持自定義檢查。以項(xiàng)目中集成的 Lint 檢查為例,講述流程:

4.3.3.1 配置 Gradle,引入 lint 庫(kù)
dependencies {
    ...
    compile 'com.android.tools.lint:lint-api:24.5.0'
    compile 'com.android.tools.lint:lint-checks:24.5.0'
}
  • lint-api: 官方給出的API,API并不是最終版,官方提醒隨時(shí)有可能會(huì)更改API接口。
  • lint-checks:已有的檢查。
4.3.3.2 定義 IssueRegistry

新建一個(gè) MyIssueRegistry 類,繼承自 IssueRegistry。用來(lái)注冊(cè)我們自定義的全部 issue

public class MyIssueRegistry extends IssueRegistry {
    @Override
    public List<Issue> getIssues() {
        System.out.println("********YXLint rules works!!!********");
        return Arrays.asList(
                LogUsageDetector.ISSUE,
                ToastUsageDetector.ISSUE,
                ActivitySuperClassDetector.ACTIVITY_SUPER_CLASS_ISSUE,
                ...
                BuildGradleVersionDetector.ISSUE);
    }
}

其中:

  • LogUsageDetector.ISSUE:用于檢查不允許直接使用 Log.* 方式輸出本地日志的代碼
  • ToastUsageDetector.ISSUE:用于檢查直接用 Toast 方式顯示 toast 的代碼
  • ActivitySuperClassDetector.ACTIVITY_SUPER_CLASS_ISSUE:用于檢查 Activity 的基類
  • BuildGradleVersionDetector.ISSUE:用于檢查 gradle 文件中不允許直接寫(xiě)數(shù)字版本號(hào)的代碼
4.3.3.3 gradle 清單項(xiàng)中注冊(cè)前面定義的 IssueRegistry
jar {
    manifest {
        attributes('Lint-Registry': 'com.netease.htlint.lintrules.MyIssueRegistry')
    }
}
4.3.3.4 定義 Detector

MyIssueRegistry 類中聲明注冊(cè)了各個(gè) DetectorIssueIssueDetector 發(fā)現(xiàn)并報(bào)告,是 Android 程序代碼可能存在的風(fēng)險(xiǎn)。而這里就需要真正實(shí)現(xiàn)這些 Detector,以檢查 Activity 的基類為例。

image

ACTIVITY_SUPER_CLASS_ISSUE 這個(gè) Issue 的定義需要使用 Issue.create(...) 方式實(shí)現(xiàn),同時(shí)需要傳入 6 個(gè)參數(shù)分別如下:

  • A:一個(gè)固定的唯一的 id 代表這個(gè) Issue
  • B:對(duì)于問(wèn)題的簡(jiǎn)短總結(jié),描述問(wèn)題而不是修復(fù)措施
  • C:完整的問(wèn)題解釋和修復(fù)建議
  • D:?jiǎn)栴}類別,現(xiàn)在已有的問(wèn)題類別有如下
    • Lint
    • Correctness (incl. Messages)
    • Security
    • Performance
    • Usability (incl. Icons, Typography)
    • Accessibility
    • Internationalization
    • Bi-directional text
  • E:優(yōu)先級(jí),必須在1到10之間,10為最重要/最嚴(yán)重。
  • F:嚴(yán)重級(jí)別,可選值有 Fatal, Error, Warning, Informational, Ignore
  • G:為 IssueDetector 提供映射關(guān)系,Detector 就是當(dāng)前類。聲明掃描檢測(cè)的范圍 Scope,描述 Detector 需要分析時(shí)需要考慮的文件集,包括:Resource 文件或目錄、Java 文件、Class 文件

ActivitySuperClassDetector 繼承自 Detector,并實(shí)現(xiàn) Detector.JavaScaner。這里主要自定義實(shí)現(xiàn)的方法如上圖 H,I

  • H:檢查類的基類是 "android.support.v4.app.Activity" 或 "android.app.Activity"
  • I:具體檢查類的方法,這里需要排除非 yanxuan 包名下的代碼,判斷當(dāng)前類的基類是否是 BaseBlankActivityBaseActionBarActivity?如果都不是的話,則報(bào)告錯(cuò)誤
  • J:報(bào)出問(wèn)題的方法:該方法中指定參數(shù)有:
    • 需要報(bào)錯(cuò)的 Issue
    • 發(fā)生問(wèn)題的代碼在語(yǔ)法樹(shù)上的節(jié)點(diǎn)
    • 發(fā)生問(wèn)題的代碼位置
    • 警告的信息
4.3.3.5 生成 jar 包

完成上述步驟,可以在控制臺(tái)中通過(guò)命令 ../../gradlew assemble 來(lái)執(zhí)行編譯任務(wù),就可以輸出我們需要的 jar 文件 (htlintrules_jar-0.0.1.jar) 了

4.3.3.6 jar 的使用

按照 Google 方法,可以將 htlintrules_jar-0.0.1.jar 拷貝到 ~/.android/lint 中,但缺點(diǎn)是針對(duì)會(huì)影響一臺(tái)機(jī)器其他的工程。很明顯,我們的自定義 Lint 檢查有很多是項(xiàng)目中特有的一些編碼規(guī)范。

為此,我們采用 LinkedIn 方案:將 jar 放到一個(gè) aar 中。這樣我們就可以針對(duì)工程進(jìn)行自定義 Lint,lint.jar 只對(duì)當(dāng)前工程有效。

在現(xiàn)有的 htlintrules_jar 工程的 build.gradle 中添加代碼,整體看起來(lái)如下:

apply plugin: 'java'
apply plugin: 'maven'
dependencies {
    compile 'com.android.tools.lint:lint-api:24.5.0'
    compile 'com.android.tools.lint:lint-checks:24.5.0'
}
    
jar {
    manifest {
        attributes('Lint-Registry': 'com.netease.htlint.lintrules.MyIssueRegistry')
    }
}
    
configurations {
    lintJarOutput
}
    
dependencies {
    lintJarOutput files(jar)
}
    
defaultTasks 'assemble'

同時(shí)新建另一個(gè)工程 htlint,在其 build.gradle 文件中添加如下代碼:

/*
 * rules for including "lint.jar" in aar
 */
configurations {
    lintJarImport
}
    
dependencies {
    lintJarImport project(path: ':htlintrules_jar', configuration: "lintJarOutput")
}
    
task copyLintJar(type: Copy) {
    from (configurations.lintJarImport) {
        rename {
            String fileName ->
                'lint.jar'
        }
    }
    into 'build/intermediates/lint/'
}
    
project.afterEvaluate {
    def compileLintTask = project.tasks.find { it.name == 'compileLint' }
    compileLintTask.dependsOn(copyLintJar)
}

最后在 app 工程的 build.gradle 中添加 htlint 引用,配置完成

dependencies {
    compile project(':htlint') // lint 檢查庫(kù)
    ...
}
4.3.3.7 自定義 Lint 檢查執(zhí)行及結(jié)果檢查

${項(xiàng)目工程}/app/ 目錄下執(zhí)行 ../gradlew lint

image

根據(jù)提示查看 lint-result.html 文件,可以查看到前面編寫(xiě)的 ActivitySuperClassDetector.ACTIVITY_SUPER_CLASS_ISSUE 已經(jīng)生效,并且檢查出了相關(guān)的非規(guī)范代碼。

image
image
4.3.3.8 排除錯(cuò)誤的檢查結(jié)果

前面很好的給出了檢查結(jié)果了,然而我們會(huì)發(fā)現(xiàn),FullScreenVideoActivity 確實(shí)是需要的錯(cuò)誤檢查結(jié)果,而 WXEntryActivity 卻不是,這個(gè)類是有集成微信分享時(shí)需要的,并且按照微信開(kāi)放平臺(tái)的文檔來(lái)編寫(xiě),因此并不需要按照項(xiàng)目規(guī)范,繼承 BaseBlankActivityBaseActionBarActivity。為此,我們期望 WXEntryActivity 不應(yīng)該被檢查出 WrongActivitySuperClass 錯(cuò)誤

為此,我們可以在 WXEntryActivity 類名簽名添加 SuppressLint 注解:

@SuppressLint("WrongActivitySuperClass")
public class WXEntryActivity extends Activity implements IWXAPIEventHandler{
    ...
}
  1. 排除 java 類或者方法的 Lint 檢查

    若需要抑制某個(gè) Issue 檢查,可以在類定義簽名或者方法定義簽名,添加注解 @SuppressLint(${IssueId})。這里設(shè)置的就是具體某個(gè) Issueid

    若需要抑制全部的 Issue 檢查,可以使用 all 關(guān)鍵字,比如:@SuppressLint("all")

  2. 排除 xml 資源的 Lint 檢查

    如項(xiàng)目中引入微博分享 sdk,按照官方文檔,需要在 AndroidManifest 中聲明 com.sina.weibo.sdk.net.DownloadService 這個(gè) Service,而這個(gè) Service 會(huì)被 Lint 檢查為未定義,為此需要 xml 文件中也過(guò)濾部分代碼的 Lint 的檢查:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        package="com.netease.yanxuan">
        
        ...
        
        <service
            android:name="com.sina.weibo.sdk.net.DownloadService"
            android:exported="false"
            tools:ignore="MissingRegistered" />
    </manifest>
    

    這里對(duì)于單個(gè) Issue 過(guò)濾的規(guī)則為:tools:ignore=${IssueId}

    如果需要過(guò)濾全部的 Issue,可以使用 all 關(guān)鍵字:tools:ignore="all"

4.4 其他代碼檢查工具

4.4.1 360 火線

360 火線 是 360 公司和信息安全部門(mén)深度合作,定制的適用于 360 公司產(chǎn)品的安卓 APP 安全檢查規(guī)則。總共覆蓋 61 項(xiàng)代碼檢查。使用也非常方便,細(xì)節(jié)看 使用文檔,可以直接使用 jar 包并執(zhí)行命令或集成 Android Studio Plugin 執(zhí)行檢查

image

4.4.2 pmd

pmd 代碼檢查工具,包含 16 個(gè)規(guī)則集,涵蓋了 Java 的各種常見(jiàn)問(wèn)題。其中規(guī)則集包含 基本(rulesets/basic.xml)終結(jié)函數(shù)(finalizer)未使用的代碼(rulesets/unusedcode.xml)設(shè)計(jì)(rulesets/design.xml) 等。

相比 FindBugspmd 的一些規(guī)則更具爭(zhēng)議,但 pmd 支持我們構(gòu)建自己的規(guī)則集

<?xml version="1.0"?>
<ruleset name="customruleset">
  <description>
  Sample ruleset for developerWorks article
  </description>
  <rule ref="rulesets/design.xml"/>
  <rule ref="rulesets/naming.xml"/>
  <rule ref="rulesets/basic.xml"/>
</ruleset>

4.5 代碼檢查工具整合及集成 jenkins

為整合這些檢查工具,在 gradle 中自定義 check 命名,并依賴其他的 task。在執(zhí)行檢查的時(shí)候,可以通過(guò) ./gradlew check 來(lái)執(zhí)行全部的檢查命令。

check.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint'

另一方面,這種代碼檢查,如果等到開(kāi)發(fā)完成的時(shí)候再去執(zhí)行,很可能問(wèn)題積累了很多,甚至導(dǎo)致產(chǎn)品上線前,開(kāi)發(fā)并不能來(lái)得及修正全部的問(wèn)題。為此,可以將代碼檢查的命令集成 jenkins,保證開(kāi)發(fā)每天都能看到當(dāng)前的代碼的缺陷,能及時(shí)的修改

5 總結(jié)

我們從編碼前的編碼規(guī)范,編碼進(jìn)行中的編碼模板,編碼結(jié)束后的代碼靜態(tài)檢查,保障了程序小伙伴們的代碼。除此之外,還有很多不完善的地方需要我們做進(jìn)一步處理:

  1. 和特定項(xiàng)目相關(guān)的自定義 Lint 檢查項(xiàng),僅支持了一部分的編碼規(guī)范,自定義 Lint 檢查項(xiàng)需要后續(xù)完成
  2. 雖然檢查工具很多,檢查的結(jié)果內(nèi)容也很多,而很明顯,這些檢查工具相互之間是有重復(fù)的情況(主要是純 java 代碼部分的檢查),因此如果整理檢查結(jié)果,并過(guò)濾出我們真正關(guān)心的問(wèn)題,也是后續(xù)需要完成的內(nèi)容
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評(píng)論 3 426
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 177,742評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,580評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,297評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,688評(píng)論 1 327
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,875評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,183評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,384評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,612評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,022評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,297評(píng)論 1 292
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,093評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,330評(píng)論 2 377

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,701評(píng)論 25 708
  • 前言 在團(tuán)隊(duì)Android項(xiàng)目開(kāi)發(fā)過(guò)程中,難免會(huì)出現(xiàn)一些比較不容易發(fā)現(xiàn),但是又比較低級(jí)的bug。而且因?yàn)槊總€(gè)開(kāi)發(fā)人...
    宇是我閱讀 5,074評(píng)論 3 19
  • 良好的開(kāi)始是成功的一半,對(duì)于一個(gè) Android 項(xiàng)目更是,一個(gè)好的項(xiàng)目基礎(chǔ)架構(gòu)可以對(duì)項(xiàng)目的后續(xù)發(fā)展有至關(guān)重要的作...
    大俠咕咚閱讀 1,650評(píng)論 0 8
  • 今天畫(huà)的畫(huà),心里很喜歡 昨晚上看完書(shū),剛要躺下睡覺(jué),旁邊小床上的閨女就開(kāi)始哼哼起來(lái),像是在做夢(mèng),左扭右扭,不時(shí)還搖...
    大臉and小臉閱讀 394評(píng)論 0 0
  • 今天,小周阿姨一不小心就把手機(jī)摔壞了,她把我?guī)еバ奘謾C(jī)。我們看了第一家手機(jī)店,那里面的人說(shuō)要780元。...
    漫步者說(shuō)閱讀 643評(píng)論 0 1