likes
comments
collection
share

自定义Lint

作者站长头像
站长
· 阅读数 8

前言

不知道大家有没有注意项目中黄色代码块的提示,如下图所示: 自定义Lint 或者红色标记的代码(并没有任何错误),如下图所示: 自定义Lint 上文黄色的提醒和红色警告,都是来自Android Studio内置的Lint工具检查我们的代码后而作出的动作。 通过配置Lint,也可以消除上面的提醒。那么Lint是什么呢?

lint是什么

Android Studio 提供一个名为Lint的静态代码扫描工具,可以发现并纠正代码结构中的质量问题,而无需实际执行该应用,也不必编写测试用例。 Lint 工具可检查您的 Android 项目源文件是否包含潜在错误,以及在正确性、安全性、性能、易用性、便利性和国际化方面是否需要优化改进。

也就是说,通过Lint工具,我们可以写出更高质量的代码和代码潜在的问题,妈妈再也不用担心我的同事用中文命名了。也可以通过定制Lint相关配置,提高开发效率。

工作流程

自定义Lint 上图是Lint工具的工作流程,下面了解相关概念。

  • App Source Files 源文件包含组成 Android 项目的文件,包括 Java 和 XML 文件、图标和 ProGuard 配置文件等。

  • lint.xml 文件 此配置文件可用于指定您希望排除的任何 Lint 检查以及自定义问题严重级别。

  • lint Tool 我们可以通过Android Studio 对 Android 项目运行此静态代码扫描工具。也可以手动运行。Lint 工具检查可能影响 Android 应用质量和性能的代码结构问题。

  • Lint 检查结果 我们可以在控制台(命令行运行)或 Android Studio 的 Inspection Results 窗口中查看 Lint 检查结果。

总的来说,lint Tool 作为整个流程的核心,通过输入源文件(App Source Files)和 lint 的配置(lint.xml),对源文件进行处理分析,得到最终的扫描结果,一般是以 HTML 形式输出。

使用方式

Lint 除了我们熟悉的./gradlew lint这种运行方式,还可以通过命令行工具${SDK_HOME}/tools/bin/lint和 Android Studio 的Inspect code 命令来运行。另外在 AS 在编辑器中也可以开启动态监测,Lint 会自动在后台运行,如果有错误会在开发的时候高亮出来。

自定义Lint 根据源码,整理工作流程:

自定义Lint

自定义规则

先学习相关api,可以快速理解一些概念,可以粗略看过,下结实践再回来看。

重点API

Issue

Issue 代表您想要发现并提示给开发者的一种问题,包含描述、更全面的解释、类型和优先级等等。官方提供了一个 Issue 类,您只需要实例化一个 Issue,并注册到 IssueRegistry 里。

   private  static final Issue ISSUE = Issue.create("NamingConventionWarning",
            "命名规范错误",
            "使用驼峰命名法,方法命名开头小写,类大写字母开头",
            Category.USABILITY,
            5,
            Severity.WARNING,
            new Implementation(NamingConventionDetecor.class,
                    EnumSet.of(Scope.JAVA_FILE)));
  1. id: 唯一的 id,简要表达当前问题。
  2. briefDescription: 简单描述当前问题。
  3. explanation: 详细解释当前问题和修复建议。
  4. category: 问题类别,在 Android 中主要有如下六大类:
  5. SECURITY: 安全性。例如在 AndroidManifest.xml 中没有配置相关权限等。
    • USABILITY: 易用性。例如重复图标,一些黄色警告等。
    • PERFORMANCE: 性能。例如内存泄漏,xml 结构冗余等。
    • CORRECTNESS: 正确性。例如超版本调用 API,设置不正确的属性值等。
    • A11Y: 无障碍 (Accessibility)。例如单词拼写错误等。
    • I18N: 国际化 (Internationalization)。例如字符串缺少翻译等。
  6. priority: 优先级,从 1 到 10,10 最重要。
  7. severity: 严重程度
    • FATAL
    • ERROR
    • WARNING
    • INFORMATIONAL
    • IGNORE
  8. implementation: Issue 和哪个 Detector 绑定,以及声明检查的范围。Scope有如下选择范围:
  • RESOURCE_FILE(资源文件)
  • BINARY_RESOURCE_FILE(二进制资源文件)
  • RESOURCE_FOLDER(资源文件夹)
  • ALL_RESOURCE_FILES(所有资源文件)
  • JAVA_FILE(Java文件)
  • ALL_JAVA_FILES(所有Java文件)
  • CLASS_FILE(class文件)
  • ALL_CLASS_FILES(所有class文件)
  • MANIFEST(配置清单文件)
  • PROGUARD_FILE(混淆文件)
  • JAVA_LIBRARIES(Java库)
  • GRADLE_FILE(Gradle文件)
  • PROPERTY_FILE(属性文件)
  • TEST_SOURCES(测试资源)
  • OTHER(其他)

