前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >android 自定义Lint

android 自定义Lint

作者头像
xiangzhihong
发布2018-02-06 11:26:23
1.4K0
发布2018-02-06 11:26:23
举报
文章被收录于专栏:向治洪向治洪

概述

Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。

为什么要自定义

我们在实际使用Lint中遇到了以下问题:

  • 原生Lint无法满足我们团队特有的需求,例如:编码规范。
  • 原生Lint存在一些检测缺陷或者缺少一些我们认为有必要的检测。
  • 对于正式发布包来说,debug和verbose的日志会自动不显示。

基于上面的考虑,我们开始调研并开发自定义Lint。开发中我们希望开发者使用RoboGuice的Ln替代Log/System.out.println。

相比原生的lint,Ln具有以下优势:

  • 拥有更多的有用信息,包括应用程序名字、日志的文件和行信息、时间戳、线程等。
  • 由于使用了可变参数,禁用后日志的性能比Log高。因为最冗长的日志往往都是debug或verbose日志,这可以稍微提高一些性能。
  • 可以覆盖日志的写入位置和格式。

示例代码:

首先需要配置gradle。

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.tools.lint:lint-api:24.5.0'
    compile 'com.android.tools.lint:lint-checks:24.5.0'
}

注:lint-api: 官方给出的API,API并不是最终版,官方提醒随时有可能会更改API接口。

创建Detector。 Detector负责扫描代码,发现问题并报告。

/**
 * 避免使用Log / System.out.println ,提醒使用Ln
 * https://github.com/roboguice/roboguice/wiki/Logging-via-Ln
 */
public class LogDetector extends Detector implements Detector.JavaScanner{

    public static final Issue ISSUE = Issue.create(
            "LogUse",
            "避免使用Log/System.out.println",
            "使用Ln,防止在正式包打印log",
            Category.SECURITY, 5, Severity.ERROR,
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return Collections.<Class<? extends Node>>singletonList(MethodInvocation.class);
    }

    @Override
    public AstVisitor createJavaVisitor(final JavaContext context) {
        return new ForwardingAstVisitor() {
            @Override
            public boolean visitMethodInvocation(MethodInvocation node) {

                if (node.toString().startsWith("System.out.println")) {
                    context.report(ISSUE, node, context.getLocation(node),
                                       "请使用Ln,避免使用System.out.println");
                    return true;
                }

                JavaParser.ResolvedNode resolve = context.resolve(node);
                if (resolve instanceof JavaParser.ResolvedMethod) {
                    JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolve;
                    // 方法所在的类校验
                    JavaParser.ResolvedClass containingClass = method.getContainingClass();
                    if (containingClass.matches("android.util.Log")) {
                        context.report(ISSUE, node, context.getLocation(node),
                                       "请使用Ln,避免使用Log");
                        return true;
                    }
                }
                return super.visitMethodInvocation(node);
            }
        };
    }
}

说明: 自定义Detector可以实现一个或多个Scanner接口,选择实现哪种接口取决于你想要的扫描范围。 Detector.XmlScanner Detector.JavaScanner Detector.ClassScanner Detector.BinaryResourceScanner Detector.ResourceFolderScanner Detector.GradleScanner Detector.OtherFileScanner

这里我们主要针对的是Java代码,所以我们选取JavaScanner。具体的实现逻辑: 代码中getApplicableNodeTypes方法决定了什么样的类型能够被检测到。这里我们想看Log以及println的方法调用,选取MethodInvocation。对应的,我们在createJavaVisitor创建一个ForwardingAstVisitor通过visitMethodInvocation方法来接收被检测到的Node。 可以看到getApplicableNodeTypes返回值是一个List,也就是说可以同时检测多种类型的节点来帮助精确定位到代码,对应的ForwardingAstVisitor接受返回值进行逻辑判断就可以了。

可以看到JavaScanner中还有其他很多方法,getApplicableMethodNames(指定方法名)、visitMethod(接收检测到的方法),这种对于直接找寻方法名的场景会更方便。当然这种场景我们用最基础的方式也可以完成,只是比较繁琐。

注:Lint是如何实现Java扫描分析的呢?Lint使用了Lombok做抽象语法树的分析。所以在我们告诉它需要什么类型后,它就会把相应的Node返回给我们。 当接收到返回的Node之后需要进行判断,如果调用方法是System.out.println或者属于android.util.Log类,则调用context.report上报。即调用了下面代码:

context.report(ISSUE, node, context.getLocation(node), "请使用Ln,避免使用Log");

