Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

android 自定义Lint

$
0
0

概述

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'
}

注:文章来自于美团移动团队

作者:xiangzhihong8 发表于2017/3/14 12:40:03 原文链接
阅读:100 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>