首页 体育 教育 财经 社会 娱乐 军事 国内 科技 互联网 房产 国际 女人 汽车 游戏

Android静态代码扫描效率优化与实践

2019-12-26

DevOps实践中,咱们在CI继续集成进程首要包含了代码提交、静态检测、单元测试、编译打包环节。其间静态代码检测能够在编码标准,代码缺点,功用等问题上提早预知,然后确保项意图交给质量。Android项目常用的静态扫描东西包含CheckStyle、Lint、FindBugs等,为下降接入本钱,美团点评集团内部孵化了静态代码扫描插件,调集了以上常用的扫描东西。项目初期引进集团内部基建时咱们接入了代码扫描插件,在PR流程中凭借Jenkins插件来触发自动化构建,然后到达监控代码质量的意图。初期单次构建耗时平均在1~2min左右,对研制功率影响甚少。可是跟着时刻推移,代码量随事务倍增,项目也开始运用Flavor来满意杂乱的需求,这使得咱们的单次PR构建到达了8~9min左右,其间静态代码扫描的时长约占50%,继续集成功率不高,对咱们的研制功率带来了应战。

针对以上的布景和问题,咱们考虑以下几个问题:

为了验证扫描东西的必要性,咱们关怀以下一些维度:

注:FindBugs只支撑Java1.0~1.8,现已被SpotBugs替代。鉴于部分老项目并没有迁移到Java8,现在咱们并没有运用SpotBugs替代FindBugs的原因如下,概况参阅官方文档。 一起,SpotBugs的作者也在评论是否让SpotBugs支撑老的Java版别,结论是不供给支撑。

经过以上的比照剖析咱们发现,东西的诞生都能针对性处理某一范畴问题。CheckStyle的扫描速度快功率高,对代码风格和圈杂乱度支撑友爱;FindBugs针对Java代码潜在问题,能协助咱们发现编码上的一些过错实践以及部分安全问题和功用问题;Lint是官方深度定制,功用极端强壮,且可定制性和扩展性以及全面性都体现杰出。所以归纳考虑,针对考虑一,咱们的结论是整合三种扫描东西,充分利用每一个东西的范畴特性。

已然挑选了整合这几种东西,咱们面对的应战是整合东西后扫描功率的问题,首先来剖析现在的插件究竟耗时在哪里。

Android项意图构建依靠Gradle东西,一次构建进程实际上是履行一切的Gradle Task。因为Gradle的特性,在构建时各个Module都需求履行CheckStyle、FindBugs、Lint相关的Task。关于Android来说,Task的数量还与其构建变体Variant有关,其间Variant = Flavor * BuildType。所以一个Module履行的相关使命能够由以下公式来描绘:Flavor * BuildType *,其间*为笛卡尔积。如下图所示:

能够看到,一次构建全量扫描履行的Task跟Varint个数正相关。关于现有工程的使命,咱们能够看一下现在各个使命的耗时状况:

经过对Task耗时排序,首要的耗时体现在FindBugs和Lint对每一个Module的扫描使命上,CheckStyle使命并不占首要影响。全体来看,除了东西本身的扫描时刻外,耗时首要分为多Module、多Variant带来的使命数量耗时。

关于东西本身的扫描时刻,一方面受东西本身扫描算法和检测规矩的影响,另一方面也跟扫描的文件数量相关。针对源码类型的东西比方CheckStyle和Lint,需求经过词法剖析、语法剖析生成笼统语法树,再遍历笼统语法树跟界说的检测规矩去匹配;而针对字节码文件的东西FindBugs,需求先编译源码成Class文件,再经过BCEL剖析字节码指令并与探测器规矩匹配。假如要在东西本身算法上去寻觅优化点,价值比较大也纷歧定能找到有用思路,投入产出比不高,所以咱们把精力放在削减Module和Variant带来的影响上。

从上面的耗时剖析能够知道,Module和Variant数直接影响使命数量, 一次PR提交的场景是多样的,比方多Module多Variant都有修正,所以要考虑这些都修正的场景。先剖析一个Module多Variant的场景,考虑到不同的Variant下源代码有必定差异,而且FindBugs扫描针对的是Class文件,不同的Variant都需求编译后才干扫描,直接对多Variant做处理比较杂乱。咱们能够简化问题,用以空间换时刻的办法,在提交PR的时分依据Variant用不同的Jenkins Job来履行每一个Variant的扫描使命。所以接下来的问题就转变为怎么优化在扫描单个Variant的时分多Module使命带来的耗时。

