项目地址 CustomLintRules
网上有很多讲解的文章,不再赘述。比如浅谈 Android 自定义 Lint 规则的实现。了解基础的同学可以直接看工程。
关于工程结构,简单梳理一下。
截止发文,共有5个模块。不过重点只关注app
和lintjar
即可。
lintjar
模块是自定义的lint规则。
网上其他文章用的lint库貌似比较老旧,我目前用的是:
dependencies {
compile 'com.android.tools.lint:lint-api:26.3.1'
compile 'com.android.tools.lint:lint-checks:26.3.1'
}
算是最新的。如无异常,应该会持续更新。新版本的api和老的有一定出入,并且部分用到了kotlin。
目前有如下几个检测项:
LogDetector
public class LogDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"LogUsage",
"避免调用android.util.Log",
"请勿直接调用android.util.Log,应该使用统一工具类",
Category.SECURITY, 5, Severity.ERROR,
new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethodCall(@NotNull JavaContext context,
@NotNull UCallExpression node,
@NotNull PsiMethod method) {
if (context.getEvaluator().isMemberInClass(method, "android.util.Log")) {
context.report(ISSUE, node, context.getLocation(node), "避免调用android.util.Log");
}
}
}
不多说了,其他文章里都是以这个为例子讲解的。
NewThreadDetector
检测直接new Thread
的操作。也是非常简单。
public class NewThreadDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"NewThread",
"避免自己创建Thread",
"请勿直接调用new Thread(),建议使用统一的线程管理工具类",
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(@NotNull JavaContext context,
@NotNull UCallExpression node,
@NotNull PsiMethod constructor) {
context.report(ISSUE, node, context.getLocation(node),
"避免自己创建Thread");
}
}
ConcurrentModifyDetector
检查ConcurrentModificationException的规则,应对的场景相对要简单一些。代码稍微复杂些,不再粘贴,可以直接看源码。检测结果如下:
图2可以看到,排除了break和return两种情况,检测出了第一个for循环可能出现ConcurrentModificationException
。
DrawableAttrDetector
检测自定义的drawable文件是否使用了如下格式的属性:
android:color="?attr/XXX"
、android:drawable="?attr/XXX"
。由于自定义drawable使用?attr
是api21才有的特性,并且编译器不会给出提示,在开发中会埋下隐患,所以设计了这个规则。检测结果如下:
ModuleAccessibleDetector
android的annotation包中,有个RestrictTo
的注解,可以限定类、方法等的使用范围。
@Retention(CLASS)
@Target({ANNOTATION_TYPE,TYPE,METHOD,CONSTRUCTOR,FIELD,PACKAGE})
public @interface RestrictTo {
/**
* The scope to which usage should be restricted.
*/
Scope[] value();
enum Scope {
/**
* Restrict usage to code within the same library (e.g. the same
* gradle group ID and artifact ID).
*/
LIBRARY,
/**
* Restrict usage to code within the same group of libraries.
* This corresponds to the gradle group ID.
*/
LIBRARY_GROUP,
/**
* Restrict usage to code within the same group ID (based on gradle
* group ID). This is an alias for {@link #LIBRARY_GROUP}.
*
* @deprecated Use {@link #LIBRARY_GROUP} instead
*/
@Deprecated
GROUP_ID,
/**
* Restrict usage to tests.
*/
TESTS,
/**
* Restrict usage to subclasses of the enclosing class.
* <p>
* <strong>Note:</strong> This scope should not be used to annotate
* packages.
*/
SUBCLASSES,
}
}
Scope定义了限定的几种范围。
-
TESTS
限定只有测试代码可以访问 -
SUBCLASSES
限定只有子类可以访问
LIBRARY
和LIBRARY_GROUP
,以下是我个人的理解,如有误请指出。
这两个范围需要打成aar | jar包才能起作用,以源码方式使用不起作用。
比如对于库'com.google.code.gson:gson:2.8.2'
,LIBRARY
限定只有该库中的代码可以访问;LIBRARY_GROUP
限定groupId是'com.google.code.gson'
的库可以访问,比如'com.google.code.gson:data:1.0.0
(虚构的库)。
为了使以源码方式使用时也能起作用,这里在工程的annotation
模块定义了ModuleAccessible
注解类,并使用ModuleAccessibleDetector
进行lint检测。
PrintStackTraceDetector
项目中有一些Throwable.printStackTrace()
引发的OOM的报告,为了防止伙伴们直接调用该方法,故做了检测,并且加入了自动修复功能。
public class PrintStackTraceDetector extends com.android.tools.lint.detector.api.Detector
implements com.android.tools.lint.detector.api.Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"PrintStackTraceUsage",
"避免直接调用Throwable.printStackTrace()",
"直接调用Throwable.printStackTrace()可能引起OOM",
Category.LINT, 5, Severity.ERROR,
new Implementation(PrintStackTraceDetector.class, Scope.JAVA_FILE_SCOPE));
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("printStackTrace");
}
@Override
public void visitMethodCall(@NotNull JavaContext context,
@NotNull UCallExpression node,
@NotNull PsiMethod method) {
if (context.getEvaluator().isMemberInClass(method, "java.lang.Throwable")) {
/*
报告该问题
*/
context.report(ISSUE,
method,
context.getLocation(node),
"直接调用Throwable.printStackTrace()可能引起OOM,使用自定义方法替代",
getLintFix(context, node));
}
}
/**
* lint自动修复
*/
private LintFix getLintFix(@NotNull JavaContext context,
@NotNull UCallExpression node) {
/*
先检查当前文件是否import了我们需要的类
*/
boolean hasImport = false;
List<UImportStatement> list = context.getUastFile().getImports();
for (UImportStatement statement : list) {
UElement element = statement.getImportReference();
if ("com.sollian.customlintrules.utils.LogUtils".endsWith(element.asRenderString())) {
hasImport = true;
break;
}
}
/*
第一个修复,替换方法调用
*/
LintFix fix = fix().replace()
.all()
.with("LogUtils.printStaceTrace(" + node.getReceiver().asRenderString() + ')')
.autoFix()
.build();
/*
第二个修复,import LogUtils类
*/
LintFix importFix = null;
if (!hasImport) {
UImportStatement statement = list.get(list.size() - 1);
String lastImport = statement.asRenderString() + ';';
importFix = fix().replace()
//最后的一条import语句
.text(lastImport)
//替换为最后一条import语句,加上LogUtils类
.with(lastImport + "\nimport com.sollian.customlintrules.utils.LogUtils;")
//替换位置
.range(context.getLocation(statement))
.autoFix()
.build();
}
/*
最终的修复方案
*/
LintFix.GroupBuilder builder = fix().name("使用LogUtils.printStackTrace替换").composite();
builder.add(fix);
if (importFix != null) {
builder.add(importFix);
}
return builder.build();
}
}
检测的代码很简单。
getLintFix方法是自定义的lint修复方式。我们的目的是将e.printStackTrace()
这种语句替换为LogUtils.printStackTrace(e)
这种形式。
第一个修复是字符串替换。
第二个修复是检查当前类是否import了LogUtils类,如果没有,则加上。
最后将两个修复打包成一个,然后返回。
app
模块中的MainActivity
给出了自定义检查的case,可以参考。
注:第一次使用时可能碰到自定义lint不起作用的情况,可以clean再重新build,还不行的话可以重启Android Studio
网友评论