Android 自定义Lint检测
1.背景
一个大型项目往往需要几人甚至是十几人参与开发,大家编码习惯不同,导致一个项目往往会出现几个LogUtils类。经常出现Log的tag以人名命名。尽管软件组长严令禁止,可是难免还是有漏网之鱼。以下是常见的问题:
-
LogUtil有多个
-
Log tag 以人名命名
-
Glide.with传入Application,没有考量生命周期
-
使用BitmapFactory.decodeResource 加载图片,同一个资源多次调用会重复加载
-
直接使用new Thread 去开启一个线程
-
资源文件命令各种各样
2.Lint介绍
lint是android studio自带的静态代码分析工具,能够对 Android 源代码进行扫描和检查,并发现可优化的代码和潜在性的异常,从而方便开发人员尽早地予以处理。通常在做apk的性能优化
时,Lint也可以为我们提供帮助。【Analyze】->【InSpect Code】 扫描整个项目。可以检测图片是否 重复
,优化xml布局
等等
3.自定义Lint实现
当前环境:
-
Android studio 2020.3.1
-
gradle-6.5-all.zip
-
build:gradle:4.1.1
步骤1:新建一个项目
步骤2:new 一个 名为【check】的Android library
步骤3:new 一个名为【lintrule】的 java library
App 的build.gradle:
implementation project(path: ':check')
check的 build.gradle
/**
*在库项目中使用这个新的配置来进行要包含在已发布的AAR中的lint检查,如下所示。这意味着使用库的项目也会应用这些lint检查
*/
lintPublish project(':lintrule')
lintrule的 build.gradle
apply plugin: 'java'
dependencies {
compileOnly 'com.android.tools.lint:lint-api:27.0.1'
compileOnly 'com.android.tools.lint:lint-checks:27.0.1'
}
jar {
manifest {
attributes("Lint-Registry-v2": "com.example.lintrule.LintRegistry") //更改自己的注册器
}
}
注意在高版本的gradle中,build.gradle 引入java插件是通过这种方式:
plugins {
id 'java-library'
id 'kotlin'
}
通过这种方式验证的,我做过实验最终无法显示lint提示
项目环境搭建完毕,接下来正式开始编码。在lintrule 库中创建一个文件LogDetector.java 继承Detector:
public class LogDetector extends Detector implements SourceCodeScanner {
public static String TAG="LogDetector ";
public static final Issue ISSUE = Issue.create(
"LogId", //第一无二的id即可
"不要直接使用Log", //描述信息
"不要直接使用Log", // 描述信息
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE)//文件类型意味着只扫描java文件
);
@Nullable
@Override
//根据名称 去检查方法
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e");//Log.d() Log.e() 等方法名
}
// 类似还有visitClass 包括Gradle的访问
@Override
public void visitMethodCall(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) {
boolean isMemberInClass = context.getEvaluator().isMemberInClass(method, "android.util.Log");
boolean isMemberInSubClassOf = context.getEvaluator().isMemberInSubClassOf(method, "android.util.Log", true);
System.out.println(TAG+obj.getClass().getName());//可以通过gradlew lint 看到打印信息
if (isMemberInClass || isMemberInSubClassOf) {
context.report(ISSUE, node, context.getLocation(node), "不要直接使用Log");//report 上报提示信息给开发者
}
}
}
Detector:中文意思 探测器,检测
SourceCodeScanner:指定扫描文件类型,提供对应的方法,还有Detector.GradleScanner
,ClassScanner
等等
有了Detector,还需要将Detector 注入到lint 体系中去。 重写IssueRegistry 类的getIssues方法
public class LintRegistry extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Arrays.asList(
LogDetector.ISSUE
);
}
}
之后一定要在lintrule的 build.gradle 加入下面代码
jar {
manifest {
attributes("Lint-Registry-v2": "com.example.lintrule.LintRegistry")//自己注册器的包名+类名
}
}
网上很多文章都要求如下配置, 实际测试发现不需要。只要加入上面代码 即可!
image-20211101200320313.png
之后:build gradle 或者 gradlw lint 都可以刷新lint规则,万一还不行,就重启看看
ps: 在【Terminal】中可以通过 gradlew lint
命令 查看 日志的输出
实验结果:
image-20211101212313089.png
以上就是Lint的基本使用。lint 可以检测到方法名。那么它能否检测参数值
呢?。如何给Glide.with 传入Application 提示?
4.检测Glide.with传入Application
public class GlideWithDetector extends Detector implements SourceCodeScanner {
public static final Issue ISSUE = Issue.create(
"glideWithId",
"Glide.with尽量别传入Application",
"Glide.with尽量别传入Application", // no need
Category.MESSAGES,
7,
Severity.WARNING,
new Implementation(GlideWithDetector.class, Scope.JAVA_FILE_SCOPE)
);
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("with");
}
@Override
public void visitMethodCall(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) {
boolean isMemberInClass = context.getEvaluator().isMemberInClass(method, "com.bumptech.glide.Glide");
boolean isMemberInSubClassOf = context.getEvaluator().isMemberInSubClassOf(method, "com.bumptech.glide.Glide", true);
if (isMemberInClass || isMemberInSubClassOf) {
System.out.println("Glide2: "+node.getValueArguments().stream().count());
String obj = node.getValueArguments().get(0).asSourceString();
if (obj.toLowerCase().contains("application".toLowerCase())) { //检验 application
context.report(ISSUE, node, context.getLocation(node), "Glide.with尽量别传入Application");
}
}
}
}
UCallExpression 中文表达式 如: Log.d("TAG","1111111")
PsiMethod 单纯的指 Log.d()的方法
获取参数的类型: method.getParameters()[0].getType()
获取参数值: String obj = node.getValueArguments().get(0).asSourceString();
之后将GlideWithDetector 添加进.
public class LintRegistry extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Arrays.asList(
LogDetector.ISSUE,
GlideWithDetector.ISSUE,
);
}
}
既然lint 可以扫描。build.gradle 我是不是可以根据lint 来梳理 各个组件之间的依赖关系呢?参考了一些文章:https://juejin.cn/post/6963444269419872264#heading-8 发现的确是可行的。
5.实现module组件依赖关系可视化
自定义javaBean: TreeNode
public class TreeNode {
private String currentName;//当前module的名字
private List<TreeNode>chidrenNodes;//当前module下面的子module
public TreeNode(String currentName) {
this.currentName = currentName;
}
public TreeNode(String currentName, List<TreeNode> chidrenNodes) {
this.currentName = currentName;
this.chidrenNodes = chidrenNodes;
}
public String getCurrentName() {
return currentName;
}
public List<TreeNode> getChidrenNodes() {
return chidrenNodes;
}
}
DependencyDetector 代码实现
public class DependencyDetector extends Detector implements Detector.GradleScanner { //继承GradleScanner
private TreeNode mTreeNode;
public static final Issue ISSUE = Issue.create(
"dependeId",
"不需要提示",
"不需要提示",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(DependencyDetector.class, EnumSet.of(Scope.GRADLE_FILE))
);
@Override
public void beforeCheckRootProject(@NotNull Context context) {
mTreeNode = getNodes(context.getMainProject());
super.beforeCheckRootProject(context);
stringBuffer=new StringBuffer();
printNode(mTreeNode);
System.out.println(stringBuffer.toString());
}
// 创建一个TreeNode 数据结构 递归填充个节点
public TreeNode getNodes(Project project) {
List<Project> projects = project.getDirectLibraries();//获取子project,肯定是包括我们的依赖module
List<TreeNode> nodes = new ArrayList<>();
List<String> strings = new ArrayList<>();//存在多次扫描的情况
TreeNode mTreeNode = new TreeNode(project.getName(), nodes);
if (projects == null || projects.size() == 0) {
return mTreeNode;
}
for (int i = 0; i < projects.size(); i++) {
Project mProject = projects.get(i);
if (mProject.isGradleProject() && !strings.contains(mProject.getName())) {
nodes.add(getNodes(mProject));
strings.add(mProject.getName());
}
}
return mTreeNode;
}
StringBuffer stringBuffer;
//将组件组装成,其中A,B 代表module名称
// A-->B
// A-->c
// B-->D
// B-->C
public void printNode(TreeNode mTreeNode) {
for (int i = 0; i < mTreeNode.getChidrenNodes().size(); i++) {
TreeNode childTreeNode=mTreeNode.getChidrenNodes().get(i);
stringBuffer.append(""+mTreeNode.getCurrentName()+"-->"+childTreeNode.getCurrentName()+"\n");
printNode(childTreeNode);
}
}
}
然后在gradlew lint 将输出的日志 copy出来:如 :
app-->module2
module2-->module5
app-->module3
app-->check
check-->module4
check-->module5
将此字符串复制这个字符串生成图形 网站上。能快速展示出各个module之间的依赖关系。帮助新人快速理解项目
image-20211101211642311.png事件做到这步,基本上算完成了。
以上就是我对lint的学习。lint的玩法还有很多,等待各位去挖掘。