android 自定义Lint详解手机开发

概述

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

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

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5869.html

(0)
上一篇 2021年7月17日 00:21
下一篇 2021年7月17日 00:21

相关推荐

发表回复

登录后才能评论