Android自定义Lint实践
Android Lint是Google提供給Android開發(fā)者的靜態(tài)代碼檢查工具。使用Lint對Android工程代碼進行掃描和檢查,可以發(fā)現(xiàn)代碼潛在的問題,提醒程序員及早修正。
為保證代碼質(zhì)量,美團在開發(fā)流程中加入了代碼檢查,如果代碼檢測到問題,則無法合并到正式分支中,這些檢查中就包括Lint。
我們在實際使用Lint中遇到了以下問題:
- 原生Lint無法滿足我們團隊特有的需求,例如:編碼規(guī)范。 - 原生Lint存在一些檢測缺陷或者缺少一些我們認為有必要的檢測。
基于上面的考慮,我們開始調(diào)研并開發(fā)自定義Lint。
在介紹美團的實踐之前,先用一個小例子,來看看如何進行自定義Lint。
示例介紹
開發(fā)中我們希望開發(fā)者使用RoboGuice的Ln替代Log/System.out.println。
Ln相比于Log有以下優(yōu)勢:
- 對于正式發(fā)布包來說,debug和verbose的日志會自動不顯示。
- 擁有更多的有用信息,包括應用程序名字、日志的文件和行信息、時間戳、線程等。
- 由于使用了可變參數(shù),禁用后日志的性能比Log高。因為最冗長的日志往往都是debug或verbose日志,這可以稍微提高一些性能。
- 可以覆蓋日志的寫入位置和格式。
這里我們以此為例,讓Lint檢查代碼中Log/System.out.println的調(diào)用,提醒開發(fā)者使用Ln。
創(chuàng)建Java工程,配置Gradle
apply plugin: 'java'dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.android.tools.lint:lint-api:24.5.0'compile 'com.android.tools.lint:lint-checks:24.5.0' }注:
- lint-api: 官方給出的API,API并不是最終版,官方提醒隨時有可能會更改API接口。 - lint-checks:已有的檢查。
創(chuàng)建Detector
Detector負責掃描代碼,發(fā)現(xiàn)問題并報告。
/*** 避免使用Log / System.out.println ,提醒使用Ln** RoboGuice's Ln logger is similar to Log, but has the following advantages:* - Debug and verbose logging are automatically disabled for release builds.* - Your app name, file and line of the log message, time stamp, thread, and other useful information is automatically logged for you. (Some of this information is disabled for release builds to improve performance).* - Performance of disabled logging is faster than Log due to the use of the varargs. Since your most expensive logging will often be debug or verbose logging, this can lead to a minor performance win.* - You can override where the logs are written to and the format of the logging.* * https://github.com/roboguice/roboguice/wiki/Logging-via-Ln** Created by chentong on 18/9/15.*/ public class LogDetector extends Detector implements Detector.JavaScanner{public static final Issue ISSUE = Issue.create("LogUse","避免使用Log/System.out.println","使用Ln,防止在正式包打印log",Category.SECURITY, 5, Severity.ERROR,new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));@Overridepublic List<Class<? extends Node>> getApplicableNodeTypes() {return Collections.<Class<? extends Node>>singletonList(MethodInvocation.class);}@Overridepublic AstVisitor createJavaVisitor(final JavaContext context) {return new ForwardingAstVisitor() {@Overridepublic boolean visitMethodInvocation(MethodInvocation node) {if (node.toString().startsWith("System.out.println")) {context.report(ISSUE, node, context.getLocation(node),"請使用Ln,避免使用System.out.println");return true;}JavaParser.ResolvedNode resolve = context.resolve(node);if (resolve instanceof JavaParser.ResolvedMethod) {JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolve;// 方法所在的類校驗JavaParser.ResolvedClass containingClass = method.getContainingClass();if (containingClass.matches("android.util.Log")) {context.report(ISSUE, node, context.getLocation(node),"請使用Ln,避免使用Log");return true;}}return super.visitMethodInvocation(node);}};} }可以看到這個Detector繼承Detector類,然后實現(xiàn)Scanner接口。
自定義Detector可以實現(xiàn)一個或多個Scanner接口,選擇實現(xiàn)哪種接口取決于你想要的掃描范圍
- Detector.XmlScanner - Detector.JavaScanner - Detector.ClassScanner - Detector.BinaryResourceScanner - Detector.ResourceFolderScanner - Detector.GradleScanner - Detector.OtherFileScanner
這里因為我們是要針對Java代碼掃描,所以選擇使用JavaScanner。
代碼中g(shù)etApplicableNodeTypes方法決定了什么樣的類型能夠被檢測到。這里我們想看Log以及println的方法調(diào)用,選取MethodInvocation。對應的,我們在createJavaVisitor創(chuàng)建一個ForwardingAstVisitor通過visitMethodInvocation方法來接收被檢測到的Node。
可以看到getApplicableNodeTypes返回值是一個List,也就是說可以同時檢測多種類型的節(jié)點來幫助精確定位到代碼,對應的ForwardingAstVisitor接受返回值進行邏輯判斷就可以了。
可以看到JavaScanner中還有其他很多方法,getApplicableMethodNames(指定方法名)、visitMethod(接收檢測到的方法),這種對于直接找尋方法名的場景會更方便。當然這種場景我們用最基礎(chǔ)的方式也可以完成,只是比較繁瑣。
那么其他Scanner如何去寫呢?
可以去查看各接口中的方法去實現(xiàn),一般都是有這兩種對應:什么樣的類型需要返回、接收發(fā)現(xiàn)的類型。
這里插一句,Lint是如何實現(xiàn)Java掃描分析的呢?Lint使用了Lombok做抽象語法樹的分析。所以在我們告訴它需要什么類型后,它就會把相應的Node返回給我們。
回到示例,當接收到返回的Node之后需要進行判斷,如果調(diào)用方法是System.out.println或者屬于android.util.Log類,則調(diào)用context.report上報。
context.report(ISSUE, node, context.getLocation(node), "請使用Ln,避免使用Log");第一個參數(shù)是Issue,這個之后會講到; 第二個參數(shù)是當前節(jié)點; 第三個參數(shù)location會返回當前的位置信息,便于在報告中顯示定位;
最后的字符串用來為警告添加解釋。對應報告中的位置如下圖:
這里還需要說明report會自動處理被suppress(suppressLint)/ignore(tools:ignore)的警告。所以發(fā)現(xiàn)問題直接調(diào)用report就可以,不用擔心其他問題。
Issue
Issue由Detector發(fā)現(xiàn)并報告,是Android程序代碼可能存在的bug。
public static final Issue ISSUE = Issue.create("LogUse","避免使用Log/System.out.println","使用Ln,防止在正式包打印log",Category.SECURITY, 5, Severity.ERROR,new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));聲明為final class,由靜態(tài)工廠方法創(chuàng)建。對應參數(shù)解釋如下:
- id : 唯一值,應該能簡短描述當前問題。利用Java注解或者XML屬性進行屏蔽時,使用的就是這個id。
- summary : 簡短的總結(jié),通常5-6個字符,描述問題而不是修復措施。
- explanation : 完整的問題解釋和修復建議。
- category : 問題類別。詳見下文詳述部分。
- priority : 優(yōu)先級。1-10的數(shù)字,10為最重要/最嚴重。
- severity : 嚴重級別:Fatal, Error, Warning, Informational, Ignore。
- Implementation : 為Issue和Detector提供映射關(guān)系,Detector就是當前Detector。聲明掃描檢測的范圍Scope,Scope用來描述Detector需要分析時需要考慮的文件集,包括:Resource文件或目錄、Java文件、Class文件。
與Lint HTML報告對應關(guān)系
Category詳述
系統(tǒng)現(xiàn)在已有的類別如下:
- Lint
- Correctness (incl. Messages)
- Security
- Performance
- Usability (incl. Icons, Typography)
- Accessibility
- Internationalization
- Bi-directional text
自定義Category
public class MTCategory {public static final Category NAMING_CONVENTION = Category.create("命名規(guī)范", 101); }使用
public static final Issue ISSUE = Issue.create("IntentExtraKey","intent extra key 命名不規(guī)范","請在接受此參數(shù)中的Activity中定義一個按照EXTRA_<name>格式命名的常量",MTCategory.NAMING_CONVENTION , 5, Severity.ERROR,new Implementation(IntentExtraKeyDetector.class, Scope.JAVA_FILE_SCOPE));IssueRegistry
提供需要被檢測的Issue列表
public class MTIssueRegistry extends IssueRegistry {@Overridepublic synchronized List<Issue> getIssues() {System.out.println("==== MT lint start ====");return Arrays.asList(DuplicatedActivityIntentFilterDetector.ISSUE,//IntentExtraKeyDetector.ISSUE,//FragmentArgumentsKeyDetector.ISSUE,LogDetector.ISSUE,PrivateModeDetector.ISSUE,WebViewSafeDetector.ON_RECEIVED_SSL_ERROR,WebViewSafeDetector.SET_SAVE_PASSWORD,WebViewSafeDetector.SET_ALLOW_FILE_ACCESS,WebViewSafeDetector.WEB_VIEW_USE,HashMapForJDK7Detector.ISSUE);} }在getIssues()方法中返回需要被檢測的Issue List。
在build.grade中聲明Lint-Registry屬性
jar {manifest {attributes("Lint-Registry": "com.meituan.android.lint.core.MTIssueRegistry")} }至此,自定義Lint的編碼部分就完成了。
之前提到自定義Lint是一個Java工程,那么打出的jar包如何使用呢?
jar包使用
Google方案
將jar拷貝到~/.android/lint中
$ mkdir ~/.android/lint/ $ cp customrule.jar ~/.android/lint/缺點:針對所有工程,會影響同一臺機器其他工程的Lint檢查。即便觸發(fā)工程時拷貝過去,執(zhí)行完刪除,但其他進程或線程使用./gradlew lint仍可能會受到影響。
LinkedIn方案
LinkedIn提供了另一種思路 : 將jar放到一個aar中。這樣我們就可以針對工程進行自定義Lint,lint.jar只對當前工程有效。
詳細介紹請看LinkedIn博客: Writing Custom Lint Checks with Gradle。
我們對此方案進行調(diào)研,得出以下結(jié)論:
可行性
AAR Format 中寫明可以有l(wèi)int.jar。
從Google Groups adt-dev論壇討論來看是官方目前的推薦方案,詳見:Specify custom lint JAR outside of lint tools settings directory
測試后發(fā)現(xiàn)aar中有l(wèi)int.jar ,最終APK中并不會引起包體積變化。
缺點
官方plugin偶爾出bug,給人一種不太重視的感覺。
目前plugin的支持情況是:1.1.x正常,1.2.x不支持,1.3.x修復問題,1.5.x正常。
1.2.x Gradle plugin遇到的兩個問題:
- Issue 174808:custom lint in AAR doesn’t work - Issue 178699:lint.jar in AAR doesn’t work sometimes
經(jīng)過對比,我們最終選擇了LinkedIn的方案。
在確定方案后,我們?yōu)長int增加了很多功能,包括編碼規(guī)范和原生Lint增強。這里以HashMap檢測為例,介紹一下美團Lint。
增強HashMap檢測
Lint檢測中有一項是Java性能檢測,常見的就是:HashMap can be replaced with SparseArray。
public static void testHashMap() {HashMap<Integer, String> map1 = new HashMap<Integer, String>();map1.put(1, "name");HashMap<Integer, String> map2 = new HashMap<>();map2.put(1, "name");Map<Integer, String> map3 = new HashMap<>();map3.put(1, "name"); }對于上述代碼,原生Lint只能檢測第一種情況,JDK 7泛型新寫法還檢測不到。
了解到這點之后,我們決定為HashMap提供增強檢測。
分析源碼后發(fā)現(xiàn),HashMap檢測是根據(jù)new HashMap處的泛型來判斷是否符合條件。 于是我們想到,在發(fā)現(xiàn)new HashMap后去找前面的泛型,因為本身Java就是靠類型推斷的,我們可以直接根據(jù)前面的泛型來確定是否使用SparseArray。當然,是不是HashMap還需要通過后面的new HashMap來判斷,否則容易出現(xiàn)問題。
代碼如下:
@Override public List<Class<? extends Node>> getApplicableNodeTypes() {return Collections.<Class<? extends Node>>singletonList(ConstructorInvocation.class); }private static final String INTEGER = "Integer"; //$NON-NLS-1$ private static final String BOOLEAN = "Boolean"; //$NON-NLS-1$ private static final String BYTE = "Byte"; //$NON-NLS-1$ private static final String LONG = "Long"; //$NON-NLS-1$ private static final String HASH_MAP = "HashMap"; //$NON-NLS-1$@Override public AstVisitor createJavaVisitor(@NonNull JavaContext context) {return new ForwardingAstVisitor() {@Overridepublic boolean visitConstructorInvocation(ConstructorInvocation node) {TypeReference reference = node.astTypeReference();String typeName = reference.astParts().last().astIdentifier().astValue();// TODO: Should we handle factory method constructions of HashMaps as well,// e.g. via Guava? This is a bit trickier since we need to infer the type// arguments from the calling context.if (typeName.equals(HASH_MAP)) {checkHashMap(context, node, reference);}return super.visitConstructorInvocation(node);}}; }/*** Checks whether the given constructor call and type reference refers* to a HashMap constructor call that is eligible for replacement by a* SparseArray call instead*/ private void checkHashMap(JavaContext context, ConstructorInvocation node, TypeReference reference) {StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments();if (types == null || types.size() != 2) {/*JDK 7 新寫法HashMap<Integer, String> map2 = new HashMap<>();map2.put(1, "name");Map<Integer, String> map3 = new HashMap<>();map3.put(1, "name");*/Node variableDefinition = node.getParent().getParent();if (variableDefinition instanceof VariableDefinition) {TypeReference typeReference = ((VariableDefinition) variableDefinition).astTypeReference();checkCore(context, variableDefinition, typeReference);// 此方法即原HashMap檢測邏輯}}// else --> lint本身已經(jīng)檢測 }代碼很簡單,總體就是獲取變量定義的地方,將泛型值傳入原先的檢測邏輯。
當然這里的增強也是有局限的,比如這個變量是成員變量,向前的推斷就會有問題,這點我們還在持續(xù)的優(yōu)化中。
總結(jié)一下實踐過程中的技巧:
- 因為沒有好的文檔,我們更多地是要從源碼的檢測中學習,多看lint-checks。
- 需要的時候使用SdkConstants,充分利用LintUtils,Lint給我們提供了很多方便的工具。
為自定義Lint開發(fā)plugin
aar雖然很方便,但是在團隊內(nèi)部推廣中我們遇到了以下問題:
- 配置繁瑣,不易推廣。每個庫都需要自行配置lint.xml、lintOptions,并且compile aar。 - 不易統(tǒng)一。各庫之間需要使用相同的配置,保證代碼質(zhì)量。但現(xiàn)在手動來回拷貝規(guī)則,且配置文件可以自己修改。
于是我們想到開發(fā)一個plugin,統(tǒng)一管理lint.xml和lintOptions,自動添加aar。
統(tǒng)一lint.xml
我們在plugin中內(nèi)置lint.xml,執(zhí)行前拷貝過去,執(zhí)行完成后刪除。
lintTask.doFirst {if (lintFile.exists()) {lintOldFile = project.file("lintOld.xml")lintFile.renameTo(lintOldFile)}def isLintXmlReady = copyLintXml(project, lintFile)if (!isLintXmlReady) {if (lintOldFile != null) {lintOldFile.renameTo(lintFile)}throw new GradleException("lint.xml不存在")}}project.gradle.taskGraph.afterTask { task, TaskState state ->if (task == lintTask) {lintFile.delete()if (lintOldFile != null) {lintOldFile.renameTo(lintFile)}} }統(tǒng)一lintOptions
Android plugin在1.3以后允許我們替換Lint Task的lintOptions:
def newOptions = new LintOptions() newOptions.lintConfig = lintFile newOptions.warningsAsErrors = true newOptions.abortOnError = true newOptions.htmlReport = true //不放在build下,防止被clean掉 newOptions.htmlOutput = project.file("${project.projectDir}/lint-report/lint-report.html") newOptions.xmlReport = falselintTask.lintOptions = newOptions自動添加最新aar
這里還涉及一個問題:當我們plugin開發(fā)完成提供給團隊使用的時候,假設(shè)我們需要修改lint aar,那么團隊的plugin就要統(tǒng)一升級。這點就比較繁瑣。
考慮到plugin只是一個檢查代碼插件,它最需要的應該是實時更新。 我們引入了Gradle Dynamic Versions:
plugin開發(fā)完成,就可以提供給團隊部署了。
當然為了團隊更方便地接入檢查,我們在檢查流程中內(nèi)置了腳本來自動添加plugin,這樣團隊就可以在不添加任何代碼的情況下,實現(xiàn)自定義Lint檢查。
- Google. Writing Custom Lint Rules. Android Tools Project Site.
- Google. Writing a Lint Check. Android Tools Project Site.
- Prengemann M. The Power of Custom Lint Checks. SpeakerDeck.
- Diermann A. Custom Lint Rules. GitHub.
- Diermann A. Android Lint API Reference Guide. GitHub.
- Google. Android Custom Lint Rules Sample Code. GitHub.
- Cheng Yang. Writing Custom Lint Checks with Gradle. LinkedIn.
總結(jié)
以上是生活随笔為你收集整理的Android自定义Lint实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud构建微服务架构:
- 下一篇: Android官方开发文档Trainin