2024年最全Android静态代码扫描效率优化与实践(2),安卓面试项目源码

结语

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!以下是目录截图:

由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。

再附一部分Android架构面试视频讲解:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  1. 配置远程分支别名为UpStream,其中upstreamGitUrl可以在插件提供的配置属性中设置;
  2. 获取远程目标分支的更新;
  3. 比较分支差异获取文件路径。

通过以上方式,我们找到了增量修改文件集。

Lint扫描原理分析

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

image

App Source Files

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

lint.xml

用于配置希望排除的任何 Lint 检查以及自定义问题严重级别,一般各个项目都会根据自身项目情况自定义的lint.xml来排除一些检查项。

lint Tool

一套完整的扫描工具用于对Android的代码结构进行分析,可以通过命令行、IDEA、Gradle命令三种方式运行lint工具。

lint Output

Lint扫描的输出结果。

从上面可以看出,Lint Tool就像一个加工厂,对投入进来的原料(源代码)进行加工处理(各种检测器分析),得到最终的产品(扫描结果)。Lint Tool作为一个扫描工具集,有多种使用方式。Android为我们提供了三种运行方式,分别是命令行、IDEA、Gradle任务。这三种方式最终都殊途同归,通过LintDriver来实现扫描。如下图所示:

image

为了方便查看源码,新建一个工程,在build.gradle脚本中,添加如下依赖:

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

我们可以得到如下所示的依赖:

image

lint-api-26.1.1

Lint工具集的一个封装,实现了一组API接口,用于启动Lint;

lint-checks-26.1.1

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

lint-26.1.1

可以看做是依赖上面两个jar形成的一个基于命令行的封装接口形成的脚手架工程,我们的命令行、Gradle任务都是继承自这个jar包中相关类来做的实现;

lint-gradle-26.1.1

可以看做是针对Gradle任务这种运行方式,基于lint-26.1.1做了一些封装类;

lint-gradle-api-26.1.1

真正Gradle Lint任务在执行时调用的入口;

在理解清楚了以上几个jar的关系和作用之后,我们可以发现Lint的核心库其实是前三个依赖。后面两个其实是基于脚手架,对Gradle这种运行方式做的封装。最核心的逻辑在LintDriver的Analyze方法中。

fun analyze() {

…省略部分代码…

for (project in projects) {
fireEvent(EventType.REGISTERED_PROJECT, project = project)
}
registerCustomDetectors(projects)

…省略部分代码…

try {
for (project in projects) {
phase = 1

val main = request.getMainProject(project)

// The set of available detectors varies between projects
computeDetectors(project)

if (applicableDetectors.isEmpty()) {
// No detectors enabled in this project: skip it
continue
}

checkProject(project, main)
if (isCanceled) {
break
}

runExtraPhases(project, main)
}
} catch (throwable: Throwable) {
// Process canceled etc
if (!handleDetectorError(null, this, throwable)) {
cancel()
}
}
…省略部分代码…
}

主要是以下三个重要步骤:

  • registerCustomDetectors(projects)

Lint为我们提供了许多内建的检测器,除此之外我们还可以自定义一些检测器,这些都需要注册进Lint工具用于对目标文件进行扫描。这个方法主要做以下几件事情:

  1. 遍历每一个Project和它的依赖Library工程,通过client.findRuleJars来找出自定义的jar包;
  2. 通过client.findGlobalRuleJars找出全局的自定义jar包,可以作用于每一个Android工程;
  3. 从找到的jarFiles列表中,解析出自定义的规则,并与内建的Registry一起合并为CompositeIssueRegistry; 需要注意的是,自定义的Lint的jar包存放位置是build/intermediaters/lint目录,如果是需要每一个工程都生效,则存放位置为~/.android/lint/。
  • computeDetectors(project)

这一步主要用来收集当前工程所有可用的检测器。

checkProject(project, main)接下来这一步是最为关键的一步。在此方法中,调用runFileDetectors来进行文件扫描。Lint支持的扫描文件类型很多,因为是官方支持,所以针对Android工程支持的比较友好。一次Lint任务运行时,Lint的扫描范围主要由Scope来描述。具体表现在:

