Android Lint 實踐 —— 簡介及常見問題分析

概況

QMUI Android 剛更新了 1.0.4 版本,其中主要的特性是引入了 Android Lint,對項目代碼進行優化。Android Lint 是 SDK Tools 16(ADT 16)開始引入的一個代碼掃描工具,經過對代碼進行靜態分析,能夠幫助開發者發現代碼質量問題和提出一些改進建議。除了檢查 Android 項目源碼中潛在的錯誤,對於代碼的正確性、安全性、性能、易用性、便利性和國際化方面也會做出檢查。php

而最終選擇了 Android Lint 做爲項目的代碼檢測工具,是由於它具備如下幾個特性:html

  • 已經被集成到 Android Studio,使用方便。
  • 能在編寫代碼時實時反饋出潛在的問題。
  • 能夠自定義規則。Android Lint 自己包含大量已經封裝好的接口,能提供豐富的代碼信息,開發者能夠基於這些信息進行自定義規則的編寫。

開始使用

Android Lint 的工做過程比較簡單,一個基礎的 Lint 過程由 Lint Tool(檢測工具),Source Files(項目源文件) 和 lint.xml(配置文件) 三個部分組成,Lint Tool 讀取 Source Files,根據 lint.xml 配置的規則(issue)輸出結果(以下圖)。java

如上面所描述,在 Android Studio 中,Android Lint 已經被集成,只須要點擊菜單 —— Analyze —— Inspect Code 便可運行 Android Lint,在彈出的對話框中能夠設置執行 Lint 的範圍,能夠選擇整個項目,也能夠只選擇當前的子模塊或者其餘自定義的範圍:android

檢查完畢後會彈出 Inspection 的控制檯,並在其中列出詳細的檢查結果:git

如上圖所展現的,Android Lint 對檢查的結果進行了分類,同一個規則(issue)下的問題會聚合,其中針對 Android 的規則類別會在分類前說明是 Android 相關的,主要是六類:github

  • Accessibility 無障礙,例如 ImageView 缺乏 contentDescription 描述,String 編碼字符串等問題。
  • Correctness 正確性,例如 xml 中使用了不正確的屬性值,Java 代碼中直接使用了超過最低 SDK 要求的 API 等。
  • Internationalization 國際化,如字符缺乏翻譯等問題。
  • Performance 性能,例如在 onMeasureonDraw 中執行 new,內存泄露,產生了冗餘的資源,xml 結構冗餘等。
  • Security 安全性,例如沒有使用 HTTPS 鏈接 Gradle,AndroidManifest 中的權限問題等。
  • Usability 易用性,例如缺乏某些倍數的切圖,重複圖標等。

其餘的結果條目則是針對 Java 語法的問題,另外每個問題都有區分嚴重程度(severity),從高到底依次是:正則表達式

  • Fatal
  • Error
  • Warning
  • Information
  • Ignore

其中 FatalError 都是指錯誤,可是 Fatal 類型的錯誤會直接中斷 ADT 導出 APK,更爲嚴重。另外以下圖所示,在結果列表中點擊一個條目,能夠看到詳細的源文件名和位置,以及命中的錯誤規則(issue)、解決方案或者屏蔽提示:安全

上圖的例子是在 ScrollView 的第一層子元素中設置了高度爲 match_parent,Android Lint 會直接給出解決辦法——使用 wrap_content 代替,大部分靜態語法相關的問題 Android Lint 均可以直接給出解決辦法。bash

除了直接在菜單中運行 Lint 外,大部分問題代碼在編寫時 Android Studio 就會給出提醒:異步

配置

對於執行 Lint 操做的相關配置,是定義在 gradle 文件的 lintOptions 中,可定義的選項及其默認值包括(翻譯自 LintOptions - Android Plugin 2.3.0 DSL Reference):