说明:第一个参数是Issue;第二个参数是当前节点;第三个参数location会返回当前的位置信息,便于在报告中显示定位;

这里写图片描述
这里写图片描述

Issue

Issue由Detector发现并报告,是Android程序代码可能存在的bug。实例:

public static final Issue ISSUE = Issue.create(
        "LogUse",
        "避免使用Log/System.out.println",
        "使用Ln,防止在正式包打印log",
        Category.SECURITY, 5, Severity.ERROR,
        new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));
这里写图片描述
这里写图片描述

Category

系统已有类别: Lint Correctness (incl. Messages) Security Performance Usability (incl. Icons, Typography) Accessibility Internationalization Bi-directional text

自定义Category:

public class MTCategory {
    public static final Category NAMING_CONVENTION = Category.create("命名规范", 101);
}

然后在ISSUE引用。

public static final Issue ISSUE = Issue.create(
        "IntentExtraKey",
        "intent extra key 命名不规范",
        "请在接受此参数中的Activity中定义一个按照EXTRA_<name>格式命名的常量",
        MTCategory.NAMING_CONVENTION , 5, Severity.ERROR,
        new Implementation(IntentExtraKeyDetector.class, Scope.JAVA_FILE_SCOPE));

IssueRegistry

提供需要被检测的Issue列表,形如:

public class MTIssueRegistry extends IssueRegistry {
    @Override
    public synchronized List<Issue> getIssues() {
        System.out.println("==== MT lint start ====");
        return Arrays.asList(
                DuplicatedActivityIntentFilterDetector.ISSUE,
                //IntentExtraKeyDetector.ISSUE,
                //FragmentArgumentsKeyDetector.ISSUE,
                LogDetector.ISSUE,
                PrivateModeDetector.ISSUE,
                WebViewSafeDetector.ON_RECEIVED_SSL_ERROR,
                WebViewSafeDetector.SET_SAVE_PASSWORD,
                WebViewSafeDetector.SET_ALLOW_FILE_ACCESS,
                WebViewSafeDetector.WEB_VIEW_USE,
                HashMapForJDK7Detector.ISSUE
        );
    }
}
```。
然后在getIssues()方法中返回需要被检测的Issue List列表。在build.grade中声明Lint-Registry属性。





<div class="se-preview-section-delimiter"></div>

jar { manifest { attributes(“Lint-Registry”: “com.meituan.android.lint.core.MTIssueRegistry”) } }

“`

jar {
    manifest {
        attributes("Lint-Registry": "com.meituan.android.lint.core.MTIssueRegistry")
    }
}

至此,代码上的逻辑就编写完成了,接下来是如何打包给集成方使用了。

jar包使用

将我们自定义的lint.jar完成后,我们接下来就是如何使用jar的问题了。

Google方案

将jar拷贝到~/.android/lint中,然后挺好默认的lint即可:

$ mkdir ~/.android/lint/
$ cp customrule.jar ~/.android/lint/

LinkedIn方案

LinkedIn提供了另一种思路 : 将jar放到一个aar中。这样我们就可以针对工程进行自定义Lint,lint.jar只对当前工程有效。 详细介绍请看LinkedIn博客: Writing Custom Lint Checks with Gradle

可行性

AAR Format 中写明可以有lint.jar。 从Google Groups adt-dev论坛讨论来看是官方目前的推荐方案,详见:Specify custom lint JAR outside of lint tools settings directory 测试后发现aar中有lint.jar ,最终APK中并不会引起包体积变化。 所以我们选择LinkedIn方案。方案选定后,我们怎么实践呢?

LinkedIn实践

在确定方案后,我们为Lint增加了很多功能,包括编码规范和原生Lint增强。这里以HashMap检测为例,介绍一下Lint。 Lint检测中有一项是Java性能检测,常见的报错就是:HashMap can be replaced with SparseArray。

public static void testHashMap() {
    HashMap<Integer, String> map1 = new HashMap<Integer, String>();
    map1.put(1, "name");
    HashMap<Integer, String> map2 = new HashMap<>();
    map2.put(1, "name");
    Map<Integer, String> map3 = new HashMap<>();
    map3.put(1, "name");
}

对于上述代码,原生Lint只能检测第一种情况,JDK 7泛型新写法还检测不到。所以我们需要对增强型的HashMap做Lint检查。

分析源码后发现,HashMap检测是根据new HashMap处的泛型来判断是否符合条件。于是我们想到,在发现new HashMap后去找前面的泛型,因为本身Java就是靠类型推断的,我们可以直接根据前面的泛型来确定是否使用SparseArray。