fun infer(projects: Collection?): EnumSet {
if (projects == null || projects.isEmpty()) {
return Scope.ALL
}

// Infer the scope
var scope = EnumSet.noneOf(Scope::class.java)
for (project in projects) {
val subset = project.subset
if (subset != null) {
for (file in subset) {
val name = file.name
if (name == ANDROID_MANIFEST_XML) {
scope.add(MANIFEST)
} else if (name.endsWith(DOT_XML)) {
scope.add(RESOURCE_FILE)
} else if (name.endsWith(DOT_JAVA) || name.endsWith(DOT_KT)) {
scope.add(JAVA_FILE)
} else if (name.endsWith(DOT_CLASS)) {
scope.add(CLASS_FILE)
} else if (name.endsWith(DOT_GRADLE)) {
scope.add(GRADLE_FILE)
} else if (name == OLD_PROGUARD_FILE || name == FN_PROJECT_PROGUARD_FILE) {
scope.add(PROGUARD_FILE)
} else if (name.endsWith(DOT_PROPERTIES)) {
scope.add(PROPERTY_FILE)
} else if (name.endsWith(DOT_PNG)) {
scope.add(BINARY_RESOURCE_FILE)
} else if (name == RES_FOLDER || file.parent == RES_FOLDER) {
scope.add(ALL_RESOURCE_FILES)
scope.add(RESOURCE_FILE)
scope.add(BINARY_RESOURCE_FILE)
scope.add(RESOURCE_FOLDER)
}
}
} else {
// Specified a full project: just use the full project scope
scope = Scope.ALL
break
}
}
}

可以看到,如果Project的Subset为Null,Scope就为Scope.ALL,表示本次扫描会针对能检测的所有范围,相应地在扫描时也会用到所有全部的Detector来扫描文件。

如果Project的Subset不为Null,就遍历Subset的集合,找出Subset中的文件分别对应哪些范围。其实到这里我们已经可以知道,Subset就是我们增量扫描的突破点。接下来我们看一下runFileDetectors:

if(scope.contains(Scope.JAVA_FILE)||scope.contains(Scope.ALL_JAVA_FILES)){
val checks = union(scopeDetectors[Scope.JAVA_FILE],scopeDetectors[Scope.ALL_JAVA_FILES])
if (checks != null && !checks.isEmpty()) {
val files = project.subset
if (files != null) {
checkIndividualJavaFiles(project, main, checks, files)
} else {
val sourceFolders = project.javaSourceFolders
val testFolders = if (scope.contains(Scope.TEST_SOURCES))
project.testSourceFolders
else
emptyList ()
val generatedFolders = if (isCheckGeneratedSources)
project.generatedSourceFolders
else
emptyList ()
checkJava(project, main, sourceFolders, testFolders, generatedFolders, checks)
}
}
}

这里更加明确,如果project.subset不为空,就对单独的Java文件扫描,否则,就对源码文件和测试目录以及自动生成的代码目录进行扫描。整个runFileDetectors的扫描顺序入下:

  1. Scope.MANIFEST
  2. Scope.ALL_RESOURCE_FILES)|| scope.contains(Scope.RESOURCE_FILE) || scope.contains(Scope.RESOURCE_FOLDER) || scope.contains(Scope.BINARY_RESOURCE_FILE)
  3. scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)
  4. scope.contains(Scope.CLASS_FILE) || scope.contains(Scope.ALL_CLASS_FILES) || scope.contains(Scope.JAVA_LIBRARIES)
  5. scope.contains(Scope.GRADLE_FILE)
  6. scope.contains(Scope.OTHER)
  7. scope.contains(Scope.PROGUARD_FILE)
  8. scope.contains(Scope.PROPERTY_FILE)

官方文档的描述顺序一致。

现在我们已经知道,增量扫描的突破点其实是需要构造project.subset对象。

/**

  • Adds the given file to the list of files which should be checked in this
  • project. If no files are added, the whole project will be checked.
  • @param file the file to be checked
    */
    public void addFile(@NonNull File file) {
    if (files == null) {
    files = new ArrayList<>();
    }
    files.add(file);
    }

