概述
Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。
为什么要自定义
我们在实际使用Lint中遇到了以下问题:
- 原生Lint无法满足我们团队特有的需求,例如:编码规范。
- 原生Lint存在一些检测缺陷或者缺少一些我们认为有必要的检测。
- 对于正式发布包来说,debug和verbose的日志会自动不显示。
基于上面的考虑,我们开始调研并开发自定义Lint。开发中我们希望开发者使用RoboGuice的Ln替代Log/System.out.println。
相比原生的lint,Ln具有以下优势:
- 拥有更多的有用信息,包括应用程序名字、日志的文件和行信息、时间戳、线程等。
- 由于使用了可变参数,禁用后日志的性能比Log高。因为最冗长的日志往往都是debug或verbose日志,这可以稍微提高一些性能。
- 可以覆盖日志的写入位置和格式。
示例代码:
首先需要配置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接口。
创建Detector。
Detector负责扫描代码,发现问题并报告。
/**
* 避免使用Log / System.out.println ,提醒使用Ln
* https://github.com/roboguice/roboguice/wiki/Logging-via-Ln
*/
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));
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
return Collections.<Class<? extends Node>>singletonList(MethodInvocation.class);
}
@Override
public AstVisitor createJavaVisitor(final JavaContext context) {
return new ForwardingAstVisitor() {
@Override
public 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可以实现一个或多个Scanner接口,选择实现哪种接口取决于你想要的扫描范围。
Detector.XmlScanner
Detector.JavaScanner
Detector.ClassScanner
Detector.BinaryResourceScanner
Detector.ResourceFolderScanner
Detector.GradleScanner
Detector.OtherFileScanner
这里我们主要针对的是Java代码,所以我们选取JavaScanner。具体的实现逻辑:
代码中getApplicableNodeTypes方法决定了什么样的类型能够被检测到。这里我们想看Log以及println的方法调用,选取MethodInvocation。对应的,我们在createJavaVisitor创建一个ForwardingAstVisitor通过visitMethodInvocation方法来接收被检测到的Node。
可以看到getApplicableNodeTypes返回值是一个List,也就是说可以同时检测多种类型的节点来帮助精确定位到代码,对应的ForwardingAstVisitor接受返回值进行逻辑判断就可以了。
可以看到JavaScanner中还有其他很多方法,getApplicableMethodNames(指定方法名)、visitMethod(接收检测到的方法),这种对于直接找寻方法名的场景会更方便。当然这种场景我们用最基础的方式也可以完成,只是比较繁琐。
注:Lint是如何实现Java扫描分析的呢?Lint使用了Lombok做抽象语法树的分析。所以在我们告诉它需要什么类型后,它就会把相应的Node返回给我们。
当接收到返回的Node之后需要进行判断,如果调用方法是System.out.println或者属于android.util.Log类,则调用context.report上报。即调用了下面代码:
context.report(ISSUE, node, context.getLocation(node), "请使用Ln,避免使用Log");
说明:第一个参数是Issue;第二个参数是当前节点;第三个参数location会返回当前的位置信息,便于在报告中显示定位;
Issue
Issue由Detector发现并报告,是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));
Category
系统已有类别:
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("命名规范", 101);
}
然后在ISSUE引用。
public static final Issue ISSUE = Issue.create(
"IntentExtraKey",
"intent extra key 命名不规范",
"请在接受此参数中的Activity中定义一个按照EXTRA_<name>格式命名的常量",
MTCategory.NAMING_CONVENTION , 5, Severity.ERROR,
new Implementation(IntentExtraKeyDetector.class, Scope.JAVA_FILE_SCOPE));
IssueRegistry
提供需要被检测的Issue列表,形如:
public class MTIssueRegistry extends IssueRegistry {
@Override
public 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属性。
<div class="se-preview-section-delimiter"></div>
jar {
manifest {
attributes(“Lint-Registry”: “com.meituan.android.lint.core.MTIssueRegistry”)
}
}
“`
jar {
manifest {
attributes("Lint-Registry": "com.meituan.android.lint.core.MTIssueRegistry")
}
}
至此,代码上的逻辑就编写完成了,接下来是如何打包给集成方使用了。
jar包使用
将我们自定义的lint.jar完成后,我们接下来就是如何使用jar的问题了。
Google方案
将jar拷贝到~/.android/lint中,然后挺好默认的lint即可:
$ mkdir ~/.android/lint/
$ cp customrule.jar ~/.android/lint/
LinkedIn方案
LinkedIn提供了另一种思路 : 将jar放到一个aar中。这样我们就可以针对工程进行自定义Lint,lint.jar只对当前工程有效。
详细介绍请看LinkedIn博客: Writing Custom Lint Checks with Gradle。
可行性
AAR Format 中写明可以有lint.jar。
从Google Groups adt-dev论坛讨论来看是官方目前的推荐方案,详见:Specify custom lint JAR outside of lint tools settings directory
测试后发现aar中有lint.jar ,最终APK中并不会引起包体积变化。
所以我们选择LinkedIn方案。方案选定后,我们怎么实践呢?
LinkedIn实践
在确定方案后,我们为Lint增加了很多功能,包括编码规范和原生Lint增强。这里以HashMap检测为例,介绍一下Lint。
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做Lint检查。
分析源码后发现,HashMap检测是根据new HashMap处的泛型来判断是否符合条件。于是我们想到,在发现new HashMap后去找前面的泛型,因为本身Java就是靠类型推断的,我们可以直接根据前面的泛型来确定是否使用SparseArray。
所以,对于增强HashMap检测我们可以采用以下的方式:
@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() {
@Override
public 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本身已经检测
}
为自定义Lint开发plugin
aar虽然很方便,但是在团队内部推广中我们遇到了以下问题:
- 配置繁琐,不易推广。每个库都需要自行配置lint.xml、lintOptions,并且compile aar。
- 不易统一。各库之间需要使用相同的配置,保证代码质量。但现在手动来回拷贝规则,且配置文件可以自己修改。
于是我们想到开发一个plugin,统一管理lint.xml和lintOptions,自动添加aar。
统一lint.xml
我们在plugin中内置lint.xml,执行前拷贝过去,执行完成后删除。
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)
}
}
}
统一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 = false
lintTask.lintOptions = newOptions
自动添加最新aar
考虑到plugin只是一个检查代码插件,它最需要的应该是实时更新。当 我们引入了Gradle Dynamic Versions,就可以做到实时更新了:
project.dependencies {
compile 'com.meituan.android.lint:lint:latest.integration'
}
project.configurations.all {
resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
}
注:文章来自于美团移动团队