0
点赞
收藏
分享

微信扫一扫

android 自定义Lint


概述

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

说明:
自定义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会返回当前的位置信息,便于在报告中显示定位;

android 自定义Lint_android

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

android 自定义Lint_程序员_02

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

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

自动添加最新aar

考虑到plugin只是一个检查代码插件,它最需要的应该是实时更新。当 我们引入了Gradle Dynamic Versions,就可以做到实时更新了:

project.dependencies {
compile 'com.meituan.android.lint:lint:latest.integration'

project.configurations.all {
resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'

注:文章来自于美团移动团队


举报

相关推荐

0 条评论