IssueRegistry

用于注册要检查的Issue(规则),只有注册了Issue,该Issue才能被使用。例如注册上文的命名规范规则。

   public class Register extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(NamingConventionDetector.ISSUE);
    }
}

Detector

另外您还需要实现一个 Detector。Detector 负责扫描代码并找到有问题的地方,然后把它们报告出来。一个 Detector 可以报告多种类型的 Issue,可以针对不同类型的问题使用不同的严重程度,这样用户可以更精确地控制他们想要看到的内容。

Scanner

扫描并发现代码中的Issue,Detector需要实现Scaner,可以继承一个到多个。

  • UastScanner: 扫描 Java 文件和 Kotlin 文件
  • ClassScanner: 扫描 Class 文件
  • XmlScanner: 扫描 XML 文件
  • ResourceFolderScanner: 扫描资源文件夹
  • BinaryResourceScanner: 扫描二进制资源文件
  • OtherFileScanner: 扫描其他文件
  • GradleScanner: 扫描 Gradle 脚本

旧版本的JavaScanner、JavaPsiScanner随着版本的更新已经被UastScanner替代了。

实践

新建lint module

新建一个 module,在选择 module 类型的界面,选择 Java or Kotlin Library,然后给新建的 module 命名,例如 lint。 在新建的 module 下的 build.gradle 文件添加依赖:

// Lint    
compileOnly "com.android.tools.lint:lint-api:26.3.2"    
compileOnly "com.android.tools.lint:lint-checks:26.3.2"

## 在 app module 添加 lintChecks

为了方便在写完 Lint 检查的代码后进行测试,在 app module 下的 build.gradle 文件添加依赖:

dependencies {    lintChecks project(':lint')}

扫描xml文件

要分析一个 XML 文件,可以重写 visitDocument() 方法。这个方法每个 XML 文件都会调用一次,然后传入 XML DOM 模型,之后您就可以自己遍历并做分析。 但是呢,我们通常只关注一些特定的标签和一些特定的属性,为了让扫描更快,Detector 可以指定我们关注的元素和属性。

要筛选我们关注的元素或属性,只需实现 getApplicableElements() 或 getApplicableAttributes() 方法,并返回一个标签或属性名称的字符串列表。然后再实现 visitElement() 或 visitAttribute() 方法,这两个方法针对每个指定的元素和属性都会调用一次。

public class MyDetector extends ResourceXmlDetector {

    public static final Issue ISSUE = Issue.create(
            "MyId",
            "My brief summary of the issue",
            "My longer explanation of the issue",
            Category.CORRECTNESS, 6, Severity.WARNING,
            new Implementation(MyDetector.class, Scope.RESOURCE_FILE_SCOPE));
    
    @Override
    public Collection<String> getApplicableElements() {
        return Collections.singletonList(
                "com.google.io.demo.MyCustomView");
    }

    @Override
    public void visitElement(XmlContext context, Element element) {
        if (!element.hasAttributeNS(
                "http://schemas.android.com/apk/res/com.google.io.demo",
                "exampleString")) {
            context.report(ISSUE, element, context.getLocation(element),
                    "Missing required attribute 'exampleString'");
        }
    }
}

扫描Java/Kotlin文件

public class NamingConventionDetector extends Detector implements Detector.UastScanner {
    //定义命名规范规则
    public static final Issue ISSUE = Issue.create("NamingConventionWarning",
            "命名规范错误",
            "使用驼峰命名法,方法命名开头小写",
            Category.USABILITY,
            5,
            Severity.WARNING,
            new Implementation(NamingConventionDetector.class,
                    EnumSet.of(Scope.JAVA_FILE)));

    //返回我们所有感兴趣的类,即返回的类都被会检查
    @Nullable
    @Override
    public List<Class<? extends UElement>> getApplicableUastTypes() {
        return Collections.<Class<? extends UElement>>singletonList(UClass.class);
    }

    //重写该方法,创建自己的处理器
    @Nullable
    @Override
    public UElementHandler createUastHandler(@NotNull final JavaContext context) {
        return new UElementHandler() {
            @Override
            public void visitClass(@NotNull UClass node) {
                node.accept(new NamingConventionVisitor(context, node));
            }
        };
    }
    //定义一个继承自AbstractUastVisitor的访问器,用来处理感兴趣的问题
    public static class NamingConventionVisitor extends AbstractUastVisitor {

        JavaContext context;

        UClass uClass;

        public NamingConventionVisitor(JavaContext context, UClass uClass) {
            this.context = context;
            this.uClass = uClass;
        }