所以,对于增强HashMap检测我们可以采用以下的方式:

@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
    return Collections.<Class<? extends Node>>singletonList(ConstructorInvocation.class);
}

private static final String INTEGER = "Integer";                        //$NON-NLS-1$
private static final String BOOLEAN = "Boolean";                        //$NON-NLS-1$
private static final String BYTE = "Byte";                              //$NON-NLS-1$
private static final String LONG = "Long";                              //$NON-NLS-1$
private static final String HASH_MAP = "HashMap";                       //$NON-NLS-1$

@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
    return new ForwardingAstVisitor() {

        @Override
        public boolean visitConstructorInvocation(ConstructorInvocation node) {
            TypeReference reference = node.astTypeReference();
            String typeName = reference.astParts().last().astIdentifier().astValue();
            // TODO: Should we handle factory method constructions of HashMaps as well,
            // e.g. via Guava? This is a bit trickier since we need to infer the type
            // arguments from the calling context.
            if (typeName.equals(HASH_MAP)) {
                checkHashMap(context, node, reference);
            }
            return super.visitConstructorInvocation(node);
        }
    };
}

/**
 * Checks whether the given constructor call and type reference refers
 * to a HashMap constructor call that is eligible for replacement by a
 * SparseArray call instead
 */
private void checkHashMap(JavaContext context, ConstructorInvocation node, TypeReference reference) {
    StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments();
    if (types == null || types.size() != 2) {
        /*
        JDK 7 新写法
        HashMap<Integer, String> map2 = new HashMap<>();
        map2.put(1, "name");
        Map<Integer, String> map3 = new HashMap<>();
        map3.put(1, "name");
         */

        Node variableDefinition = node.getParent().getParent();
        if (variableDefinition instanceof VariableDefinition) {
            TypeReference typeReference = ((VariableDefinition) variableDefinition).astTypeReference();
            checkCore(context, variableDefinition, typeReference);// 此方法即原HashMap检测逻辑
        }

    }
    // else --> lint本身已经检测
}

为自定义Lint开发plugin

aar虽然很方便,但是在团队内部推广中我们遇到了以下问题:

  • 配置繁琐,不易推广。每个库都需要自行配置lint.xml、lintOptions,并且compile aar。
  • 不易统一。各库之间需要使用相同的配置,保证代码质量。但现在手动来回拷贝规则,且配置文件可以自己修改。

于是我们想到开发一个plugin,统一管理lint.xml和lintOptions,自动添加aar。

统一lint.xml

我们在plugin中内置lint.xml,执行前拷贝过去,执行完成后删除。

lintTask.doFirst {

    if (lintFile.exists()) {
        lintOldFile = project.file("lintOld.xml")
        lintFile.renameTo(lintOldFile)
    }
    def isLintXmlReady = copyLintXml(project, lintFile)

    if (!isLintXmlReady) {
        if (lintOldFile != null) {
            lintOldFile.renameTo(lintFile)
        }
        throw new GradleException("lint.xml不存在")
    }

}

project.gradle.taskGraph.afterTask { task, TaskState state ->
    if (task == lintTask) {
        lintFile.delete()
        if (lintOldFile != null) {
            lintOldFile.renameTo(lintFile)
        }
    }
}

统一lintOptions

Android plugin在1.3以后允许我们替换Lint Task的lintOptions:

def newOptions = new LintOptions()
newOptions.lintConfig = lintFile
newOptions.warningsAsErrors = true
newOptions.abortOnError = true
newOptions.htmlReport = true
//不放在build下,防止被clean掉
newOptions.htmlOutput = project.file("${project.projectDir}/lint-report/lint-report.html")
newOptions.xmlReport = false

lintTask.lintOptions = newOptions

自动添加最新aar

考虑到plugin只是一个检查代码插件,它最需要的应该是实时更新。当 我们引入了Gradle Dynamic Versions,就可以做到实时更新了:

project.dependencies {
    compile 'com.meituan.android.lint:lint:latest.integration'
}

project.configurations.all {
    resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-03-14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
    • 为什么要自定义
      • Issue
        • Category
          • IssueRegistry
          • jar包使用
            • Google方案
              • LinkedIn方案
              • LinkedIn实践
                • 为自定义Lint开发plugin
                  • 统一lint.xml
                  • 统一lintOptions
                  • 自动添加最新aar
              相关产品与服务
              腾讯云代码分析
              腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档