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布局 等等

image-20211101103528279.png

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的玩法还有很多,等待各位去挖掘。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容