文章目录

  • 背景
  • 项目当前代码质量问题例子
    • 重复类问题
    • Java 代码问题
    • Kotlin 代码问题
  • 预期收益
  • 技术方案
    • 技术调研
    • 技术实施
      • 总体流程
      • 技术细节
        • CPD 重复代码检查
        • PMD Java 代码检查
        • detekt Kotlin 代码检查
        • lint Java 和 Kotlin 代码检查
        • 统一触发检查工具
        • CI 集成
  • 结语

背景

随着项目的不断迭代,以及代码的增加和开发人员的增加,代码规范或代码质量的把控,是当前版本发布前必要的一环。在当前开发流程中:编码→构建→测试→发布,代码规范或代码质量相关问题,只能靠人工 Review,或灰度和线上 Bugly 反馈。人工 Review 代码,可能比较费时以及遗漏部分Case,而灰度和线上 Bugly 反馈,为时已晚。所以,要在版本发布前尽量去发现代码质量问题,避免带到线上(被动反馈),可以在构建过程之前中去添加 静态代码检查环节,让每一次的构建都能自动地去分析代码是否存在质量问题。

项目当前代码质量问题例子

下面简单例举项目中的几个代码质量问题。

重复类问题

项目中存在代码复制和粘贴的情况,例如:
common/player/src/main/java/com/xxx/xxx/player/ijk/util/ArrayUtils.java 和 类 template/src/main/java/com/xxx/xxx/module/templatelist/ijk/util/ArrayUtils.java ,类 common/player/src/main/java/com/xxx/xxx/player/ijk/util/StringUtils.javatemplate/src/main/java/com/xxx/xxx/module/templatelist/ijk/util/StringUtils.java,类common/player/src/main/java/com/xxx/xxx//ijk/util/CpuInfo.java和 类template/src/main/java/com/xxx/xxx/module/templatelist/ijk/util/CpuInfo.java中的代码几乎完全一样。

Java 代码问题

  1. 容易报控制针异常问题,例如:
    mainvideoeditor/src/xxx/java/com/xxx/xxx/module/editor/speed/adapter/SpeedRvAdapter.java 类中的87行:
    public void reset() {if (mDataList == null && mDataList.isEmpty()) {return;}for (int i = 0; i < mDataList.size(); ++i) {mDataList.get(i).selected = false;}notifyDataSetChanged();}
  1. 线程不安全问题,例如:

mainvideoeditor/src/xxx/java/com/xxx/xxx/module/filter/datacenter/VisualEffectsItemHelper.java 中的29行:

public class VisualEffectsItemHelper {public static List<VisualEffectItem> VISUAL_EFFECTS_LIST;//...省略public static List<VisualEffectItem> getVisualEffectsList() {if (VISUAL_EFFECTS_LIST == null) {VISUAL_EFFECTS_LIST = new ArrayList<>(FX_COUNT);VISUAL_EFFECTS_LIST.add(new VisualEffectItem(FX_RANK_BRIGHTNESS,R.string.video_editor_visual_effect_fx_name_brightness,R.drawable.ic_editor_visual_effect_fx_brightness,EditFxVisualCustomFilterEffectHelper.get(EditVisualEffectsConst.FX_TYPE_BRIGHTNESS)));//...省略}
  1. I/O流没有关闭问题,例如:
    mainvideoeditor/src/xxx/java/com/xxx/xxx/module/humanaction/mediacodec/utils/GlUtil.java中的234行:
  public static String readTextFromRawResource(final Context applicationContext,@RawRes final int resourceId) {final InputStream inputStream =applicationContext.getResources().openRawResource(resourceId);final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);String nextLine;final StringBuilder body = new StringBuilder();try {while ((nextLine = bufferedReader.readLine()) != null) {body.append(nextLine);body.append('\n');}} catch (IOException e) {return null;}return body.toString();}

