东莞建工集团企业网站,北京环球影城无限次快速通道,买链接网,关于网页制作的论文Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查#xff0c;可以发现代码潜在的问题#xff0c;提醒程序员及早修正。 为保证代码质量#xff0c;美团在开发流程中加入了代码检查#xff0c;如果代码检测到问题… Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查可以发现代码潜在的问题提醒程序员及早修正。 为保证代码质量美团在开发流程中加入了代码检查如果代码检测到问题则无法合并到正式分支中这些检查中就包括Lint。 我们在实际使用Lint中遇到了以下问题- 原生Lint无法满足我们团队特有的需求例如编码规范。 - 原生Lint存在一些检测缺陷或者缺少一些我们认为有必要的检测。 基于上面的考虑我们开始调研并开发自定义Lint。 在介绍美团的实践之前先用一个小例子来看看如何进行自定义Lint。 示例介绍 开发中我们希望开发者使用RoboGuice的Ln替代Log/System.out.println。 Ln相比于Log有以下优势 对于正式发布包来说debug和verbose的日志会自动不显示。拥有更多的有用信息包括应用程序名字、日志的文件和行信息、时间戳、线程等。由于使用了可变参数禁用后日志的性能比Log高。因为最冗长的日志往往都是debug或verbose日志这可以稍微提高一些性能。可以覆盖日志的写入位置和格式。这里我们以此为例让Lint检查代码中Log/System.out.println的调用提醒开发者使用Ln。 创建Java工程配置Gradle apply plugin: javadependencies {compile fileTree(dir: libs, include: [*.jar])compile com.android.tools.lint:lint-api:24.5.0compile com.android.tools.lint:lint-checks:24.5.0
}注- lint-api: 官方给出的APIAPI并不是最终版官方提醒随时有可能会更改API接口。 - lint-checks已有的检查。 创建Detector Detector负责扫描代码发现问题并报告。 /*** 避免使用Log / System.out.println ,提醒使用Ln** RoboGuices Ln logger is similar to Log, but has the following advantages:* - Debug and verbose logging are automatically disabled for release builds.* - Your app name, file and line of the log message, time stamp, thread, and other useful information is automatically logged for you. (Some of this information is disabled for release builds to improve performance).* - Performance of disabled logging is faster than Log due to the use of the varargs. Since your most expensive logging will often be debug or verbose logging, this can lead to a minor performance win.* - You can override where the logs are written to and the format of the logging.* * https://github.com/roboguice/roboguice/wiki/Logging-via-Ln** Created by chentong on 18/9/15.*/
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));Overridepublic ListClass? extends Node getApplicableNodeTypes() {return Collections.Class? extends NodesingletonList(MethodInvocation.class);}Overridepublic AstVisitor createJavaVisitor(final JavaContext context) {return new ForwardingAstVisitor() {Overridepublic 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继承Detector类然后实现Scanner接口。自定义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接收检测到的方法这种对于直接找寻方法名的场景会更方便。当然这种场景我们用最基础的方式也可以完成只是比较繁琐。 那么其他Scanner如何去写呢可以去查看各接口中的方法去实现一般都是有这两种对应什么样的类型需要返回、接收发现的类型。 这里插一句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会返回当前的位置信息便于在报告中显示定位最后的字符串用来为警告添加解释。对应报告中的位置如下图 这里还需要说明report会自动处理被suppress(suppressLint)/ignore(tools:ignore)的警告。所以发现问题直接调用report就可以不用担心其他问题。 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));声明为final class由静态工厂方法创建。对应参数解释如下 id : 唯一值应该能简短描述当前问题。利用Java注解或者XML属性进行屏蔽时使用的就是这个id。summary : 简短的总结通常5-6个字符描述问题而不是修复措施。explanation : 完整的问题解释和修复建议。category : 问题类别。详见下文详述部分。priority : 优先级。1-10的数字10为最重要/最严重。severity : 严重级别Fatal, Error, Warning, Informational, Ignore。Implementation : 为Issue和Detector提供映射关系Detector就是当前Detector。声明扫描检测的范围ScopeScope用来描述Detector需要分析时需要考虑的文件集包括Resource文件或目录、Java文件、Class文件。与Lint HTML报告对应关系 Category详述 系统现在已有的类别如下 LintCorrectness (incl. Messages)SecurityPerformanceUsability (incl. Icons, Typography)AccessibilityInternationalizationBi-directional text自定义Category public class MTCategory {public static final Category NAMING_CONVENTION Category.create(命名规范, 101);
}使用 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 {Overridepublic synchronized ListIssue 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属性 jar {manifest {attributes(Lint-Registry: com.meituan.android.lint.core.MTIssueRegistry)}
}至此自定义Lint的编码部分就完成了。之前提到自定义Lint是一个Java工程那么打出的jar包如何使用呢 jar包使用 Google方案 将jar拷贝到~/.android/lint中 $ mkdir ~/.android/lint/
$ cp customrule.jar ~/.android/lint/缺点针对所有工程会影响同一台机器其他工程的Lint检查。即便触发工程时拷贝过去执行完删除但其他进程或线程使用./gradlew lint仍可能会受到影响。 LinkedIn方案 LinkedIn提供了另一种思路 : 将jar放到一个aar中。这样我们就可以针对工程进行自定义Lintlint.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中并不会引起包体积变化。 缺点 官方plugin偶尔出bug给人一种不太重视的感觉。 目前plugin的支持情况是1.1.x正常1.2.x不支持1.3.x修复问题1.5.x正常。1.2.x Gradle plugin遇到的两个问题- Issue 174808:custom lint in AAR doesn’t work - Issue 178699:lint.jar in AAR doesn’t work sometimes 经过对比我们最终选择了LinkedIn的方案。 在确定方案后我们为Lint增加了很多功能包括编码规范和原生Lint增强。这里以HashMap检测为例介绍一下美团Lint。 增强HashMap检测 Lint检测中有一项是Java性能检测常见的就是HashMap can be replaced with SparseArray。 public static void testHashMap() {HashMapInteger, String map1 new HashMapInteger, String();map1.put(1, name);HashMapInteger, String map2 new HashMap();map2.put(1, name);MapInteger, String map3 new HashMap();map3.put(1, name);
}对于上述代码原生Lint只能检测第一种情况JDK 7泛型新写法还检测不到。了解到这点之后我们决定为HashMap提供增强检测。分析源码后发现HashMap检测是根据new HashMap处的泛型来判断是否符合条件。 于是我们想到在发现new HashMap后去找前面的泛型因为本身Java就是靠类型推断的我们可以直接根据前面的泛型来确定是否使用SparseArray。当然是不是HashMap还需要通过后面的new HashMap来判断否则容易出现问题。 代码如下 Override
public ListClass? extends Node getApplicableNodeTypes() {return Collections.Class? extends NodesingletonList(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() {Overridepublic 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) {StrictListAccessorTypeReference, TypeReference types reference.getTypeArguments();if (types null || types.size() ! 2) {/*JDK 7 新写法HashMapInteger, String map2 new HashMap();map2.put(1, name);MapInteger, 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-checks。需要的时候使用SdkConstants充分利用LintUtilsLint给我们提供了很多方便的工具。为自定义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 falselintTask.lintOptions newOptions自动添加最新aar 这里还涉及一个问题当我们plugin开发完成提供给团队使用的时候假设我们需要修改lint aar那么团队的plugin就要统一升级。这点就比较繁琐。考虑到plugin只是一个检查代码插件它最需要的应该是实时更新。 我们引入了Gradle Dynamic Versions project.dependencies {compile com.meituan.android.lint:lint:latest.integration
}project.configurations.all {resolutionStrategy.cacheDynamicVersionsFor 0, seconds
}plugin开发完成就可以提供给团队部署了。当然为了团队更方便地接入检查我们在检查流程中内置了脚本来自动添加plugin这样团队就可以在不添加任何代码的情况下实现自定义Lint检查。 Google. Writing Custom Lint Rules. Android Tools Project Site.Google. Writing a Lint Check. Android Tools Project Site.Prengemann M. The Power of Custom Lint Checks. SpeakerDeck.Diermann A. Custom Lint Rules. GitHub.Diermann A. Android Lint API Reference Guide. GitHub.Google. Android Custom Lint Rules Sample Code. GitHub.Cheng Yang. Writing Custom Lint Checks with Gradle. LinkedIn.