思路其实很简单的,流程很简单
gradle 插件 copy git hooks------> git hooks 自动执行增量扫描的任务 ------> git diff 找到增量代码 ------> lint-api.jar 调用 project.addfile() 扫描增量代码 ------>javamail 发送问题邮件 ------>git reset 回滚代码
好了现在你已经得到我的大乘佛法了,你可以屁颠屁颠地回大唐娶妻生子走向人生巅峰了,我保证我不阻止你。
找到增量代码
这个命令感谢我的另一个同事马老板,他坐为旁边,我每次急躁的时候他都耐心帮我找答案。
- private List<String> getPostCommitChange() {
- ArrayList<String> filterList = new ArrayList<String>()
- try {
- String projectDir = getProject().getProjectDir()
- String commond = "git diff --name-only --diff-filter=ACMRTUXB HEAD~1 HEAD~0 $projectDir"
- String changeInfo = commond.execute(null, project.getRootDir()).text.trim()
- if (changeInfo == null || changeInfo.empty) {
- return filterList
- }
- String[] lines = changeInfo.split("\\n")
- return lines.toList()
- } catch (Exception e) {
- return filterList
- }
- }
用 git diff 命令找到刚提交的 commit 都改动了哪些文件,我讲一下他的每个参数的意思
这里着重说一下在 gralde 里写命令的一个注意点,要执行带有单引号的命令会执行为空的问题
譬如
- git status -s | grep -v '^D'//列出当前要提交的commit变动了哪些文件并排除删除的文件
你以为
就行了吗,太天真了,执行结果为空,刚开始我以为只要加上转义符就行,结果还是不行。后面反复实验发现要这么写
- "git status -s | grep -v '^D'".execute
- ["/bin/bash", "-c", "git status -s | grep -v '^D'"].execute()
增量代码扫描具体实现
原理比较长,怕大家看的似懂非懂,我先给结果,这样比较好。看到一些不明白的名词可以先忽略掉,后面原理里面会提,我尽量讲的浅显易懂。
我写了一个增量扫描的 task,然后写了一个 LintClinet,这个 LintClient 会扫描代码,它继承 android gradle 的 LintGradleClient,task 会调用这个 client 的 run 方法,run 方法就是扫描方法。
而增量扫描的关键性代码是修改 LintGradleClient 的 createLintRequest 方法,往 project 加入要扫描的文件
- @Override
- protected LintRequest createLintRequest(@NonNull List<File> files) {
- //注意这个project是com.android.tools.lint.detector.api.project
- LintRequest lintRequest = super.createLintRequest(files);
- for (Project project : lintRequest.getProjects()) {
- project.addFile(changefile);//加入要扫描的文件
- addChangeFiles(project);
- }
- return lintRequest;
- }
有个注意点我要提一下
LintGradleClient 构造函数需要参数,除了 variant 可以为空,其他都不能为空。因为不在 android gradle 插件内部,所以有些参数获取需要动一些脑筋。
- LintGradleClient(
- IssueRegistry registry,//扫描规则
- LintCliFlags flags,
- org.gradle.api.Project gradleProject,//gradle 项目
- AndroidProject modelProject,// android项目
- File sdkHome,// android sdk目录
- Variant variant,//编译的Variant
- BuildToolInfo buildToolInfo) {//编译工具包
篇幅有限,参数讲太多反而把大家搞糊涂,我就讲一个参数,如何获取 AndroidProject
- private AndroidProject getAndroidProject() {
- GradleConnector gradleConn = GradleConnector.newConnector()
- gradleConn.forProjectDirectory(getProject().getProjectDir())
- AndroidProject modelProject = gradleConn.connect().getModel(AndroidProject.class)
- return modelProject
- }
增量代码扫描原理分析
刚开始想的很简单呀,命令行 Lint 不是也能扫描代码吗,那里面肯定有指定扫描文件和目录的参数吧,别说还真有, --sources <dir> ,结果一试,发现是有结果,但是扫描出来的问题根本不是那个文件的问题呀,然后我同事说在他电脑却提示不能扫描 gradle 项目,一下子就蒙蔽了,无从下手的感觉,刚开始我以为命令没用对,但是改来改去都不对,后面我尝试去除里面的 gradle project 判断限制,然后指定扫描文件,还是扫描不出该有的问题,我就先暂停这个方案的研究。
既然上面这条路走不通,我就去找 android studio 的源码看他是怎么实现增量扫描的,结果在 Android Studio 源码里面,搜索 lint 根本没有找到任何相关的代码,后面发现其实是在另外的 Plugin 源码里。不过他依赖于 Intellij Module,Module 会找到每个类,那我又没有 Module 这个上下文,这么说这个方案还是走不通。
那就再换一个思路,Android Gradle 插件不是也可以实现 Lint 扫描,那我改一改不就可以增量扫描,结果一拿到他的代码就感觉无从下手,改来改去都不对呀,不知道哪一行代码可以实现增量扫描,就算后面完成了增量扫码,扫描也很慢。
带着上面的几个坑,我研究了 Lint 内部的实现原理找到了增量代码扫描的实现方法
我先讲一下关于 Lint 的预备知识,然后再来讲上面几个问题,方便大家更好理解
Lint 扫描内部原理
其实无论是 Lint 命令行、android gradle 插件、android studio 都依赖了两个 jar
lint-api.jar:lint-api 是代码扫描的具体实现
lint-check.jar:lint-check 是默认的扫描规则
lint-api.jar 内部实现原理:
LintDriver 调用 analyze() 分析 LintRequest 中的文件 ------>checkProject----->runFileDetectors----->check 对应文件的 Visitor,譬如 JavaPsiVisitor 分析 java 文件,AsmVisitor 分析 class 文件等
下面讲讲三种方式分别怎么实现的
Lint 命令行:
lint.sh------>lint.jar------>LintCliClient 的 run(IssueRegistry registry, List<File> files)------>LintDriver analyze 分析 project
Lint Gradle Task:
Lint.groovy------>LintGradleClient 的 run(IssueRegistry registry)------>LintDriver analyze 分析 LintGradleProject
Android Studio:
AndroidLintGlobalInspectionContext------> performPreRunActivities-----> LintDriver analyze 分析 IntellijLintProject
明白了原理,我们回到上面两个问题
我举个例子:
譬如有个 TestActivity 里面写了静态的 activity 变量,LeakDetector 会去检查这个情况,但是直接 lint --sources app/src/com/demo/TestActivity.java . 你会发现扫描不出这个错误或者提示'app' is a Gradle project. To correctly analyze Gradle projects, you should run "gradlew :lint" instead. [LintError],其实这两个问题都是同一个原因。
LeakDetector 会去判断静态变量是不是 Activity 类,但是变量的 PsiField 却是 com.demo.TestActivity 不是'android'开头,这样就扫描不出问题了。
- @Override
- public
- void
- visitField
- (PsiField field)
- {
- String fqn= field.getType().getCanonicalText();
- if (fqn.startsWith("android.")) {//fqn变量是com.demo.TestActivity
- if (isLeakCandidate(cls, mContext.getEvaluator())
- && !isAppContextName(cls, field)) {
- String message = "Do not place Android context classes in static fields; "
- + "this is a memory leak (and also breaks Instant Run)";
- report(field, modifierList, message);
- }
- }
- }
那为什么 fqn 不是 android.app.activity 呢,因为 lint 命令行会把 lib 目录下面 jar 的 class 加入扫描形成抽象语法树,但是 gradle 项目是 compile jar 的,不在 lib 目录下面,这就是为什么高版本的 lint 里面提示不能扫描 gradle 项目。这也侧面说明了命令行 lint 走不通
android studio 内部会扫描 IntellijLintProject 中的文件,IntellijLintProject 是由
create(IntellijLintClient client, List<VirtualFile> files,Module... modules) 生成的,那就只要找到文件加入 project 的代码就能找到增量代码扫描的方案了。
- if (project != null) {
- project.setDirectLibraries(Collections.<Project>emptyList());
- if (file != null) {
- project.addFile(VfsUtilCore.virtualToIoFile(file));
- }
- }
那为什么 addfile 以后 LintDriver 会增量扫描呢, 拿 java 文件扫描举个例子,LintDriver 会判断 subset 是不是为空,不为空就不扫描 JavaSourceFolders,只扫描增量文件。
- List<File> files = project.getSubset();
- if (files != null) {//判断是不是要增量扫描
- checkIndividualJavaFiles(project, main, checks, files);
- } else {
- List<File> sourceFolders = project.getJavaSourceFolders();
- List<File> testFolders = scope.contains(Scope.TEST_SOURCES)
- ? project.getTestSourceFolders() : Collections.emptyList();
- checkJava(project, main, sourceFolders, testFolders, checks);
- }
只扫描优先级高的问题
虽然 Lint 支持配置 lint.xml 去忽略 Issue,但是只能一个个忽略,我的方案是设置优先级低的规则为 Severity.IGNORE,LintDirver 会忽略 Severity.IGNORE 的规则
- @Override
- public Severity getSeverity(Issue issue) {
- Severity severity = super.getSeverity(issue);
- if (onlyHighPriority) {
- if (issue.getCategory().compareTo(Category.USABILITY) < 0 && issue.getPriority() > 4) {//只扫描优先级比较高的规则
- return severity;
- }
- return Severity.IGNORE;
- }
- return severity;
- }
自动执行代码扫描
Git Hooks 提供了 post-commit 实现 commit 之后自动执行任务,但是你会发现在 post-commit 里写 ./gradlew Lint, 还是要等 lint 任务执行完了才 commit 成功。我发现只要在 shell 脚本里加入 &>/dev/null 就可以后台执行了。
- nohup. / gradlew LintIncrement & >/dev/null &
自动同步 Git Hooks
如果 Git Hooks 脚本需要每台电脑自己去复制,这明显不利于团队合作,而且不方便后面更新脚本,我选择用 Gradle 命令复制到指定目录,但是这里有个问题,gradle 插件能带资源文件吗,如果没有专门学过 gradle 说不定一时无从下手,还好我刚好以前看过 fastdex 里面是怎么解决的, 通过 getResourceAsStream 可以复制 Gradle 插件 resources 下面的文件
- public
- static
- void
- copyResourceFile
- (
- String name, File dest
- ) throws IOException
- {
- FileOutputStream os = null;
- File parent = dest.getParentFile();
- if (parent != null && (!parent.exists())) {
- parent.mkdirs();
- }
- InputStream is = null;
- try {
- is = FileUtils.class.getResourceAsStream("/" + name);
- os = new FileOutputStream(dest, false);
- byte[] buffer = new byte[BUFFER_SIZE];
- int length;
- while ((length = is.read(buffer)) > 0) {
- os.write(buffer, 0, length);
- }
- } finally {
- if (is != null) {
- is.close();
- }
- if (os != null) {
- os.close();
- }
- }
- }
复制脚本 installGitHooks 是这样实现的,finalizedBy 保证它在 build 任务后面自动执行,它会把 / resource/post-commit 文件复制到工程. git/hooks/post-commit。chmod -R +x .git/hooks / 一定要写,不然没有权限
- private
- void
- createGitHooksTask
- (Project project)
- {
- def preBuild = project.tasks.findByName("preBuild")
- if (preBuild == null) {
- throw new GradleException("lint need depend on preBuild and clean task")
- return
- }
- def installGitHooks = project.getTasks().create("installGitHooks")
- .doLast {
- File postCommitFile = new File(project.rootProject.rootDir, PATH_POST_COMMIT)
- if (lintIncrementExtension.isCheckPostCommit()) {
- FileUtils.copyResourceFile("post-commit", postCommitFile)
- } else {
- if (preCommitDestFile.exists()) {
- preCommitDestFile.delete()
- }
- }
- Runtime.getRuntime().exec("chmod -R +x .git/hooks/")
- }
- preBuild.finalizedBy installGitHooks
- }
Gradle 插件实现发送邮件
原来打算直接用 shell 脚本里面的 sendmail 去发送邮件的,但是听同事说如果 mac 上没有登录邮箱是没法发送成功的,我就用了 javamail,网上的方案大多数是在 java 里面实现 javamail,在 gradle 里面发送邮件的方案比较少,我尝试了多次才解决。
首先在 gradle 插件的 build.gradle 里面加入 javamail 的依赖,刚开始我是直接 compile 了,但是运行以后提示我没找到 javamail 的类,原来是要 ant 能找到 javamail 的类才行
- configurations {
- antClasspath
- }
- dependencies {
- antClasspath 'ant:ant-javamail:1.+'
- antClasspath 'javax.activation:activation:1.1.1'
- antClasspath 'javax.mail:mail:1.+'
- }
- ClassLoader antClassLoader = org.apache.tools.ant.Project.class.classLoader
- configurations.antClasspath.each { File jar ->
- antClassLoader.addURL( jar.toURI().toURL() )
- }
然后在 gralde 里面执行发送任务
- void send(File file) {
- getProject().ant.mail(
- from: fromMail,// 发件方
- tolist: toList,//收件方
- ccList: ccList,//抄送方
- message: message,//消息内容
- subject: subject,//标题
- mailhost: mailhost,//SMTP转发服务器
- messagemimetype: "text/html",//消息格式
- files: file.getAbsolutePath()//发送文件目录
- )
- }
这里有几个注意点
发现问题回滚代码
- if (lintClient.haveErrors() ) {
- "git reset HEAD~1".execute(null, project.getRootDir())
- }
如何调试 gradle 插件
我原来看了几篇 Lint 原理分析就打算去实现增量扫描,然后发现看和做还是不一样的,中间遇到好多问题,还好 gradle 插件可以调试。
第一步 点击 edit configurations
第二步 创建 remote,默认选项就可以
第三步 在你要运行的 gradle 任务里面加入
- -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
第四步,先点击运行你要运行的 gradle 任务,gradle 会等待你点击 remote,然后就可以调试了
Lint B 版本变动
发现 android gradle 最新的几个版本对于 lint 做了一些优化,我顺便提一下。
2.3.0 以后运行./gradlew lint 会更快,Google 实现了 LintCharSequence 来完成数据的存储和传参,实现了内存中只有一份拷贝
2.3.0 以后 lint-report.html 是 material design,更好看、更方便查问题
2.3.0 以后支持 baseline 增量显示 bug
3.0.0 以后自定义 lint 规则就不用像原来美团的方法一样麻烦了,官方支持
扫描会更快,uast 语法树替换了现在的 psi 和 lombok 语法树
尾声
回过头来看,其实增量扫描也很简单,就一行关键性代码
。
- project.addfile(file)
最后讲一下大家关心的开源问题吧,那要等在公司内部稳定运行以后在公司 Github 地址开源,毕竟我们是一款严肃的产品嘛。
来源: http://mp.weixin.qq.com/s/RMz0y0nXatupoUomoWOtmQ