lint介绍
android lint是一个静态代码分析工具,通过lint工具,你可以不用边运行边调试,或者通过单元测试进行代码检查,可以检测代码中不规范、不和要求的问题,解决一些潜在的bug。lint工具可以在命令行上使用,也可以在adt中使用。
比如当想检查在manifest.xml中是否有activity,activity中是否包含了launcher activity。如果没有进行错误的警告。
通过lint的这种手段,可以对代码进行规范的控制,毕竟一个团队每个人的风格不同,但是要注意的当然是代码的质量,所以lint可以进行代码的规范和质量控制。
在Android studio还没出来时,lint和Eclipse并不能很好的结合在一起,只能作为一个独立的工具,通过命令行去执行lint检查。
在android studio出现之后,不再建议单独使用lint命令,而是结合gradle进行操作,命令为* ./gradlew lint *进行执行
lint工具通过一下六个方面去检查代码中的问题correctness, security, performance, usability, accessibility, and internationalization。检查的范围包括java文件,xml文件,class文件。
lint工具在sdk16版本之后就带有了,所以在sdk目录/tools/可以找到lint工具。现在建议与gradle一起使用,使用./gradlew lint进行
参考官方文档介绍
使用lint的方法
关于lint的一些命令,可以参考官网,这里简单介绍一些。
- lint path(项目目录) ——进行项目的lint检查
- lint –disable id(MissingTranslation,UnusedIds,Usability:Icons) path ——id是lint issue(问题)的标志,检查项目,不包括指定的issue
- lint –check id path ——利用指定的issue进行项目检查
- lint –list ——列出所有的issue
- lint –show id ——介绍指定的issue
- lint –help ——查看帮助
1.使用android studio自带的lint工具
点击Analyze的Inspect Code选项,即可开启lint检查,在Inspection窗口中可以看到lint检查的结果,lint查询的错误类型包括:
- Missing Translation and Unused Translation【缺少翻译或者没有】
- Layout Peformance problems (all the issues the old layoutopt tool used to find, and more)【布局展示问题】
- Unused resources【没有使用的资源】
- Inconsistent array sizes (when arrays are defined in multiple configurations)【不一致的数组大小】
- Accessibility and internationalization problems (hardcoded strings, missing contentDescription, etc)【可访问性和国际化问题,包括硬链接的字符串,缺少contentDescription,等等】
- Icon problems (like missing densities, duplicate icons, wrong sizes, etc)【图片问题,丢失密度,重复图片,错误尺寸等】
- Usability problems (like not specifying an input type on a text field)【使用规范,比如没有在一个文本上指定输入的类型】
- Manifest errors【Manifest.xml中的错误】
- and so on
android自带的lint规则的更改可以在Setting的Edit选项下选择Inspections(File > Settings > Project Settings),对已有的lint规则进行自定义选择。
参考官方文档
2.使用lint.xml定义检查规则
可以通过lint.xml来自定义检查规则,这里的自定义是指定义系统原有的操作,所以和第一个步骤的结果是一样的,只是可以更方便的配置。
lint.xml生效的位置是要放在项目的根目录下面,lint.xml的示例如下:
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- 忽略指定的检查 -->
<issue id="IconMissingDensityFolder" severity="ignore" />
<!-- 忽略指定文件的指定检查 -->
<issue id="ObsoleteLayoutParam">
<ignore path="res/layout/activation.xml" />
<ignore path="res/layout-xlarge/activation.xml" />
</issue>
<!-- 更改检查问题归属的严重性 -->
<issue id="HardcodedText" severity="error" />
</lint>
3.自定义lint检查规则
使用google提供的lint规则,确实能找出项目代码中不规范的地方,同时也帮助纠正了许多编译时错误。除了使用google提供的lint规则,我们当然也希望我们能够通过自定义lint规则来为自己的项目进行一个代码检测,包括代码质量把控,查找关键代码位置,或者检测代码规范,比如是否在用了流的地方关闭了流。那么如何自定义lint规则和如何将写好的规则加入已有的lint规则中,就是接下来呀研究的地方。
如何加入已有的lint规则
lint规则是基于JAVA写的,是在AST抽象语法树上去进行一个解析。所以在写lint规则的时候,要学习一下AST抽象语法树。才知道如何去寻找一个类方法和其参数等。以下有两种方法:
(1)所以自定义lint规则应该是一个写好的jar包,jar包生效的位置是在~/.android/lint目录,这个是对于Mac和Linux来说的,对于Windows来说就是在C:/Users/Administrator/.android/lint下,放到这个目录下,lint工具会自动加载这个jar包作为lint的自定义检查规则。
(2)放到lint目录下着实是一件比较麻烦的事情,即使可以用脚本来代替,但是仍然不是一个特别方便的方法。也是由于当android项目直接依赖于lint.jar包时不能起作用,而无法进行直接依赖。
而aar很好的解决了这个问题,aar能够将项目中的资源、class文件、jar文件等都包含,所以通过将lint.jar放入lintaar中,再由项目依赖于lintaar,这时候就可以达到自定义lint检查的目的。
下面就是如何使自定义lint生效的代码示例,使用第二个方法(第二个方法就包括了第一个方法):
主要包括了两个Module一个是lintJar,一个是lintAar。还有一个是测试的app项目。使用自定义lint的主要方式是利用lintAar将lintJar生成的jar包大包成aar,方便引用。所以主要是gradle的操作。
在lintJar中,主要是编写lint规则。它的gradle如下:
apply plugin: 'java'
// 依赖于lint的规则的api
dependencies {
compile 'com.android.tools.lint:lint-api:24.3.1'
compile 'com.android.tools.lint:lint-checks:24.3.1'
testCompile 'com.android.tools.lint:lint-tests:24.3.1'
}
/**
* Lint-Registry是透露给lint工具的注册类的方法,也就是PermissionIssueRegistry是lint工具的入口,同时也通过这个方法进行打jar包
*/
jar {
manifest {
attributes("Lint-Registry": "com.mogujie.PermissionIssueRegistry")
}
}
defaultTasks 'assemble'
// 定义一个方法lintJarOutput
configurations {
lintJarOutput
}
// 指定定义方法lintJarOutput的作用,此处是获得调用jar方法后的生成的jar包
dependencies {
lintJarOutput files(jar)
}
在lintAar中,gradle如下:
apply plugin: 'com.android.library'
android {
// 此处省略,太多了,就是普通的as生成的模板
}
// 依赖所有jar结尾的jar包
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
// 定义方法lintJarImport
configurations {
lintJarImport
}
// 链接到lintJar中的lintJarOutput方法,调用jar方法,并获得jar包
dependencies {
lintJarImport project(path: ':lintjar', configuration: 'lintJarOutput')
}
// 将得到的JAR包复制到目录build/intermediates/lint/下
task copyLintJar(type: Copy) {
from (configurations.lintJarImport) {
rename {
String fileName ->
'lint.jar'
}
}
into 'build/intermediates/lint/'
}
// 当项目build到compileLint这一步时执行copyLintJar方法
project.afterEvaluate {
def compileLintTask = project.tasks.find{ it.name == 'compileLint'}
compileLintTask.dependsOn(copyLintJar)
}
在app项目中,为了方便在命令窗口看到输出的信息,还需要在gradle中添加一段代码:
android {
lintOptions {
textReport true // 输出lint报告
textOutput 'stdout'
abortOnError false // 遇到错误不停止
}
}
如何编写lint规则
编写lint规则实际上是利用了java的语法树进行一个结构的遍历,是利用java语言进行开发的,使用的Google提供的lint检查api。
先设定一个问题:检查流的close方法是否在finally中被调用。
对于这个问题的想法是
1.先检查流的close方法所在的位置。
2.检查close外层的try/catch所在位置
3.检查try/catch外层的try/catch,并与finally的行数进行判断
lint规则时在libjar项目中进行编写,所以gradle需要依赖lint的相关api。
首先先看下需要定义的注册类,也就是暴露给lint的入口,这个入口需要在gradle中进行注册,请看前面的哦。IssueRegistry就是注册类,继承他,并重写getIssues的方法即可。
MyIssueRegistry.java
/**
* 把所定义的Issue进行导出,用于提供此jar中所有输出的lint规则,这个类是暴露给lint的一个注册类
*/
public class MyIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
System.out.println("***************************************************");
System.out.println("**************** lint is starting *****************");
System.out.println("***************************************************");
return Arrays.asList(
CloseDetector.ISSUE
);
}
}
Detector就是检查的工具类,通过继承Detector实现自己的检查规则
Issue就是定义的问题的描述,通过Issue可将检查结果进行反馈
Detector.JavaScanner是针对于java文件扫描的接口文件,以及一些方法。除此之外海游XmlScanner, GradleScanner, ClassScanner, BinaryResourceScanner, ResourceFolderScanner, OtherFileScanner。
由于刚刚提出的问题是关于close的,则Detector定义为CloseDetector。
CloseDetector.java
/**
* 定义代码检查规则
* 这个是针对try和catch中的finally是否包涵close方法进行一个判断
* 由于要对java代码进行扫描,因此继承的是javascanner的接口
*/
public class CloseDetector extends Detector implements Detector.JavaScanner {
public static Issue ISSUE = Issue.create(
"CloseMethod",
"close方法应该在finally中调用",
"close方法应该在finally中调用,防止导致内存泄漏",
// 这个主要是用于对问题的分类,不同的问题就可以集中在一起显示。
Category.CORRECTNESS,
// 优先级
6,
// 定义查找问题的严重级别
Severity.ERROR,
// 提供处理该问题的Detector和该Detector所关心的资源范围。当系统生成了抽象语法树(Abstract syntax tree,简称AST),或者遍历xml资源时,就会调用对应Issue的处理器Detector。
new Implementation(CloseDetector.class,
Scope.JAVA_FILE_SCOPE)
);
// 限定关心的方法的调用类
public static final String[] sSupportSuperType = new String[]{
"java.io.InputStream", "java.io.OutputStream", "android.database.Cursor"
};
/**
* 只关心名是close的方法
*
* @return
*/
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("close");
}
/**
* 该方法调用时,会传入代表close方法被调用的节点MethodInvocation,以及所在java文件的上下文JavaContext,
* 还有AstVisitor。由于我们没有重写createJavaVisitor方法,所以不用管AstVisitor。
* MethodInvocation封装了close被调用处的代码,而结合JavaContext对象,即可寻找对应的上下文,来帮助我们判断条件。
*
* @param context
* @param visitor
* @param node
*/
@Override
public void visitMethod(JavaContext context, AstVisitor visitor, MethodInvocation node) {
// 判断类型,看下所监测的资源是否是我们定义的相关资源
// 通过JavaContext的resolve的方法,传入node节点,由于所有的AST树上的节点都继承自NODE,所以可以通过node去找到class
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) context.resolve(node);
JavaParser.ResolvedClass clzz = method.getContainingClass();
boolean isSubClass = false;
for (int i = 0; i < sSupportSuperType.length; i++) {
if (clzz.isSubclassOf(sSupportSuperType[i], false)) {
isSubClass = true;
break;
}
}
if (!isSubClass) super.visitMethod(context, visitor, node);
/**
* 查找try和block的信息
* 在AST中,close 代码节点应该是try的一个子孙节点(try是语法上的block),所以从close代码节点向上追溯,
* 可以找到对应的try,而Node对象本来就有getParent方法,所以可以递归调用该方法来找到Try节点(这也是一个节点),
* 或者调用JavaContext的查找限定parent类型的方法:
*/
Try fTryBlock = context.getParentOfType(node, Try.class);
int fLineNum = context.getLocation(fTryBlock).getStart().getLine();
System.out.println(" fLineNum=" + fLineNum);
/**
* 如果close在try模块中,接着就要对try进行向上查找,看try是否被包裹在try中,同时,是否处于finally中,try节点
* 有一个astFinally的方法,可以得到finally的节点,只要判断节点的位置,既可以实现判断close是否在finally中
*/
Try sTryBlock = context.getParentOfType(fTryBlock, Try.class);
Block finaBlock = sTryBlock.astFinally();
int sLineNum = context.getLocation(finaBlock).getStart().getLine();
System.out.println(" sLineNum=" + sLineNum);
/**
* 若我们确定了close是在try 块中,且try块不在finally里,那么就需要触发Issue,这样在html报
* 告中就可以找到对应的信息了
* 一个莫名的bug,不能再这里写成
* if (fLineNum < sLineNum){
* context.report(ISSUE, node, context.getLocation(node), "请在finally中调用close");
* }
* 否则再直接运行Analyze下Inspect选项后,在inspection窗口中没有办法看到Error from custom lint Check
* 的错误信息
*/
if (fLineNum > sLineNum) {
return;
} else {
context.report(ISSUE, node, context.getLocation(node), "please use close method in finally");
}
}
}
通过短短的示例学习一下lint规则的编写。由于网上关于lint规则的编写还是比较少的,所以要想了解lint规则,还需要多研究下google的源代码,从源代码中进行学习。
注意
在使用android studio中的lint检查的工具的时候,有时候需要退出,然后重新启动之后,我门加入的lint.aar才会在系统中生效。
同时也可以在命令窗口上实时调试,请看文中所述。