Kotlin 代码问题

  1. 声明异常却没有捕获,引发运行崩溃问题,例如:
    material/src/main/java/com/xxx/xxx/module/lol/LoLRemoteFragment.kt 中的97行:
    override fun onAttach(context: Context) {super.onAttach(context)mListener = if (context is OnFragmentInteractionListener) {context} else {throw RuntimeException("$context must implement OnFragmentInteractionListener")}}
  1. 容易报空指针异常,例如:
    material/src/main/java/com/xxx/xxx/module/material/viewmodel/MaterialMusicViewModel.kt的36行:
    fun getMusicTab() {viewModelScope.launch {val response = try {repository.makeMusicTabRequest()} catch (e: Exception) {null}//livedata的value不能为空musicCategoryLiveData.value = response}}

预期收益

根据上面项目中的代码质量问题例子,做构建阶段前的静态代码检查,预期可以得到收益大概分为以下几点:

  1. 防止版本线上出现 Java 和 Kotlin 代码低端错误引起崩溃,如空指针,异常未捕获,部分线程,性能等问题;
  2. 防止项目中出现复制粘贴代码情况;
  3. 制定代码规范约束,统一开发人员的开发风格,方便代码的阅读和维护;
  4. 可以根据项目当前状况,扩展规则,解决日志,线程,文件,权限等使用相关问题,特别是权限的使用导致发版后在应用市场不能上架问题;
  5. 遏制重复资源,大资源,不合理资源的引入问题。

技术方案

根据项目的目前状况,静态代码检查工具需要满足的需求有至少以下几点:

  1. 增量检查,由于项目从未做过静态代码检查,全量检查会影响开发者的正常迭代时间,所以只需对修改或增加文件检查即可;
  2. 重复文件检查,由于项目部分历史原因,项目中存在复制和粘贴的代码,所以需要检查出这些代码,并把这些代码进行抽象或下沉到通用组件,不再有冗余代码;
  3. Java 和 Kotlin 代码检查,目前急需的是要去发现一些易出错(线上异常引起崩溃),性能,安全性的代码,其它的代码风格,代码设计,易用性等可以后续再检查
  4. 资源文件检查,drawable, layout, assets等资源文件,当前可以不用去检查,但是需要支持制定相关规则(检查文件大小,无用资源等),方便后续进行检查;
  5. 自定义规则检查,除了通用的规则检查外,可以自定义一些适合当前项目状况的规则去做检查,如日志,线程,文件,权限使用规范等。

技术调研

目前对于Android项目来说,官方有提供 lint 静态代码检查工具,它同时支持Java 和 Kotlin 代码检查。其它比较成熟的针对 Java 代码的检查工具有:FindBugs, PMD, CheckStyle,errorprone;针对 Kotlin 代码的检查工具有:ktlint , detekt。由于项目当前是同时使用 Java 和 Kotlin 开发,所以需要综合来使用这些工具。

检查工具对比:

名称 简要 原理 Gradle 增量 自定义
lint lint 工具可以检查您的 Android 项目源文件是否有潜在的 bug,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。 最新版本通过UAST来分析代码,以前的版本使用过AST和PSI来分析代码 支持 不支持(可Hook) 支持
FindBugs Java 代码检查工具,运行的是 Java 字节码,而不是源码。 将字节码与一组缺陷模式进行对比来发现可能存在的问题。这些问题包括空指针引用、无限递归循环、死锁等 支持 支持 支持
PMD Java 代码检查工具,PMD可以发现常见的编程缺陷,如未使用的变量、空捕获块、不必要的对象创建等。它主要关注Java和Apex,但支持其他六种语言。 使用AST来分析代码 支持 支持 支持
CheckStyle Java 代码检查工具,通过检查对代码编码格式,命名约定,Javadoc,类设计等方面进行代码规范和风格的检查,从而有效约束开发人员更好地遵循代码编写规范。 使用AST来分析代码 支持 支持 支持
errorprone Java 代码检查工具,在编译期间查找代码缺陷的代码检查工具 ,hook正常的build过程,错误产生时及时告知,提供修复建议,并允许基于这些修复建议制定相应模型。 使用AST来分析代码 支持 不支持 支持
ktlint Kotlin代码检查工具,Kotlin 代码风格检查,遵循的 Android Kotlin 官方代码风格,可以自动格式化代码。 使用Kotlin AST来分析代码 支持 支持 支持
detekt Kotlin代码检查工具,高度可配置的规则集,支持不同的报告格式,检查风格和潜在性风险。 使用Kotlin AST来分析代码 支持 支持 支持

这里,对于重复类检查,主要会使用和 PMD 同一个开发者的 CPD。

以上工具,根据我们项目当前现状,目前不适合接入工具有:

  • FindBugs ,由于是字节码检查,自定义规则成本高,且年久失修
  • CheckStyle,只是Java代码的规范和风格检查
  • errorprone,编译期间检查,且自定义规则成本高
  • ktlint,只是 Kotlin 代码的规范和风格检查

剩余工具相关内置规范:

  • lint:正确性、安全性、性能、易用性、无障碍性和国际化;
  • PMD:最佳做法、代码样式、设计、文档、容易出错、多线程、性能、安全、其他规则集;
  • CPD :Java 和 Kotlin 重复代码;
  • detekt:代码注释和文档、复杂代码、协程、空代码块、异常、格式化代码、命名规范、性能,潜在性bug、风格。

根据项目当前的情况,第一期只检查正确性,安全性,性能,多线程,容易出错,协程,异常,潜在性bug问题。等问题收敛后,再进一步检查易用性,设计,复杂代码等,以及后续自定义规则检查

技术实施

总体流程

在静态代码检查初期,最主要的是自动化检查,且不影响常规开发。所以把检查放在CI 构建阶段之前的分析阶段中,在分析阶段去创建一个静态代码检查任务,每当代码 push 到 Gitlab,就去触发这个任务。下面为总体流程:

技术细节

首先,根据项目的上下文环境,去集成每一个工具。关于重复代码检查,检查的是项目中的所有Java 和 Kotlin 文件(全量检查),所以要把 CPD 这个工具独立集成和触发。关于 Java 和 Kotlin 代码增量(新增或修改)检查, 需要把得到的增量文件,分发给 lint、PMD、detekt 工具去检查。 另外,lint 工具 不支持增量检查,还需要开发适配 lint 增量检查的插件。

下面为各个工具接入的简单概况,不介绍细节。

CPD 重复代码检查

根据 CPD 接入文档,以及它的 CLI 。新建 cpd.gradle 文件(放在.codequality目录下,后面的都一样),编写相关脚本(这里只做核心介绍):

    //将收集到项目 java 和 kotlin 文件列表分别交给 cpdruncpd(javalist, 340, 'java') { String output ->reportLines.addAll(output.readLines())}runcpd(kotlinlist, 210, 'kotlin') { String output ->reportLines.addAll(output.readLines())}void runcpd(File filelist, int tokens, String language, Closure closure) {def out = new ByteArrayOutputStream(1024)project.javaexec { spec ->spec.main = "net.sourceforge.pmd.cpd.CPD"spec.maxHeapSize = '2g'spec.classpath = this.classpath.get()spec.args '--minimum-tokens', tokens == 0 ? '200' : tokensspec.args '--language', language ?: 'java'spec.args '--format csv --failOnViolation false'.tokenize()spec.args '--filelist', filelist.pathspec.args '--skip-lexical-errors', '--ignore-annotations'spec.standardOutput = outspec.ignoreExitValue = true}closure.call(out.toString("UTF-8"))logger.lifecycle("CPD run for $filelist")}

使用 cpd 需要的 依赖有:

 'net.sourceforge.pmd:pmd-core:6.36.0''net.sourceforge.pmd:pmd-java:6.36.0''net.sourceforge.pmd:pmd-kotlin:6.10.0'

可以在 它的 github 上去下载最新版本:https://github.com/pmd/pmd/releases/tag/pmd_releases/6.49.0。需要把这些添加到 gradledependencies 中,然后才能使用 project.javaexec 去执行。

PMD Java 代码检查

根据 PMD 的 gradle 接入文档,再结合项目环境,新建 pmd.gradle 文件(放在.codequality目录下):

/** des : inspect java code* doc : https://pmd.github.io/latest/pmd_userdocs_tools_gradle.html* use : should apply this script in root build.gradle* how to analyze result : https://pmd.github.io/latest/pmd_userdocs_report_formats.html , see html example.*/
apply plugin: 'pmd'def pmdConfigPath = new File(codequality, 'scripts/pmd').pathtask pmd(type: Pmd) {FileCollection fileCollection//获取到 git diff 后的 java 文件if (project.hasProperty("commitJavaFiles")) {fileCollection = project.files(project.property("commitJavaFiles"))} else {//当前项目下的 src 目录下的所有文件,注意这里可能有 flavorfileCollection = project.files("src")}if (fileCollection == null) {return}//区分是 CI 环境,还是本地环境def isCiServer = falseif (project.hasProperty("isCiServer")) {isCiServer = project.property("isCiServer")}if (isCiServer) {ignoreFailures = false} else {ignoreFailures = true}consoleOutput = true//规则集ruleSetFiles = files("${pmdConfigPath}/pmd-ruleset.xml")ruleSets = []source fileCollectioninclude '**/*.java'exclude '**/gen/**'reports {xml.getRequired().set(false)html.getRequired().set(true)//结果输出到项目根目录下的 build/reports/pmd 文件夹中,只需要html格式的结果文档def destination = new File(new File("${project.rootProject.buildDir.path}/reports/pmd"), "${project.name}-pmd.html")html.destination(destination)}
}

规则集的配置,在同目录下创建 scripts/pmd/pmd-ruleset.xml

<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Custom Rules"xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd"><description>Custom ruleset for Android application</description><!-- rule des :https://pmd.github.io/pmd-6.41.0/pmd_rules_java.html --><exclude-pattern>.*/R.java</exclude-pattern><exclude-pattern>.*/gen/.*</exclude-pattern><!-- Your rules will come here --><!--     容易出现运行时错误 --><rule ref="category/java/errorprone.xml"><exclude name="BeanMembersShouldSerialize" /></rule><!--    多线程时问题--><rule ref="category/java/multithreading.xml" /><!--    需要优化代码的地方--><rule ref="category/java/performance.xml" /><!--    安全问题--><rule ref="category/java/security.xml" />
</ruleset>

这里前期只配置:容易出现运行时错误,多线程时问题,需要优化代码的地方,安全问题规则。

detekt Kotlin 代码检查

根据 detekt 的 gradle 接入文档,首先在项目根目录下配置 classpath

buildscript {repositories {gradlePluginPortal()}dependencies {classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0"}
}

然后新建 detekt.grale 文件:

/** des : inspect kotlin code* doc : https://github.com/detekt/detekt* use : should apply this script in root build.gradle* how to analyze result : https://detekt.dev/docs/introduction/reporting , see html example.*/
apply plugin: 'io.gitlab.arturbosch.detekt'def detektConfigPath = new File(codequality, 'scripts/detekt').pathdetekt {//获取到 git diff 后的 kotlin 文件if (project.hasProperty("commitKotlinFiles")) {source = project.files(project.property("commitKotlinFiles"))}buildUponDefaultConfig = true // preconfigure defaults//规则集config = files("$detektConfigPath/detekt.yml")// point to your custom config defining rules to run, overwriting default behaviorbaseline = file("$detektConfigPath/baseline.xml")basePath = projectDir.pathdef isCiServer = false//区分是 CI 环境,还是本地环境if (project.hasProperty("isCiServer")) {isCiServer = project.property("isCiServer")}if (isCiServer) {ignoreFailures = false} else {ignoreFailures = true}
}tasks.detekt.jvmTarget = "1.8"tasks.named("detekt").configure {reports {// Enable/Disable HTML report (default: true)def destination = new File(new File("${project.rootProject.buildDir.path}/reports/detekt"), "${project.name}-detekt.html")html.required.set(true)html.outputLocation.set(destination)}
}

规则集的配置,在同目录下创建 scripts/detekt/detekt.xml

#whiteProjectListbuild:
#  maxIssues: 0
#  excludeCorrectable: false
#  weights:# complexity: 2# LongParameterList: 1# style: 1# comments: 1config:validation: truewarningsAsErrors: false
#  checkExhaustiveness: false# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'excludes: ''processors:active: trueexclude:- 'DetektProgressListener'# - 'KtFileCountProcessor'# - 'PackageCountProcessor'# - 'ClassCountProcessor'# - 'FunctionCountProcessor'# - 'PropertyCountProcessor'# - 'ProjectComplexityProcessor'# - 'ProjectCognitiveComplexityProcessor'# - 'ProjectLLOCProcessor'# - 'ProjectCLOCProcessor'# - 'ProjectLOCProcessor'# - 'ProjectSLOCProcessor'# - 'LicenseHeaderLoaderExtension'console-reports:active: trueexclude:- 'ProjectStatisticsReport'- 'ComplexityReport'- 'NotificationReport'- 'FindingsReport'- 'FileBasedFindingsReport'#  - 'LiteFindingsReport'output-reports:active: trueexclude:# - 'TxtOutputReport'# - 'XmlOutputReport'# - 'HtmlOutputReport'# - 'MdOutputReport'
#协程问题
coroutines:active: trueGlobalCoroutineUsage:active: falseInjectDispatcher:active: truedispatcherNames:- 'IO'- 'Default'- 'Unconfined'RedundantSuspendModifier:active: trueSleepInsteadOfDelay:active: trueSuspendFunWithCoroutineScopeReceiver:active: falseSuspendFunWithFlowReturnType:active: true
#空代码块问题
empty-blocks:active: trueEmptyCatchBlock:active: trueallowedExceptionNameRegex: '_|(ignore|expected).*'EmptyClassBlock:active: trueEmptyDefaultConstructor:active: trueEmptyDoWhileBlock:active: trueEmptyElseBlock:active: trueEmptyFinallyBlock:active: trueEmptyForBlock:active: trueEmptyFunctionBlock:active: trueignoreOverridden: trueEmptyIfBlock:active: trueEmptyInitBlock:active: trueEmptyKtFile:active: trueEmptySecondaryConstructor:active: trueEmptyTryBlock:active: trueEmptyWhenBlock:active: trueEmptyWhileBlock:active: true
#异常问题
exceptions:active: trueExceptionRaisedInUnexpectedLocation:active: truemethodNames:- 'equals'- 'finalize'- 'hashCode'- 'toString'InstanceOfCheckForException:active: trueexcludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']NotImplementedDeclaration:active: falseObjectExtendsThrowable:active: falsePrintStackTrace:active: trueRethrowCaughtException:active: trueReturnFromFinally:active: trueignoreLabeled: falseSwallowedException:active: trueignoredExceptionTypes:- 'InterruptedException'- 'MalformedURLException'- 'NumberFormatException'- 'ParseException'allowedExceptionNameRegex: '_|(ignore|expected).*'ThrowingExceptionFromFinally:active: trueThrowingExceptionInMain:active: falseThrowingExceptionsWithoutMessageOrCause:active: trueexcludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']exceptions:- 'ArrayIndexOutOfBoundsException'- 'Exception'- 'IllegalArgumentException'- 'IllegalMonitorStateException'- 'IllegalStateException'- 'IndexOutOfBoundsException'- 'NullPointerException'- 'RuntimeException'- 'Throwable'ThrowingNewInstanceOfSameException:active: trueTooGenericExceptionCaught:active: trueexcludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']exceptionNames:- 'ArrayIndexOutOfBoundsException'- 'Error'- 'Exception'- 'IllegalMonitorStateException'- 'IndexOutOfBoundsException'- 'NullPointerException'- 'RuntimeException'- 'Throwable'allowedExceptionNameRegex: '_|(ignore|expected).*'TooGenericExceptionThrown:active: trueexceptionNames:- 'Error'- 'Exception'- 'RuntimeException'- 'Throwable'
#性能问题
performance:active: trueArrayPrimitive:active: trueCouldBeSequence:active: falsethreshold: 3ForEachOnRange:active: trueexcludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']SpreadOperator:active: trueexcludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']UnnecessaryTemporaryInstantiation:active: true
#潜在bug问题
potential-bugs:active: trueAvoidReferentialEquality:active: trueforbiddenTypePatterns:- 'kotlin.String'CastToNullableType:active: falseDeprecation:active: falseDontDowncastCollectionTypes:active: falseDoubleMutabilityForCollection:active: truemutableTypes:- 'kotlin.collections.MutableList'- 'kotlin.collections.MutableMap'- 'kotlin.collections.MutableSet'- 'java.util.ArrayList'- 'java.util.LinkedHashSet'- 'java.util.HashSet'- 'java.util.LinkedHashMap'- 'java.util.HashMap'DuplicateCaseInWhenExpression:active: trueElseCaseInsteadOfExhaustiveWhen:active: falseEqualsAlwaysReturnsTrueOrFalse:active: trueEqualsWithHashCodeExist:active: trueExitOutsideMain:active: falseExplicitGarbageCollectionCall:active: trueHasPlatformType:active: trueIgnoredReturnValue:active: truerestrictToAnnotatedMethods: truereturnValueAnnotations:- '*.CheckResult'- '*.CheckReturnValue'ignoreReturnValueAnnotations:- '*.CanIgnoreReturnValue'ignoreFunctionCall: []ImplicitDefaultLocale:active: trueImplicitUnitReturnType:active: falseallowExplicitReturnType: trueInvalidRange:active: trueIteratorHasNextCallsNextMethod:active: trueIteratorNotThrowingNoSuchElementException:active: trueLateinitUsage:active: falseexcludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']ignoreOnClassesPattern: ''MapGetWithNotNullAssertionOperator:active: trueMissingPackageDeclaration:active: falseexcludes: ['**/*.kts']MissingWhenCase:active: trueallowElseExpression: trueNullCheckOnMutableProperty:active: falseNullableToStringCall:active: falseRedundantElseInWhen:active: trueUnconditionalJumpStatementInLoop:active: falseUnnecessaryNotNullOperator:active: trueUnnecessarySafeCall:active: trueUnreachableCatchBlock:active: trueUnreachableCode:active: trueUnsafeCallOnNullableType:active: trueexcludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']UnsafeCast:active: trueUnusedUnaryOperator:active: trueUselessPostfixExpression:active: trueWrongEqualsTypeParameter:active: true

这里前期只配置:协程问题,空代码问题,异常问题,性能问题,潜在bug问题规则。

lint Java 和 Kotlin 代码检查

根据 lint 的 gradle 接入文档,新建 lint.grale 文件:

/** des : inspect java and kotlin code by increment* doc :  https://juejin.cn/post/7093133800812576781#heading-8* use : should apply this script in root build.gradle* custom lint plugin : https://github.com/googlesamples/android-custom-lint-rules* */
project.afterEvaluate {if (!(project.getPlugins().hasPlugin('com.android.application') ||project.getPlugins().hasPlugin('com.android.library'))) {return}//自定义的 lint 增量插件project.apply plugin: 'com.xxx.xxx.lint'def lintConfigPath = new File(codequality, 'scripts/lint').pathproject.android {//doc : https://developer.android.google.cn/reference/tools/gradle-api/7.1/com/android/build/api/dsl/LintOptions?hl=enlint {// true:关闭lint报告的分析进度quiet true// true:错误发生后停止gradle构建abortOnError false// true:只报告errorignoreWarnings true// true:忽略有错误的文件的全/绝对路径(默认是true)absolutePaths true// true:检查所有问题点,包含其他默认关闭项checkAllWarnings false// true:所有warning当做errorwarningsAsErrors false// 关闭指定问题检查disable 'Usability', 'Accessibility', 'Internationalization','ResourceName'// true:error输出文件不包含源码行号noLines false// true:显示错误的所有发生位置,不截取showAll true// 回退lint设置(默认规则)lintConfig file("$lintConfigPath/default-lint.xml")// true:生成txt格式报告(默认false)textReport true// true:生成XML格式报告xmlReport false// true:生成HTML报告(带问题解释,源码位置,等)htmlReport true// html报告可选路径(构建器默认是lint-results.html )def destination = new File(new File("${project.rootProject.buildDir.path}/reports/lint"), "${project.name}-lint.html")htmlOutput file(destination)//  true:所有正式版构建执行规则生成崩溃的lint检查,如果有崩溃问题将停止构建checkReleaseBuilds truecheckDependencies false}}
}

由于 lint 不支持增量文件检查,这里需要 hook 原 lint plugin 中添加检查文件的地方,hook 原理可以查看AGP7.0增量Lint适配,这里不具体介绍。当拦截到 lint 插件中 Main 类的 run 方法时,通过 LintRequest 获取 project,然后将添加增量文件,此时 lint 就只会检查增量文件,不会检查项目所有文件。那么,为了适配 lint 增量检查需要做的就是,在运行 lint task 之前,将 git diff 出的文件列表,存储到一个本地文件中,然后运行 lint task 的时候,去读取这个文件中的文件列表,然后添加到 project 检查文件列表中,这样就实现了 lint 增量检查。这里不再展开细节。

统一触发检查工具

接入完 CPD,PMD,detekt,lint 工具后,就是这么去做增量检查(版本迭代,新增或修改文件检查),这里主要结合 git log 命令。再新建code_inspect_increment.gradle文件(还是放在.codequality目录下)

ext {//存储增量的Java文件,给pmd使用commitJavaFiles = new ArrayList<String>()//存储增量的kotlin文件,给detekt使用commitKotlinFiles = new ArrayList<String>()commitAuthor = ""appVersionName = ""
}
/*** 增量开关*/
def isIncrement = false
/*** 是否是CI环境* @return true是,否则不是*/
def isCiServer() {def isCiServer = falseif (project.hasProperty("isCiServer")) {isCiServer = project.property("isCiServer")}return isCiServer
}
/*** 通过cmd命令获取结果值* @return string 结果值*/
def getResultFromCmd(String cmd) {logger.lifecycle("${project.name} cmd:$cmd")def out = new ByteArrayOutputStream()if (System.properties['os.name'].toLowerCase().contains('windows')) {project.exec {ExecSpec execSpec ->executable 'cmd'args '/c', "$cmd"standardOutput = out}.assertNormalExitValue()} else {project.exec {ExecSpec execSpec ->executable 'bash'args '-c', "$cmd"standardOutput = out}.assertNormalExitValue()}return out.toString().trim()
}
/*** 获取提交作者* @return 提交作者*/
def getCommitAuthor() {def commitAuthor = ""if (isCiServer()) {commitAuthor = "${System.env.CI_COMMIT_AUTHOR}"commitAuthor = commitAuthor.substring(0, commitAuthor.indexOf('<')).trim()} else {commitAuthor = getResultFromCmd("git config --get user.name")}return commitAuthor
}/*** 获取提交 sha,用于 git diff 出增量文件,其它项目仓库,这个方法需要更改实现* @return 提交 sha*/
def getCommitTargetSha() {//目前这个仓库只能根据上一个merge节点,进行代码增量检测def commitTargetSha = getResultFromCmd("git log --max-count=1 --pretty=%H --merges")return commitTargetSha
}
/*** 获取提交文件,只包含.java和.kt的文件* @return 文件相对路径数组*/
def getCommitFiles() {def commitTargetSha = getCommitTargetSha()def commitSha = getResultFromCmd("git log --max-count=1 --pretty=%H")def commitAuthor = getCommitAuthor()def cmd = "git log --name-only --author=$commitAuthor --pretty=\"format:\" $commitTargetSha..$commitSha"def result = getResultFromCmd(cmd)def projectStartPath = getProjectStartPath()logger.lifecycle("projectStartPath=$projectStartPath")if (result != null && result.trim().length() > 0) {def files = result.readLines().findAll { it.startsWith("${projectStartPath}") && isFilePathValid(it) }.toArray()return files.toUnique()}return null
}
/*** 判断文件路径是否合理* @param path 文件路径* @return true合理,否则不合理*/
static def isFilePathValid(String path) {return (path.endsWith('.kt') || path.endsWith('.java') || path.endsWith('.xml') || path.endsWith('.png')) && !path.contains('/test/') && !path.contains('/androidTest/') && !path.contains('/generated/')
}
/*** 获取src目录下的所有文件,只包含.java和.kt的文件* @return 文件相对路径数组*/
def getAllFiles() {def fileCollection = project.files("src")if (fileCollection == null) {return null}def files = fileCollection.asFileTree.getFiles()def iterator = files.iterator()def allFiles = new ArrayList<String>(files.size())while (iterator.hasNext()) {File file = iterator.next()def path = getProjectStartPath() + File.separator + project.relativePath(file.path)if (path != null && isFilePathValid(path)) {allFiles.add(path)}}return allFiles.toArray(new Object[allFiles.size()])
}
/*** 获取项目名称,如gradleengine,framework/asmbase* @return 项目名称*/
def getProjectStartPath() {def projectRelativePath = project.relativePath(project.path).replaceAll(':', '/')return projectRelativePath.substring(1)
}task runCodeInspectIncrement {group = "codeInspect"description = "存储增量文件"def commitJavaFiles = new ArrayList<String>()def commitKotlinFiles = new ArrayList<String>()def commitFiles = new Object[0]if (isIncrement) {commitFiles = getCommitFiles()} else {commitFiles = getAllFiles()}saveCommitFiles(commitFiles)if (commitFiles != null && commitFiles.length > 0) {def projectStartPath = getProjectStartPath()for (String s : commitFiles) {def commitFile = s.toString().trim()if (commitFile.endsWith('.kt')) {commitKotlinFiles.add(commitFile.substring(projectStartPath.length() + 1))} else if (commitFile.endsWith('.java')) {commitJavaFiles.add(commitFile.substring(projectStartPath.length() + 1))}}}if (!commitKotlinFiles.isEmpty()) {logger.lifecycle("${project.name} commitKotlinFiles:$commitKotlinFiles")}if (!commitJavaFiles.isEmpty()) {logger.lifecycle("${project.name} commitJavaFiles:$commitJavaFiles")}project.setProperty("commitKotlinFiles", commitKotlinFiles)project.setProperty("commitJavaFiles", commitJavaFiles)project.setProperty("commitAuthor", getCommitAuthor())project.setProperty("appVersionName", getAppVersionName())
}/*** 存储提交文件* @param commitFiles 提交文件*/
def saveCommitFiles(Object[] commitFiles) {//commitFiles 是对象def lintBuildDir = new File(project.buildDir, "code_inspect")if (!lintBuildDir.exists()) {lintBuildDir.mkdirs()}def changedFile = new File(lintBuildDir, "commit_files.txt")if (commitFiles == null || commitFiles.length == 0) {//添加一个默认文件,防止Lint全量扫描def buildFilePath = "build.gradle"commitFiles = new Object[]{buildFilePath}}def fileOutputStream = new FileOutputStream(changedFile)def rootProjectPath = project.rootProject.rootDir.path + File.separator//存储绝对路径for (int i = 0; i < commitFiles.length; i++) {def commitFile = commitFiles[i].toString()fileOutputStream.write((rootProjectPath + commitFile).getBytes("utf-8"))if (i != commitFiles.length - 1)fileOutputStream.write("\n".getBytes("utf-8"))}fileOutputStream.flush()fileOutputStream.close()logger.lifecycle("Save Commit Files In: ${changedFile.path} \n$commitFiles")
}static def getAppVersionName() {return "unknown"
}

这里需要注意的是 gradle task : runCodeInspectIncrement,这个自定义 的 task,它的作用就是 通过 git 命令将修改或新增文件存储到 project 的额外变量中,以及存储到 本地文件中(给 lint 插件做增量扫描使用)

接下来,就是如何统一触发这些插件。再新建code_inspect.gradle文件(还是放在.codequality目录下):

task runCodeInspect {group = "codeInspect"description = "静态代码检查,统一触发pmd detekt lint任务"
}project.afterEvaluate {bindTask("lint", "runCodeInspect") {checkLintResultValid()}bindTask("pmd", "runCodeInspect") {checkPmdResultValid()}bindTask("detekt", "runCodeInspect") {checkDetektResultValid()}
}/*** 绑定任务* @param taskName 任务名称* @param targetTaskName 被绑定的任务名称* @param action taskName指定的任务执行完成后的回调*/
def bindTask(String taskName, String targetTaskName, groovy.lang.Closure action) {def task = project.tasks.findByName(taskName)if (task != null) {logger.lifecycle("${task} will be run")def targetTask = project.tasks.findByName(targetTaskName)task.doLast(action)targetTask.finalizedBy(task)}
}

上面的流程大概是,新建 gradle task : runCodeInspect,在这个 task 执行完的时候 (finalizedBy),就去触发 pmd, detekt ,lint 相关的 task 去执行静态代码检查。检查完成后,在项目根目录下的 build/reports/pmd 或 build/reports/detekt 或 uild/reports/lint 查看结果 html 报告。

在这里,html 报告,只是给自己看的一个报告,如果要给大家看或者老板看,那么重要的是怎样将这个分析结果报告数据可视化。这才是这个工具最重要的一环。这里不在展开,感兴趣可以结合自己项目背景,思考如何去做

最后,在项目根目录下的 build.gradle 中,让每个子项目都添加上面的脚本依赖:

ext {//是不是CI环境isCiServer = System.env.containsKey('CI_SERVER')//插件工具存放目录codequality = new File(projectDir, '.codequality')//静态代码检查开关isCodeInspectEnable = true
}
//重复代码检查
if (isCodeInspectEnable) {def cpdPath = new File(codequality, 'cpd.gradle').pathapply from: "$cpdPath"
}allprojects {if (isCodeInspectEnable ) {def basePath = new File(codequality, 'code_inspect_increment.gradle').pathapply from: "$basePath"def pmdPath = new File(codequality, 'pmd.gradle').pathapply from: "$pmdPath"def detektPath = new File(codequality, 'detekt.gradle').pathapply from: "$detektPath"def lintPath = new File(codequality, 'lint.gradle').pathapply from: "$lintPath"def codeInspectPath = new File(codequality, 'code_inspect.gradle').pathapply from: "$codeInspectPath"}}

重复代码检查,cpd 任务可以独立触发。因为重复代码的检查,期望是先全部解决。

CI 集成

在开发者将代码 push 到 Gitlab 后,在 CI 执行中,添加分析阶段,在分析阶段中添加 静态代码检查 任务,当检查出代码质量问题,就中断执行,也就是 pipeline 不通过(此时可以禁止合入代码),然后上传检查报告。这样就可以自动化对 修改或新增文件进行检查了。

接下来,在项目的 .gitlab-ci.yml 文件中,添加 stage:analyze 中,在 analyze 中添加 job: inspect java and kotlin codes

stages:- 其它- analyze- 其它inspect java and kotlin codes:tags:- apk- androidstage: analyzescript:- echo "$CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA"- echo "$CI_JOB_STAGE}_reports_${CI_PROJECT_NAME}_${CI_BUILD_REF_NAME}"- ./gradlew runCodeInspectbefore_script:- source change_java_versionartifacts:name: "$CI_JOB_STAGE}_reports_${CI_PROJECT_NAME}_$CI_COMMIT_REF_SLUG"when: on_failureexpire_in: 1 dayspaths:- "build/reports/*"only:- branchesexcept:- masterallow_failure: false

这里其实就是运行 .codequality/code_inspect.gradle中建立的 gradle task:./gradlew runCodeInspect。如果对 Gtilab CI/CD 不熟悉,可以查看上一篇文章:Gitlab CI/CD 简单介绍。

结语

上面对项目的当前代码质量问题,以及如何去解决这个问题,以及项目的预期收益进行了分析。这次静态代码检查的主要是解决重复代码,Java 和 Kotlin 代码的易出错,线程,安全,性能等优先问题。在静态代码检查接入项目后,项目的执行流程是:开发者 push 代码到 Gitlab ,触发 CI,在 CI 的分析阶段,然后执行 静态代码检查 job,在这个 job 中执行 gradle task : runCodeInspect 任务,当任务运行结束,将代码检查报告上传到 Gitlab,开发者下载报告,根据报告建议进行代码更改。

静态代码检查是代码开发完后的一种代码质量防控措施,如果提高代码质量,最主要的还是开发者编写代码的时候,进行全面和详细的思考。

Android 静态代码检查相关推荐

  1. 美团外卖Android Lint代码检查实践

    概述 Lint是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题,提醒开发人员及早修正,提高代码质量.除了Android原生提供的几百个Lint规则,还可以开发自定义L ...

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

    背景与问题 DevOps实践中,我们在CI(Continuous Integration)持续集成过程主要包含了代码提交.静态检测.单元测试.编译打包环节.其中静态代码检测可以在编码规范,代码缺陷,性 ...

  3. java lint_Android静态代码检查-Lint

    参考文章: Improving Your Code with lint lint 使用 lint 增强你的代码 Android Lint简介 gradle lint gradle中有lint任务,可以 ...

  4. 提高代码质量之静态代码检查

    http://www.jianshu.com/p/2b8d34b2267c 前言 在团队Android项目开发过程中,难免会出现一些比较不容易发现,但是又比较低级的bug.而且因为每个开发人员的编码习 ...

  5. jenkins+findbugs+checkstyle+PMD静态代码检查(二)

    可以根据自己的需求选中对应的插件进行配置(不一定非要同时配置三个插件) jenkins:持续集成的工具 fundbugs:检测代码静态错误的插件  例如:定义了没有用到的对象,string类型的比较使 ...

  6. android静态代码扫描,android 静态代码扫描

    开始做这样一个东西是为了帮助开发减少代码方面的问题,提高代码质量,减小以后上线的风险.前面看了 360 的那个静态代码扫描感觉很强大,但目前没这实力去做成这样,希望早日开源,多多学习.所以就先用开源的 ...

  7. 静态代码检查工具简介

    静态代码检查工具简介 在 Java 软件开发过程中,开发团队往往要花费大量的时间和精力发现并修改代码缺陷.传统的代码复审.同行评审,通过人工方式来检查缺陷仍然是一件耗时耗力的事情.Java 静态代码分 ...

  8. Android Lint代码检查实践

    1. 为啥用Lint 平时开发中我们在提mr的时候都会进行review,但有些问题通过人眼去看很难发现,比如Fragment必须有空参构造函数,因为在内存不足的时候Fragment恢复默认是通过反射调 ...

  9. 静态代码检查工具 cppcheck 的使用

    CppCheck是一个C/C++代码缺陷静态检查工具.不同于C/C++编译器及其它分析工具,CppCheck只检查编译器检查不出来的bug,不检查语法错误.所谓静态代码检查就是使用一个工具检查我们写的 ...

最新文章

  1. 在 Node.js 中用子进程操作标准输入/输出
  2. 大数据时代:从1.0到3.0 | 专访清华社会学系教授罗家德
  3. yolo_model to output理解
  4. 【Win 10 应用开发】文件读写的三种方案
  5. java生产者消费者gui_java理论之java--GUI(图形用户管理)与 IO/流
  6. arm linux 时间获取,菜鸟自学ARM:linux下编程获取系统时间和设置时间
  7. bzoj 1070: [SCOI2007]修车【最小费用最大流】
  8. postman访问abp接口因cookies问题导致400错误
  9. 大型计算机grads使用,win10中GRADS出现FAST_CWD pointer错误的详细解决方法
  10. ble mesh 学习笔记(9)telink的mesh-lpn代码分析
  11. ipad和android同步照片,4种方式将照片从iPhone传输到iPad
  12. SpringBoot集成MongoDB实现两种CRUD
  13. CSS 实战 - 个人名片设计
  14. 电脑应用程序分身双开及多开(能记住账号和密码)
  15. 气象数据统一服务接口(MUSIC)简介
  16. c++ 字符串去掉首尾空格_C++ 去掉字符串的首尾空格和全部空格
  17. 树的前序,中序,后序遍历。
  18. Java 基础核心知识
  19. 什么是物联卡,物联卡注意事项
  20. 什么是图论和图论在数字图像中的应用

热门文章

  1. 山东省滨州市谷歌高清卫星地图下载
  2. 【转】愤怒的小鸟 PC汉化版
  3. BUGKU CTF (Crypto第一篇)
  4. android文字替换快捷键是什么,AndroidStudio快捷键
  5. iOS 9 的新特性
  6. [FairyGUI][Unity]加载FGUI打包成AB包的资源
  7. 小程序物流追踪状态页面
  8. springboot图书管理怎么实现借书时间到期提醒_智慧档案管理系统的功能,你真的了解?...
  9. 我们经常会用遇到一些文件需要网签,如何在word中制作手写签名?
  10. 职称认定和职称评审的区别是什么