Android自定义Lint实践
Android Lint是Google提供給Android開(kāi)發(fā)者的靜態(tài)代碼檢查工具。使用Lint對(duì)Android工程代碼進(jìn)行掃描和檢查,可以發(fā)現(xiàn)代碼潛在的問(wèn)題,提醒程序員及早修正。
為保證代碼質(zhì)量,美團(tuán)在開(kāi)發(fā)流程中加入了代碼檢查,如果代碼檢測(cè)到問(wèn)題,則無(wú)法合并到正式分支中,這些檢查中就包括Lint。
我們?cè)趯?shí)際使用Lint中遇到了以下問(wèn)題:
- 原生Lint無(wú)法滿足我們團(tuán)隊(duì)特有的需求,例如:編碼規(guī)范。 - 原生Lint存在一些檢測(cè)缺陷或者缺少一些我們認(rèn)為有必要的檢測(cè)。
基于上面的考慮,我們開(kāi)始調(diào)研并開(kāi)發(fā)自定義Lint。
在介紹美團(tuán)的實(shí)踐之前,先用一個(gè)小例子,來(lái)看看如何進(jìn)行自定義Lint。
示例介紹
開(kāi)發(fā)中我們希望開(kāi)發(fā)者使用RoboGuice的Ln替代Log/System.out.println。
Ln相比于Log有以下優(yōu)勢(shì):
- 對(duì)于正式發(fā)布包來(lái)說(shuō),debug和verbose的日志會(huì)自動(dòng)不顯示。
- 擁有更多的有用信息,包括應(yīng)用程序名字、日志的文件和行信息、時(shí)間戳、線程等。
- 由于使用了可變參數(shù),禁用后日志的性能比Log高。因?yàn)樽钊唛L(zhǎng)的日志往往都是debug或verbose日志,這可以稍微提高一些性能。
- 可以覆蓋日志的寫入位置和格式。
這里我們以此為例,讓Lint檢查代碼中Log/System.out.println的調(diào)用,提醒開(kāi)發(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并不是最終版,官方提醒隨時(shí)有可能會(huì)更改API接口。 - lint-checks:已有的檢查。
創(chuàng)建Detector
Detector負(fù)責(zé)掃描代碼,發(fā)現(xiàn)問(wèn)題并報(bào)告。
/*** 避免使用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),"請(qǐng)使用Ln,避免使用System.out.println");return true;}JavaParser.ResolvedNode resolve = context.resolve(node);if (resolve instanceof JavaParser.ResolvedMethod) {JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolve;// 方法所在的類校驗(yàn)JavaParser.ResolvedClass containingClass = method.getContainingClass();if (containingClass.matches("android.util.Log")) {context.report(ISSUE, node, context.getLocation(node),"請(qǐng)使用Ln,避免使用Log");return true;}}return super.visitMethodInvocation(node);}};} }可以看到這個(gè)Detector繼承Detector類,然后實(shí)現(xiàn)Scanner接口。
自定義Detector可以實(shí)現(xiàn)一個(gè)或多個(gè)Scanner接口,選擇實(shí)現(xiàn)哪種接口取決于你想要的掃描范圍
- Detector.XmlScanner - Detector.JavaScanner - Detector.ClassScanner - Detector.BinaryResourceScanner - Detector.ResourceFolderScanner - Detector.GradleScanner - Detector.OtherFileScanner
這里因?yàn)槲覀兪且槍?duì)Java代碼掃描,所以選擇使用JavaScanner。
代碼中g(shù)etApplicableNodeTypes方法決定了什么樣的類型能夠被檢測(cè)到。這里我們想看Log以及println的方法調(diào)用,選取MethodInvocation。對(duì)應(yīng)的,我們?cè)赾reateJavaVisitor創(chuàng)建一個(gè)ForwardingAstVisitor通過(guò)visitMethodInvocation方法來(lái)接收被檢測(cè)到的Node。
可以看到getApplicableNodeTypes返回值是一個(gè)List,也就是說(shuō)可以同時(shí)檢測(cè)多種類型的節(jié)點(diǎn)來(lái)幫助精確定位到代碼,對(duì)應(yīng)的ForwardingAstVisitor接受返回值進(jìn)行邏輯判斷就可以了。
可以看到JavaScanner中還有其他很多方法,getApplicableMethodNames(指定方法名)、visitMethod(接收檢測(cè)到的方法),這種對(duì)于直接找尋方法名的場(chǎng)景會(huì)更方便。當(dāng)然這種場(chǎng)景我們用最基礎(chǔ)的方式也可以完成,只是比較繁瑣。
那么其他Scanner如何去寫呢?
可以去查看各接口中的方法去實(shí)現(xiàn),一般都是有這兩種對(duì)應(yīng):什么樣的類型需要返回、接收發(fā)現(xiàn)的類型。
這里插一句,Lint是如何實(shí)現(xiàn)Java掃描分析的呢?Lint使用了Lombok做抽象語(yǔ)法樹的分析。所以在我們告訴它需要什么類型后,它就會(huì)把相應(yīng)的Node返回給我們。
回到示例,當(dāng)接收到返回的Node之后需要進(jìn)行判斷,如果調(diào)用方法是System.out.println或者屬于android.util.Log類,則調(diào)用context.report上報(bào)。
context.report(ISSUE, node, context.getLocation(node), "請(qǐng)使用Ln,避免使用Log");第一個(gè)參數(shù)是Issue,這個(gè)之后會(huì)講到; 第二個(gè)參數(shù)是當(dāng)前節(jié)點(diǎn); 第三個(gè)參數(shù)location會(huì)返回當(dāng)前的位置信息,便于在報(bào)告中顯示定位;
最后的字符串用來(lái)為警告添加解釋。對(duì)應(yīng)報(bào)告中的位置如下圖:
這里還需要說(shuō)明report會(huì)自動(dòng)處理被suppress(suppressLint)/ignore(tools:ignore)的警告。所以發(fā)現(xiàn)問(wèn)題直接調(diào)用report就可以,不用擔(dān)心其他問(wèn)題。
Issue
Issue由Detector發(fā)現(xiàn)并報(bào)告,是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)建。對(duì)應(yīng)參數(shù)解釋如下:
- id : 唯一值,應(yīng)該能簡(jiǎn)短描述當(dāng)前問(wèn)題。利用Java注解或者XML屬性進(jìn)行屏蔽時(shí),使用的就是這個(gè)id。
- summary : 簡(jiǎn)短的總結(jié),通常5-6個(gè)字符,描述問(wèn)題而不是修復(fù)措施。
- explanation : 完整的問(wèn)題解釋和修復(fù)建議。
- category : 問(wèn)題類別。詳見(jiàn)下文詳述部分。
- priority : 優(yōu)先級(jí)。1-10的數(shù)字,10為最重要/最嚴(yán)重。
- severity : 嚴(yán)重級(jí)別:Fatal, Error, Warning, Informational, Ignore。
- Implementation : 為Issue和Detector提供映射關(guān)系,Detector就是當(dāng)前Detector。聲明掃描檢測(cè)的范圍Scope,Scope用來(lái)描述Detector需要分析時(shí)需要考慮的文件集,包括:Resource文件或目錄、Java文件、Class文件。
與Lint HTML報(bào)告對(duì)應(yīng)關(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ī)范","請(qǐng)?jiān)诮邮艽藚?shù)中的Activity中定義一個(gè)按照EXTRA_<name>格式命名的常量",MTCategory.NAMING_CONVENTION , 5, Severity.ERROR,new Implementation(IntentExtraKeyDetector.class, Scope.JAVA_FILE_SCOPE));IssueRegistry
提供需要被檢測(cè)的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()方法中返回需要被檢測(cè)的Issue List。
在build.grade中聲明Lint-Registry屬性
jar {manifest {attributes("Lint-Registry": "com.meituan.android.lint.core.MTIssueRegistry")} }至此,自定義Lint的編碼部分就完成了。
之前提到自定義Lint是一個(gè)Java工程,那么打出的jar包如何使用呢?
jar包使用
Google方案
將jar拷貝到~/.android/lint中
$ mkdir ~/.android/lint/ $ cp customrule.jar ~/.android/lint/缺點(diǎn):針對(duì)所有工程,會(huì)影響同一臺(tái)機(jī)器其他工程的Lint檢查。即便觸發(fā)工程時(shí)拷貝過(guò)去,執(zhí)行完刪除,但其他進(jìn)程或線程使用./gradlew lint仍可能會(huì)受到影響。
LinkedIn方案
LinkedIn提供了另一種思路 : 將jar放到一個(gè)aar中。這樣我們就可以針對(duì)工程進(jìn)行自定義Lint,lint.jar只對(duì)當(dāng)前工程有效。
詳細(xì)介紹請(qǐng)看LinkedIn博客: Writing Custom Lint Checks with Gradle。
我們對(duì)此方案進(jìn)行調(diào)研,得出以下結(jié)論:
可行性
AAR Format 中寫明可以有l(wèi)int.jar。
從Google Groups adt-dev論壇討論來(lái)看是官方目前的推薦方案,詳見(jiàn):Specify custom lint JAR outside of lint tools settings directory
測(cè)試后發(fā)現(xiàn)aar中有l(wèi)int.jar ,最終APK中并不會(huì)引起包體積變化。
缺點(diǎn)
官方plugin偶爾出bug,給人一種不太重視的感覺(jué)。
目前plugin的支持情況是:1.1.x正常,1.2.x不支持,1.3.x修復(fù)問(wèn)題,1.5.x正常。
1.2.x Gradle plugin遇到的兩個(gè)問(wèn)題:
- Issue 174808:custom lint in AAR doesn’t work - Issue 178699:lint.jar in AAR doesn’t work sometimes
經(jīng)過(guò)對(duì)比,我們最終選擇了LinkedIn的方案。
在確定方案后,我們?yōu)長(zhǎng)int增加了很多功能,包括編碼規(guī)范和原生Lint增強(qiáng)。這里以HashMap檢測(cè)為例,介紹一下美團(tuán)Lint。
增強(qiáng)HashMap檢測(cè)
Lint檢測(cè)中有一項(xiàng)是Java性能檢測(cè),常見(jiàn)的就是: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"); }對(duì)于上述代碼,原生Lint只能檢測(cè)第一種情況,JDK 7泛型新寫法還檢測(cè)不到。
了解到這點(diǎn)之后,我們決定為HashMap提供增強(qiáng)檢測(cè)。
分析源碼后發(fā)現(xiàn),HashMap檢測(cè)是根據(jù)new HashMap處的泛型來(lái)判斷是否符合條件。 于是我們想到,在發(fā)現(xiàn)new HashMap后去找前面的泛型,因?yàn)楸旧鞪ava就是靠類型推斷的,我們可以直接根據(jù)前面的泛型來(lái)確定是否使用SparseArray。當(dāng)然,是不是HashMap還需要通過(guò)后面的new HashMap來(lái)判斷,否則容易出現(xiàn)問(wè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檢測(cè)邏輯}}// else --> lint本身已經(jīng)檢測(cè) }代碼很簡(jiǎn)單,總體就是獲取變量定義的地方,將泛型值傳入原先的檢測(cè)邏輯。
當(dāng)然這里的增強(qiáng)也是有局限的,比如這個(gè)變量是成員變量,向前的推斷就會(huì)有問(wèn)題,這點(diǎn)我們還在持續(xù)的優(yōu)化中。
總結(jié)一下實(shí)踐過(guò)程中的技巧:
- 因?yàn)闆](méi)有好的文檔,我們更多地是要從源碼的檢測(cè)中學(xué)習(xí),多看lint-checks。
- 需要的時(shí)候使用SdkConstants,充分利用LintUtils,Lint給我們提供了很多方便的工具。
為自定義Lint開(kāi)發(fā)plugin
aar雖然很方便,但是在團(tuán)隊(duì)內(nèi)部推廣中我們遇到了以下問(wèn)題:
- 配置繁瑣,不易推廣。每個(gè)庫(kù)都需要自行配置lint.xml、lintOptions,并且compile aar。 - 不易統(tǒng)一。各庫(kù)之間需要使用相同的配置,保證代碼質(zhì)量。但現(xiàn)在手動(dòng)來(lái)回拷貝規(guī)則,且配置文件可以自己修改。
于是我們想到開(kāi)發(fā)一個(gè)plugin,統(tǒng)一管理lint.xml和lintOptions,自動(dòng)添加aar。
統(tǒng)一lint.xml
我們?cè)趐lugin中內(nèi)置lint.xml,執(zhí)行前拷貝過(guò)去,執(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自動(dòng)添加最新aar
這里還涉及一個(gè)問(wèn)題:當(dāng)我們plugin開(kāi)發(fā)完成提供給團(tuán)隊(duì)使用的時(shí)候,假設(shè)我們需要修改lint aar,那么團(tuán)隊(duì)的plugin就要統(tǒng)一升級(jí)。這點(diǎn)就比較繁瑣。
考慮到plugin只是一個(gè)檢查代碼插件,它最需要的應(yīng)該是實(shí)時(shí)更新。 我們引入了Gradle Dynamic Versions:
plugin開(kāi)發(fā)完成,就可以提供給團(tuán)隊(duì)部署了。
當(dāng)然為了團(tuán)隊(duì)更方便地接入檢查,我們?cè)跈z查流程中內(nèi)置了腳本來(lái)自動(dòng)添加plugin,這樣團(tuán)隊(duì)就可以在不添加任何代碼的情況下,實(shí)現(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实践的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
 
                            
                        - 上一篇: Spring Cloud构建微服务架构:
- 下一篇: 小夕说,不了解动态空间增长的程序喵都是假