/**

  • The list of files to be checked in this project. If null, the whole
  • project should be checked.
  • @return the subset of files to be checked, or null for the whole project
    */
    @Nullable
    public List getSubset() {
    return files;
    }

注释也很明确的说明了只要Files不为Null,就会扫描指定文件,否则扫描整个工程。

Lint增量扫描Gradle任务实现

前面分析了如何获取差异文件以及增量扫描的原理,分析的重点还是侧重在Lint工具本身的实现机制上。接下来分析,在Gradle中如何实现一个增量扫描任务。大家知道,通过执行./gradlew lint命令来执行Lint静态代码检测任务。创建一个新的Android工程,在Gradle任务列表中可以在Verification这个组下面找到几个Lint任务,如下所示:

image

这几个任务就是 Android Gradle插件在加载的时候默认创建的。分别对应于以下几个Task:

  • lint->LintGlobalTask:由TaskManager创建;
  • lintDebug、lintRelease、lintVitalRelease->LintPerVariantTask:由ApplicationTaskManager或者LibraryTaskManager创建,其中lintVitalRelease只在release下生成;

所以,在Android Gradle 插件中,应用于Lint的任务分别为LintGlobalTask和LintPerVariantTask。他们的区别是前者执行的是扫描所有Variant,后者执行只针对单独的Variant。而我们的增量扫描任务其实是跟Variant无关的,因为我们会把所有差异文件都收集到。无论是LintGlobalTask或者是LintPerVariantTask,都继承自LintBaseTask。最终的扫描任务在LintGradleExecution的runLint方法中执行,这个类位于lint-gradle-26.1.1中,前面提到这个库是基于Lint的API针对Gradle任务做的一些封装。

/** Runs lint on the given variant and returns the set of warnings */
private Pair<List, LintBaseline> runLint(
@Nullable Variant variant,
@NonNull VariantInputs variantInputs,
boolean report, boolean isAndroid) {
IssueRegistry registry = createIssueRegistry(isAndroid);
LintCliFlags flags = new LintCliFlags();
LintGradleClient client =
new LintGradleClient(
descriptor.getGradlePluginVersion(),
registry,
flags,
descriptor.getProject(),
descriptor.getSdkHome(),
variant,
variantInputs,
descriptor.getBuildTools(),
isAndroid);
boolean fatalOnly = descriptor.isFatalOnly();
if (fatalOnly) {
flags.setFatalOnly(true);
}
LintOptions lintOptions = descriptor.getLintOptions();
if (lintOptions != null) {
syncOptions(
lintOptions,
client,
flags,
variant,
descriptor.getProject(),
descriptor.getReportsDir(),
report,
fatalOnly);
} else {
// Set up some default reporters
flags.getReporters().add(Reporter.createTextReporter(client, flags, null,
new PrintWriter(System.out, true), false));
File html = validateOutputFile(createOutputPath(descriptor.getProject(), null, “.html”,
null, flags.isFatalOnly()));
File xml = validateOutputFile(createOutputPath(descriptor.getProject(), null, DOT_XML,
null, flags.isFatalOnly()));
try {
flags.getReporters().add(Reporter.createHtmlReporter(client, html, flags));
flags.getReporters().add(Reporter.createXmlReporter(client, xml, false));
} catch (IOException e) {
throw new GradleException(e.getMessage(), e);
}
}
if (!report || fatalOnly) {
flags.setQuiet(true);
}
flags.setWriteBaselineIfMissing(report && !fatalOnly);

Pair<List, LintBaseline> warnings;
try {
warnings = client.run(registry);
} catch (IOException e) {
throw new GradleException(“Invalid arguments.”, e);
}

if (report && client.haveErrors() && flags.isSetExitCode()) {
abort(client, warnings.getFirst(), isAndroid);
}

return warnings;
}

我们在这个方法中看到了warnings = client.run(registry),这就是Lint扫描得到的结果集。总结一下这个方法中做了哪些准备工作用于Lint扫描: 1. 创建IssueRegistry,包含了Lint内建的BuiltinIssueRegistry; 2. 创建LintCliFlags; 3. 创建LintGradleClient,这里面传入了一大堆参数,都是从Gradle Android 插件的运行环境中获得; 4. 同步LintOptions,这一步是将我们在build.gralde中配置的一些Lint相关的DSL属性,同步设置给LintCliFlags,给真正的Lint 扫描核心库使用; 5. 执行Client的Run方法,开始扫描。