关于Module数而言,咱们能够将其抽取成组件,拆分到独立库房,将扫描使命拆分到各自库房的变化时期,以aar的办法集成到主项目来削减Module带来的使命数。那关于剩余的Module怎么优化呢?无论是哪一种东西,都是对其输入文件进行处理,CheckStyle对Java源代码文件处理,FindBugs对Java字节码文件处理,假如咱们能够经过一次使命搜集到一切Module的源码文件和编译后的字节码文件,咱们就能够削减多Module的使命了。所以关于全量扫描,咱们的首要方针是来处理怎么一次性搜集一切Module的方针文件。

上面的优化思路都是依据全量扫描的,处理的是多Module多Variant带来的使命数量耗时。前面说到,东西本身的扫描时刻也跟扫描的文件数量有关,那么是否能够从扫描的文件数量来下手呢?考虑平常的开发场景,提交PR时只是部分文件修正,咱们没必要把那些没修正过的存量文件再参加扫描,而只针对修正的增量文件扫描,这样能很大程度下降无效扫描带来的功率问题。有了思路,那么咱们考虑以下几个问题:

依据上面的剖析与考虑途径,接下来咱们详细介绍怎么处理上述问题。

获取一切Module方针文件集,首先要找出哪些Module参加了扫描。一个Module工程在Gradle构建体系中被描绘为一个“Project”,那么咱们只需求找出主工程依靠的一切Project即可。因为依靠装备的多样性,咱们能够挑选在某些Variant下依靠不同的Module,所以获取参加一次构建时与当时Variant相关的Project方针,咱们能够用如下办法:

static Set Project collectDepProject {
 if  {
 result = new HashSet 
 Set taskSet = variant.javaCompiler.taskDependencies.getDependencies
 taskSet.each { Task task - 
 if ) {
 result.add
 BaseVariant childVariant = getVariant
 if  == variant.name.toLowerCase) {
 collectDepProject
 return result
}

现在文件集分为两类,一类是源码文件,另一类是字节码文件,别离能够如下处理:

projectSet.each { targetProject - 
 if  GradleUtils.hasAndroidPlugin) {
 GradleUtils.getAndroidExtension.sourceSets.all { AndroidSourceSet sourceSet - 
 if  !sourceSet.name.startsWith) {
 source sourceSet.java.srcDirs
}

注:上面的Source是CheckStyle Task的特点,用其来指定扫描的文件调集;

// 扫除去一些模板代码class文件
static final Collection String defaultExcludes = .asImmutable
List ConfigurableFileTree allClassesFileTree = new ArrayList 
ConfigurableFileTree currentProjectClassesDir = project.fileTree
allClassesFileTree.add
GradleUtils.collectDepProject.each { targetProject - 
 if  GradleUtils.hasAndroidPlugin) {
 // 或许有的工程没有Flavor只要buildType
 GradleUtils.getAndroidVariants.each { BaseVariant targetProjectVariant - 
 if  == variant.buildType.name.toLowerCase) {
 allClassesFileTree.add)
}

注:搜集到字节码文件集后,能够用经过FindBugsTask 的 Class 特点指定扫描,后文会详细介绍FindBugs Task相关特点。

关于Lint东西而言,相应的Lint Task并没有相关属功用够指定扫描文件,所以在全量扫描上,咱们暂时没有针对Lint做优化。

经过对CheckStyle和FindBugs全量扫描的优化,咱们将全体扫描时刻由本来的9min下降到了5min左右。

由前面的考虑剖析咱们知道,并不是一切的文件每次都需求参加扫描,所以咱们能够经过增量扫描的办法来进步扫描功率。

在做详细技能计划之前,咱们先调研一下业界的现有计划,调研如下:

针对Lint,咱们能够学习现有完结思路,一起深入剖析扫描原理,在3.x版别上寻觅出增量扫描的处理计划。关于CheckStyle和FindBugs,咱们需求了解东西的相关装备参数,为其指定特定的差异文件调集。

注:业界有一些增量扫描的事例,例如diff_cover,此东西首要是对单元测试全体覆盖率的检测,以增量代码覆盖率作为一个方针来衡量项意图质量,可是这跟咱们的静态代码剖析的需求不太契合。它有一个比较好的思路是找出差异的代码行来剖析覆盖率,粒度比较细。可是关于静态代码扫描,只是的差异行不足以完结上下文的语义剖析,尤其是针对FindBugs这类需求剖析字节码的东西,获取的差异行还需求经过编译成Class文件才干进行剖析,计划并不可取。

增量扫描的第一步是获取待扫描的方针文件。咱们能够经过git diff指令来获取差异文件,值得注意的是关于删去的文件和重命名的文件需求疏忽,咱们更关怀新增和修正的文件,而且只需求获取差异文件的途径就好了。举个比如:git diff –name-only –diff-filter=dr commitHash1 commitHash2,以上指令意思是比照两次提交记载的差异文件并获取途径,过滤删去和重命名的文件。关于寻觅本地库房的差异文件上面的指令现已足够了,可是关于PR的状况还有一些杂乱,需求比照本地代码与长途库房方针分支的差异。集团的代码管理东西在Jenkins上有相应的插件,该插件默许供给了几个参数,咱们需求用到以下两个: - ${targetBranch}:需求合入代码的方针分支地址; - ${sourceCommitHash}:需求提交的代码hash值;

经过这两个参数履行以下一系列指令来获取与长途方针分支的差异文件。

git remote add upstream ${upstreamGitUrl}
git fetch upstream ${targetBranch}
git diff --name-only --diff-filter=dr $sourceCommitHash upstream/$targetBranch

经过以上办法,咱们找到了增量修正文件集。

在剖析Lint增量扫描原理之前,先介绍一下Lint扫描的作业流程:

项目中的源文件,包含Java、XML、资源文件、proGuard等。

用于装备期望扫除的任何 Lint 检查以及自界说问题严峻等级,一般各个项目都会依据本身项目状况自界说的lint.xml来扫除一些检查项。

一套完好的扫描东西用于对Android的代码结构进行剖析,能够经过指令行、IDEA、Gradle指令三种办法运转lint东西。

Lint扫描的输出成果。

从上面能够看出,Lint Tool就像一个加工厂,对投入进来的质料进行加工处理,得到终究的产品。Lint Tool作为一个扫描东西集,有多种运用办法。Android为咱们供给了三种运转办法,别离是指令行、IDEA、Gradle使命。这三种办法终究都异曲同工,经过LintDriver来完结扫描。如下图所示:

为了便利检查源码,新建一个工程,在build.gradle脚本中,增加如下依靠:

compile 'com.android.tools.build:gradle:3.1.1'
compile 'com.android.tools.lint:lint-gradle:26.1.1'

咱们能够得到如下所示的依靠:

Lint东西集的一个封装,完结了一组API接口,用于发动Lint;

一组内建的检测器,用于对这种描绘好Issue进行剖析处理;

能够看做是依靠上面两个jar构成的一个依据指令行的封装接口构成的脚手架工程,咱们的指令行、Gradle使命都是承继自这个jar包中相关类来做的完结;

能够看做是针对Gradle使命这种运转办法,依据lint-26.1.1做了一些封装类;

真实Gradle Lint使命在履行时调用的进口;

在了解清楚了以上几个jar的联系和效果之后,咱们能够发现Lint的中心库其实是前三个依靠。后边两个其实是依据脚手架,对Gradle这种运转办法做的封装。最中心的逻辑在LintDriver的Analyze办法中。

fun analyze {
 ...省掉部分代码...
 for  {
 fireEvent
 registerCustomDetectors
 ...省掉部分代码...
 try {
 for  {
 phase = 1
 val main = request.getMainProject
 // The set of available detectors varies between projects
 computeDetectors
 if ) {
 // No detectors enabled in this project: skip it
 continue
 checkProject
 if  {
 break
 runExtraPhases
 } catch  {
 // Process canceled etc
 if ) {
 cancel
 ...省掉部分代码...
}

首要是以下三个重要过程:

Lint为咱们供给了许多内建的检测器,除此之外咱们还能够自界说一些检测器,这些都需求注册进Lint东西用于对方针文件进行扫描。这个办法首要做以下几件工作:

热门文章

随机推荐

推荐文章