android {
    lintOptions {
        // 設置爲 true,則當 Lint 發現錯誤時中止 Gradle 構建
        abortOnError false
        // 設置爲 true,則當有錯誤時會顯示文件的全路徑或絕對路徑 (默認狀況下爲true)
        absolutePaths true
        // 僅檢查指定的問題(根據 id 指定)
        check 'NewApi', 'InlinedApi'
        // 設置爲 true 則檢查全部的問題,包括默認不檢查問題
        checkAllWarnings true
        // 設置爲 true 後,release 構建都會以 Fatal 的設置來運行 Lint。
        // 若是構建時發現了致命(Fatal)的問題,會停止構建(具體由 abortOnError 控制)
        checkReleaseBuilds true
        // 不檢查指定的問題(根據問題 id 指定)
        disable 'TypographyFractions','TypographyQuotes'
        // 檢查指定的問題(根據 id 指定)
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
        // 在報告中是否返回對應的 Lint 說明
        explainIssues true
        // 寫入報告的路徑,默認爲構建目錄下的 lint-results.html
        htmlOutput file("lint-report.html")
        // 設置爲 true 則會生成一個 HTML 格式的報告
        htmlReport true
        // 設置爲 true 則只報告錯誤
        ignoreWarnings true
        // 從新指定 Lint 規則配置文件
        lintConfig file("default-lint.xml")
        // 設置爲 true 則錯誤報告中不包括源代碼的行號
        noLines true
        // 設置爲 true 時 Lint 將不報告分析的進度
        quiet true
        // 覆蓋 Lint 規則的嚴重程度,例如:
        severityOverrides ["MissingTranslation": LintOptions.SEVERITY_WARNING]
        // 設置爲 true 則顯示一個問題所在的全部地方,而不會截短列表
        showAll true
        // 配置寫入輸出結果的位置,格式能夠是文件或 stdout
        textOutput 'stdout'
        // 設置爲 true,則生成純文本報告(默認爲 false)
        textReport false
        // 設置爲 true,則會把全部警告視爲錯誤處理
        warningsAsErrors true
        // 寫入檢查報告的文件(不指定默認爲 lint-results.xml)
        xmlOutput file("lint-report.xml")
        // 設置爲 true 則會生成一個 XML 報告
        xmlReport false
        // 將指定問題(根據 id 指定)的嚴重級別(severity)設置爲 Fatal
        fatal 'NewApi', 'InlineApi'
        // 將指定問題(根據 id 指定)的嚴重級別(severity)設置爲 Error
        error 'Wakelock', 'TextViewEdits'
        // 將指定問題(根據 id 指定)的嚴重級別(severity)設置爲 Warning
        warning 'ResourceAsColor'
        // 將指定問題(根據 id 指定)的嚴重級別(severity)設置爲 ignore
        ignore 'TypographyQuotes'
    }
}複製代碼

lint.xml 這個文件則是配置 Lint 須要禁用哪些規則(issue),以及自定義規則的嚴重程度(severity),lint.xml 文件是經過 issue 標籤指定對一個規則的控制,在項目根目錄中創建一個 lint.xml 文件後 Android Lint 會自動識別該文件,在執行檢查時按照 lint.xml 的內容進行檢查。如上面提到的那樣,開發者也能夠經過 lintOptions 中的 lintConfig 選項來指定配置文件。一個 lint.xml 示例以下:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- Disable the given check in this project -->
    <issue id="HardcodedText" severity="ignore"/>
    <issue id="SmallSp" severity="ignore"/>
    <issue id="IconMissingDensityFolder" severity="ignore"/>
    <issue id="RtlHardcoded" severity="ignore"/>
    <issue id="Deprecated" severity="warning">
        <ignore regexp="singleLine"/>
    </issue>
</lint>複製代碼

issue 標籤中使用 id 指定一個規則,severity="ignore" 則代表禁用這個規則。須要注意的是,某些規則能夠經過 ignore 標籤指定僅對某些屬性禁用,例如上面的 Deprecated,表示檢查是否有使用不推薦的屬性和方法,而在 issue 標籤中包裹一個 ignore 標籤,在 ignore 標籤的 regexp 屬性中使用正則表達式指定了 singleLine,則代表對 singleLine 這個屬性屏蔽檢查。