扫描的过程上面的原理部分已经分析了,现在我们思考一下如何构造增量扫描的任务。我们已经分析到扫描的关键点是client.run(registry),所以我们需要构造一个Client来执行扫描。一个想法是通过反射来获取Client的各个参数,当然这个思路是可行的,我们也验证过实现了一个用反射方式构造的Client。但是反射这种方式有个问题是丢失了从Gradle任务执行到调用Lint API开始扫描这一过程中做的其他事情,侵入性比较高,所以我们最终采用继承LintBaseTask自行实现增量扫描任务的方式。

FindBugs扫描简介

FindBugs是一个静态分析工具,它检查类或者JAR 文件,通过Apache的BCEL库来分析Class,将字节码与一组缺陷模式进行对比以发现问题。FindBugs自身定义了一套缺陷模式,目前的版本3.0.1内置了总计300多种缺陷,详细可参考官方文档。FindBugs作为一个扫描的工具集,可以非常灵活的集成在各种编译工具中。接下来,我们主要分析在Gradle中FindBugs的相关内容。

Gradle FindBugs任务属性分析

在Gradle的内置任务中,有一个FindBugs的Task,我们看一下官方文档对Gradle属性的描述。

选几个比较重要的属性介绍:

  • Classes 该属性表示我们要分析的Class文件集合,通常我们会把编译结果的Class目录用于扫描。
  • Classpath 分析目标集合中的Class需要用到的所有相关的Classes路径,但是并不会分析它们自身,只用于扫描。
  • Effort 包含MIN,Default,MAX,级别越高,分析得越严谨越耗时。
  • findBugs ClasspathFinbugs库相关的依赖路径,用于配置扫描的引擎库。
  • reportLevel 报告级别,分为Low,Medium,High。如果为Low,所有Bug都报告,如果为High,仅报告High优先级。
  • Reports 扫描结果存放路径。

通过以上属性解释,不难发现要FindBugs增量扫描,只需要指定Classes的文件集合就可以了。

FindBugs任务增量扫描分析

在做增量扫描任务之前,我们先来看一下FindBugs IDEA插件是如何进行单个文件扫描的。

image

我们选择Analyze Current File对当前文件进行扫描,扫描结果如下所示:

image

可以看到确实只扫描了一个文件。那么扫描到底使用了哪些输入数据呢,我们可以通过扫描结果的提示清楚看到:

image

这里我们能看到很多有用的信息:

  • 源码目录列表,包含了工程中的Java目录,res目录,以及编译过程中生成的一些类目录;
  • 需要分析的目标Class集合,为编译后的Build目录下的当前Java文件对应的Class文件;
  • Aux Classpath Entries,表示分析上面的目标文件需要用到的类路径。

所以,根据IDEA的扫描结果来看,我们在做增量扫描的时候需要解决上面这几个属性的获取。在前面我们分析的属性是Gradle在FindBugs lib的基础上,定义的一套对应的Task属性。真正的FinBugs属性我们可以通过官方文档或者源码中查到。

配置AuxClasspath

前文提到,ClassPath是用来分析目标文件需要用到的相关依赖Class,但本身并不会被分析,所以我们需要尽可能全的找到所有的依赖库,否则在扫描的时候会报依赖的类库找不到。

FileCollection buildClasses = project.fileTree(dir: “ p r o j e c t . b u i l d D i r / i n t e r m e d i a t e s / c l a s s e s / {project.buildDir}/intermediates/classes/ project.buildDir/intermediates/classes/{variant.flavorName}/${variant.buildType.name}”,includes: classIncludes)

FileCollection targetClasspath = project.files()
GradleUtils.collectDepProject(project, variant).each { targetProject ->
GradleUtils.getAndroidVariants(targetProject).each { targetVariant ->
if (targetVariant.name.capitalize().equalsIgnoreCase(variant.name.capitalize())) {
targetClasspath += targetVariant.javaCompile.classpath
}
}
}

