静态代码检查
1. Lint
简介:Android Lint是SDK Tools 16开始引入的一个代码扫描工具,路径:~.\Sdk\tools\bin\link.bat,studio默认支持。 通过Lint,无需实际执行应用,也不必编写测试用例,就可以检查Android项目中源文件是否存潜在的错误,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。
- correctness(正确性)eg:id唯一,废弃的api,未定义的参数。
- usability(可用性)eg:重复的drawable,imageview使用gif。
- security(安全性)eg:provider权限,android:debuggable模式。
- accessibility(可访问性)eg:ImageView和ImageButton应该提供contentDescription。
- performance(性能)eg:避免ondraw()实例化Paint,无用参数和方法提示。
- i18n(国际化)eg:中文硬编码,日期格式不符当前区域。
过程:
- 读取源代码:Android Lint首先读取Android项目中的源代码文件,包括Java和XML文件。
- 编译字节码:将读取的源代码编译成字节码,这个过程是由Java编译器完成的。
- 扫描字节码:Android Lint使用一组预定义的规则来扫描编译后的字节码,查找潜在的问题。
- 标记问题:如果发现潜在的问题,Lint会将其标记出来,并生成一个报告。
- 生成修复建议:Lint还会为每个标记的问题生成一份修复建议,以帮助开发者修复这些问题。
developer.android.google.cn/studio/writ…
1.1 使用规则
1.1.1 基础规则
在每个module下的build.gradle中配置lint属性,根据需求,选择某几项即可。
android {
lint {
// true--关闭lint报告的分析进度
quiet true
// true--错误发生后停止gradle构建
abortOnError false
// true--只报告error
ignoreWarnings true
// true--忽略有错误的文件的全/绝对路径(默认是true)
//absolutePaths true
// true--检查所有问题点,包含其他默认关闭项
checkAllWarnings true
// true--所有warning当做error
warningsAsErrors true
// 关闭指定问题检查
disable 'TypographyFractions', 'TypographyQuotes'
// 打开指定问题检查
enable 'RtlHardcoded', 'RtlCompat', 'RtlEnabled'
// 仅检查指定问题
check 'NewApi', 'InlinedApi'
// true--error输出文件不包含源码行号
noLines true
// true--显示错误的所有发生位置,不截取
showAll true
// 回退lint设置(默认规则)
lintConfig file("default-lint.xml")
// true--生成txt格式报告(默认false)
textReport true
// 重定向输出;可以是文件或'stdout'
textOutput 'stdout'
// true--生成XML格式报告
xmlReport false
// 指定xml报告文档(默认build/reports/lint-results.xml)
xmlOutput file("lint-report.xml")
// true--生成HTML报告(带问题解释,源码位置,等)
htmlReport true
// html报告可选路径(构建器默认是build/reports/lint-results.html )
htmlOutput file("lint-report.html")
// true--所有正式版构建执行规则生成崩溃的lint检查,如果有崩溃问题将停止构建
checkReleaseBuilds true
// 在发布版本编译时检查(即使不包含lint目标),指定问题的规则生成崩溃
fatal 'NewApi', 'InlineApi'
// 指定问题的规则生成错误
error 'Wakelock', 'TextViewEdits'
// 指定问题的规则生成警告
warning 'ResourceAsColor'
// 忽略指定问题的规则(同关闭检查)
ignore 'TypographyQuotes'
// 运行 lint 会将所有当前问题记录在 lint-baseline.xml 文件中。
// 基准快照,先使用 lint 让构建失败,而不必先返回并解决所有现有问题。
baseline file("lint-baseline.xml")
}
}
1.1.2 部分属性解析
-
abortOnError false,建议false,设置为true表示错误发生即停止构建。
-
lintConfig file("default-lint.xml"),基础属性的详细配置,放在module根目录下。
指定 lint 检查偏好设置,lint.xml 文件由封闭的 <lint> 父标记组成,此标记包含一个或多个 <issue> 子元素。lint 会为每个 <issue> 指定一个唯一的 id 属性值。
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- icon空资源文件夹忽略 -->
<issue id="IconMissingDensityFolder" severity="ignore" />
<!-- 忽略ObsoleteLayoutParam组下的特殊文件 -->
<issue id="ObsoleteLayoutParam">
<ignore path="res/layout/activation.xml" />
<ignore path="res/layout-xlarge/activation.xml" />
</issue>
<!-- 忽略UselessLeaf组下的文件 -->
<issue id="UselessLeaf">
<ignore path="res/layout/main.xml" />
</issue>
<!-- 设置硬编码 警告级别error -->
<issue id="HardcodedText" severity="error" />
</lint>
-
baseline file("lint-baseline.xml"),基准文件,存在的情况下可增量查询。
- 运行
lint会将所有当前问题记录在lint-baseline.xml文件中。 - 基准快照,先使用 lint 让构建失败,而不必先返回并解决所有现有问题。
- 运行
-
checkOnly += [],筛选只进行检测的id,eg:'UselessLeaf','HardcodedText'
1.1.3 自定义规则
背景:基础规则只适用于系统认定的非合理行为,项目中情况复杂,需要对基础规则进行拓展。
自定义 Lint 规则最终都会打成 JAR 包,只需将该输出 JAR 提供给其他组件使用即可。目前有两种方式可供选择:
- 全局方案
把此 jar 拷贝到 ~/.android/lint/ 目录中即可。缺点显而易见:针对所有工程生效,会影响同一台机器其他工程的 Lint 检查。即便触发工程时拷贝过去,执行完删除,但其他进程或线程使用 ./gradlew lint 仍可能会受到影响。
- aar方案
Lint.jar 置于一个 aar 中,如果某个工程想要接入执行自定义的 lint 规则,只需依赖这个发布后的 aar 即可,如此一来,新增的 lint 规则就可将影响范围控制在单个项目内了。该方案也是目前各厂商常用的方法。
2. .jar及.aar的生成和使用
2.1 jar包的生成
2.1.1 创建 java-library & 配置 lint 依赖
a. project -> new module(name: checks)
b. 配置build.gradle如下(本文version:31.1.3):
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compileOnly "com.android.tools.lint:lint-api:$lint_version"
compileOnly "com.android.tools.lint:lint-checks:$lint_version"
testImplementation "junit:junit:4.12"
testImplementation "com.android.tools.lint:lint:$lint_version"
testImplementation "com.android.tools.lint:lint-tests:$lint_version"
testImplementation "com.android.tools:testutils:$lint_version"
}
jar {
manifest {
// Only use the "-v2" key here if your checks have been updated to the
// new 3.0 APIs (including UAST)
attributes("Lint-Registry-v2": "com.autoai.checks.MyIssueRegistry")
}
}
2.1.2 自定义Registry
需要自定义一个 Registry 声明自己需要检测的 Issues,表示启用的Lint规则列表。eg:
public class MyIssueRegistry extends IssueRegistry {
@Override
public synchronized List<Issue> getIssues() {
System.out.println("MyIssueRegistry ==== start ====");
return Arrays.asList(NewThreadDetector.ISSUE);
}
/**
* 最低api支持
* @return
*/
@Override
public int getMinApi() {
return super.getMinApi();
}
/**
* 设置当前操作用户信息
* @return
*/
@Nullable
@Override
public Vendor getVendor() {
return new Vendor("name", "name@qq.com");
}
}
2.1.3 自定义Detector
定义检测规则,添加到Issue生效,eg:
public class NewThreadDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"NewThread",
"避免自己创建Thread",
"请勿直接调用new Thread(),建议使用AsyncTask或统一的线程管理工具类",
Category.PERFORMANCE, 5, Severity.ERROR,
new Implementation(NewThreadDetector.class, Scope.JAVA_FILE_SCOPE));
@Override
public List<String> getApplicableConstructorTypes() {
return Collections.singletonList("java.lang.Thread");
}
@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
@NonNull PsiNewExpression node, @NonNull PsiMethod constructor) {
System.out.println("请勿直接调用new Thread(),建议使用AsyncTask或统一的线程管理工具类");
context.report(ISSUE, node, context.getLocation(node),
"请勿直接调用new Thread(),建议使用AsyncTask或统一的线程管理工具类");
}
}
2.1.4 输出
单独编译module ./gradlew checks:assemble ,build/libs下生成jar包。
2.2 aar包的生成
2.2.1 创建 java-library & 导入jar到当前包
a. project -> new module(name: lintaar)
b. lintPublish会把checks.jar一并打包打当前aar
dependencies {
lintPublish project(':checks')
}
2.2.2 输出
单独编译module ./gradlew lintaar:assemble ,build/output/aar下生成aar包。
2.3 Scanner解析
UastScanner:扫描 Java 或者 kotlin 源文件
ClassScanner:扫描字节码或编译的类文件
BinaryResourceScanner:扫描二进制资源文件(res/raw/bitmap等)
ResourceFolderScanner:扫描资源文件夹
XmlScanner:扫描 xml 格式文件
GradleScanner:扫描 Gradle 格式文件
OtherFileScanner:其他类型文件
UastScanner 的常用回调方法:
1.getApplicableUastTypes
此方法返回需要检查的AST节点的类型,类型匹配的UElement将会被createUastHandler(createJavaVisitor)创建的UElementHandler(Visitor)检查。
2.createUastHandler
创建一个UastHandler来检查需要检查的UElement,对应于getApplicableUastTypes
3.getApplicableMethodNames
返回你所需要检查的方法名称列表,或者返回null,相匹配的方法将通过visitMethod方法被检查
4.visitMethod
检查与getApplicableMethodNames相匹配的方法
5.getApplicableConstructorTypes
返回需要检查的构造函数类型列表,类型匹配的方法将通过visitConstructor被检查
6.visitConstructor
检查与getApplicableConstructorTypes相匹配的构造方法
7.getApplicableReferenceNames
返回需要检查的引用路径名,匹配的引用将通过visitReference被检查
8.visitReference
检查与getApplicableReferenceNames匹配的引用
9.appliesToResourceRefs
返回需要检查的资源引用,匹配的引用将通过visitResourceReference被检查
10.visitResourceReference
检查与appliesToResourceRefs匹配的资源引用
11.applicableSuperClasses
返回需要检查的父类名列表,此处需要类的全路径名
12.visitClass
检查applicableSuperClasses返回的类
2.4 自定义原理
整体流程:
lint.lint() -> // 执行./gradlew app:lint,会调用lint方法
runLint() -> // lint中根据搜索范围 最终调用此方法,创建了IssRegistry,LintGradleClient
LintGradleIssueRegistry -> //构造issue集合
LintGradleClient.run(IssueRegistry) -> // 创建了LintDriver,用于处理数据
LintDriver.analyze() -> // 解析规则,确定查找范围
registerCustomDetector(), fireEvent(Scope) -> // 加载自定义规则
checkProject(Project) // java源文件 - java字节码 - gradle文件 - 其它文件 - proguard - property
3. 发布
- 方法一:导入lint.jar 到
~/.android/lint/下,全局生效 - 方法二:导入上面自定义规则生成的aar包,所有依赖此aar的模块有效
- 运行指令
- ./gradlew :module名:lint