另外開發者也可使用 @SuppressLint(issue id) 標註針對某些代碼忽略某些 Lint 檢查,這個標註既能夠加到成員變量以前,也能夠加到方法聲明和類聲明以前,分別針對不一樣範圍進行屏蔽。

常見問題

咱們在使用 Android Lint 對項目進行檢查後,整理了一些問題及解決方法,下面列舉較爲常見的場景:

ScrollView size validation

這也是上文提到過的一個狀況,在 ScrollView 的第一層子元素中設置了高度爲 match_parent,這是錯誤的寫法,實際上在 measure 時這裏一定會被看成 wrap_content 去處理,所以按照 Lint 的建議,直接改成 wrap_content 便可。

Handler reference leaks

Handler 引用的內存泄露問題,例以下面的例子:

protected static final int STOP = 0x10000;
protected static final int NEXT = 0x10001;

@BindView(R.id.rectProgressBar) QMUIProgressBar mRectProgressBar;
@BindView(R.id.circleProgressBar) QMUIProgressBar mCircleProgressBar;

int count;

private Handler myHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case STOP:
                break;
            case NEXT:
                if (!Thread.currentThread().isInterrupted()) {
                    mRectProgressBar.setProgress(count);
                    mCircleProgressBar.setProgress(count);
                }
        }
    }
};複製代碼

首先非靜態的內部類或者匿名類會隱式的持有其外部類的引用,內部類使用了外部類的方法/成員變量也會致使其持有外部類引用,所以上面這種狀況會致使 handler 持有了外部類,外部類同時持有 handler,handler 是異步的,當 handler 的消息發送出去後,外部類因 hanlder 的持有而沒法銷燬,最終致使內存泄露。

解決辦法則是把該內部類改成 static,內部類中使用的外部類方法/成員變量改成弱引用,具體以下:

@BindView(R.id.rectProgressBar) QMUIProgressBar mRectProgressBar;
@BindView(R.id.circleProgressBar) QMUIProgressBar mCircleProgressBar;

int count;

private ProgressHandler myHandler = new ProgressHandler();

@Override
protected View onCreateView() {
    myHandler.setProgressBar(mRectProgressBar, mCircleProgressBar);
}

private static class ProgressHandler extends Handler {
    private WeakReference<QMUIProgressBar> weakRectProgressBar;
    private WeakReference<QMUIProgressBar> weakCircleProgressBar;

    public void setProgressBar(QMUIProgressBar rectProgressBar, QMUIProgressBar circleProgressBar) {
        weakRectProgressBar = new WeakReference<>(rectProgressBar);
        weakCircleProgressBar = new WeakReference<>(circleProgressBar);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case STOP:
                break;
            case NEXT:
                if (!Thread.currentThread().isInterrupted()) {
                    if (weakRectProgressBar.get() != null && weakCircleProgressBar.get() != null) {
                        weakRectProgressBar.get().setProgress(msg.arg1);
                        weakCircleProgressBar.get().setProgress(msg.arg1);
                    }
                }
        }

    }
}複製代碼

Memory allocations within drawing code

onMeasure、onDraw 都是被頻繁調用的方法,所以 Lint 不建議在其中執行 new 操做,能夠在 onCreateView 等非頻繁調用的時機進行 new 操做,並用成員變量保存,再在 onMeasure 中使用成員變量。

‘private’ method declared ‘final’

private static final void addLinkMovementMethod(TextView t) {
    MovementMethod m = t.getMovementMethod();

    // ...
}複製代碼

如上面的示例代碼,會產生 ‘private’ method declared ‘final’ 的警告,由於私有方法是不會被 override 的,所以徹底沒有必要聲明 final

參考資料:

使用 Lint 改進您的代碼 | Android Studio
LintOptions - Android Plugin 2.3.0 DSL Reference
Android Lint Checks - Android Studio Project Site

相關文章
相關標籤/搜索