classpath = variant.javaCompile.classpath + targetClasspath + buildClasses

FindBugs增量扫描误报优化

对于增量文件扫描,参与的少数文件扫描在某些模式规则上可能会出现误判,但是全量扫描不会有问题,因为参与分析的目标文件是全集。举一个例子:

class A {
public static String buildTime = “”;

}

静态变量buildTime会被认为应该加上Final,但是其实其他类会对这个变量赋值。如果单独扫描类A文件,就会报缺陷BUG_TYPE_MS_SHOULD_BE_FINAL。我们通过Findbugs-IDEA插件来扫描验证,也同样会有一样的问题。要解决此类问题,需要找到谁依赖了类A,并且一同参与扫描,同时也需要找出类A依赖了哪些文件,简单来说:需要找出与类A有直接关联的类。为了解决这个问题,我们通过ASM来找出相关的依赖,具体如下:

void findAllScanClasses(ConfigurableFileTree allClass) {
allScanFiles = [] as HashSet
String buildClassDir = “ p r o j e c t . b u i l d D i r / {project.buildDir}/ project.buildDir/FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN”

Set moduleClassFiles = allClass.files
for (File file : moduleClassFiles) {
String[] splitPath = file.absolutePath.split(“ F I N D B U G S A N A L Y S I S D I R / FINDBUGS_ANALYSIS_DIR/ FINDBUGSANALYSISDIR/FINDBUGS_ANALYSIS_DIR_ORIGIN/”)
if (splitPath.length > 1) {
String className = getFileNameNoFlag(splitPath[1],‘.’)
String innerClassPrefix = “”
if (className.contains(‘$’)) {
innerClassPrefix = className.split(‘\$’)[0]
}
if (diffClassNamePath.contains(className) || diffClassNamePath.contains(innerClassPrefix)) {
allScanFiles.add(file)
} else {
Iterable classToResolve = new ArrayList()
classToResolve.add(file.absolutePath)
Set dependencyClasses = Dependencies.findClassDependencies(project, new ClassAcceptor(), buildClassDir, classToResolve)
for (File dependencyClass : dependencyClasses) {
if (diffClassNamePath.contains(getPackagePathName(dependencyClass))) {
allScanFiles.add(file)
break
}
}
}
}
}
}

通过以上方式,我们可以解决一些增量扫描时出现的误报情况,相比IDEA工具,我们更进一步降低了扫描部分文件的误报率。

CheckStyle增量扫描

相比而言,CheckStyle的增量扫描就比较简单了。CheckStyle对源码扫描,根据官方文档各个属性的描述,我们发现只要指定Source属性的值就可以指定扫描的目标文件。

void configureIncrementScanSource() {
boolean isCheckPR = false
DiffFileFinder diffFileFinder

if (project.hasProperty(CodeDetectorExtension.CHECK_PR)) {
isCheckPR = project.getProperties().get(CodeDetectorExtension.CHECK_PR)
}

if (isCheckPR) {
diffFileFinder = new DiffFileFinderHelper.PRDiffFileFinder()
} else {
diffFileFinder = new DiffFileFinderHelper.LocalDiffFileFinder()
}

source diffFileFinder.findDiffFiles(project)

if (getSource().isEmpty()) {
println ‘没有找到差异java文件,跳过checkStyle检测’
}
}

经过全量扫描和增量扫描的优化,我们整个扫描效率得到了很大提升,一次PR构建扫描效率整体提升50%+。优化数据如下:

关于面试的充分准备

一些基础知识和理论肯定是要背的,要理解的背,用自己的语言总结一下背下来。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,我能明显感觉到国庆后多了很多高级职位,所以努力让自己成为高级工程师才是最重要的。

好了,希望对大家有所帮助。

接下来是整理的一些Android学习资料,有兴趣的朋友们可以关注下我免费领取方式

①Android开发核心知识点笔记

②对标“阿里 P7” 40W+年薪企业资深架构师成长学习路线图

③面试精品集锦汇总

④全套体系化高级架构视频

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

④全套体系化高级架构视频

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

[外链图片转存中…(img-aAat1T12-1715856941456)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值