        @Override
        public boolean visitClass(@NotNull UClass node) {
            //获取当前类名
            char beginChar = node.getName().charAt(0);
            int code = beginChar;
            //如果类名不是大写字母,则触碰Issue,lint工具提示问题
            if (97 < code && code < 122) {
                context.report(ISSUE,context.getNameLocation(node),
                        "the  name of class must start with uppercase:" + node.getName());
                //返回true表示触碰规则,lint提示该问题;false则不触碰
                return true;
            }

            return super.visitClass(node);
        }

        @Override
        public boolean visitMethod(@NotNull UMethod node) {
            //当前方法不是构造方法
            if (!node.isConstructor()) {
                char beginChar = node.getName().charAt(0);
                int code = beginChar;
                //当前方法首字母是大写字母,则报Issue
                if (65 < code && code < 90) {
                    context.report(ISSUE, context.getLocation(node),
                            "the method must start with lowercase:" + node.getName());
                    //返回true表示触碰规则,lint提示该问题;false则不触碰
                    return true;
                }
            }
            return super.visitMethod(node);

        }

    }
}

上文NamingConventionDetector类,已经是全部代码,只检查类名和方法名是否符合驼峰命名法,可以根据具体需求,重写抽象类AbstractUastVisitor的visitXXX方法。

如果处理特定的方法或者其他,也可以使用默认的处理器。重写Scanner相关方法。例如

 @Override
public List<String> getApplicableMethodNames() {
    return Arrays.asList("e","v");
}

表示e(),v()方法会被检测到,并调用visitMethod()方法,实现自己的逻辑。

    @Override
    public void visitMethod JavaContext context,  JavaElementVisitor visitor,  PsiMethodCallExpression call, PsiMethod method) {
        //todo something
        super.visitMethod(context, visitor, call, method);
    }

报告

如果 Detector 定位到一个问题,需要使用 Context 对象(Detector 的每个方法都会传入进来)调用 report() 方法来报告错误。

private void reportError(XmlContext context, Element element) {
    context.report(
            ISSUE,
            element,
            context.getLocation(element),
            "请不要在 AndroidManifest.xml 文件里同时设置方向和透明主题"
    );
}

除了列出要报告的问题外,还需要提供位置、作用域节点和错误提示消息:

  • 作用域节点:对于 XML 和 Java 源文件,是指发生的错误周围最近的 XML DOM 或 Parse AST 树节点,例如上面传入的 element 对象。
  • 位置:是指错误发生的位置。一般只需将 AST/XML 节点传递给 context.getLocation() 方法,该方法将创建一个具有正确文件名和与给定节点相对应的偏移量的 Location。如果你的错误与某个属性有关,则传递该属性,以使该问题更好地指出错误发生的位置。

注册自定义的Issue

public class Register extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(NamingConventionDetector.ISSUE);
    }
    
    @Override    
    public int getApi() {        
        return ApiKt.CURRENT_API;    
    }
}

gradle绑定

jar {
    manifest {
        attributes 'Lint-Registry-v2': 'com.liujian.lib.MyIssueRegistry'
    }
}

检测

在MainActivity写一个名字首字母大写的方法

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Test()
    }

    private fun Test(){

    }
}

然后在Terminal输入命令

./gradlew :app:lintDebug

观察BUILD SUCCESSFUL后在app/build/reports文件夹下有html报告,

在浏览器显示后可以看到我们刚写的测试错误的报告 自定义Lint

Jenkins集成

增量检查

使用./gradlew lint执行一次完整的 Lint 检查需要耗费很长时间,长时间的检查等待成为大家的心头之痛,阻碍静态检查在各项目上的推进。很容易想到,其实每次 commit 提交没必要执行完整的 Lint,只检查修改过的那部分代码就行。

首先,先来看下最简单的命令行方式是如何实现指定文件 Lint 的,找到命令的入口文件 Main,关键在于以下几行 自定义Lint LintGradleClient内部封装了只有一个参数的 run 方法,并且在调用父类的 run 方法时传入了一个空列表。如果传空列表,这里会发生什么呢?沿着LintCliClient.run的调用往下看,中间省略掉若干过程,最后在LintDriver里找到这么一段:

自定义Lint 当传入 files 不为空时,LintDriver会执行增量检查,否则会读取项目源文件夹下的所有文件,因此每次 gradle 执行 lint 都退化成了全量扫描。

所以这里实现增量扫描就很简单了,只需要继承LintGradleClient,对 files 入参做一些定制处理就行。通过自定义 gradle plugin,封装了一个IncrementLintTask,通过 Git diff 命令找到差异文件,然后对指定的差异文件执行 Lint,最后实现的执行方式就像这样

实战检测规则梳理

感谢