android自定义Lint实现代码检测

4,760 阅读6分钟

0.前言

最近在项目中发现了一个问题,服务器端下发了一个比较大的开屏页面,客户端在加载开屏图片的时候使用了BitmapFactory创建Bitmap,而且是在主线程做的,平时图片小,可能没出现什么问题,结果这次服务端放的图片大了,就造成了一大面积的Crash。这其实就是一个代码质量与代码规范的问题,怎么保证这个问题不在发生?靠开发者的自觉吗?我觉得整个代码世界最大的变量其实就是人,谁都有个马高镫短,喜怒哀乐,我们需要在编码过程中提供一种检测机制,用以避免这样的问题。

1.分析

我们想要的就是在编码时可以提供异常检测的方法,高亮或者加红,这在咱们平时编码时是很常见的。

当鼠标悬停在高亮的代码上时,会提供问题的描述和解决方法。需要这种效果,就需要自定义lint了

2.Lint介绍

看一下官方文档的介绍: Android Studio 提供一个名为 Lint 的代码扫描工具,可帮助您发现并纠正代码结构质量的问题,而无需实际执行该应用,也不必编写测试用例。该工具会报告其检测到的每个问题并提供该问题的描述消息和严重级别,以便您可以快速确定需要优先进行哪些关键改进。此外,您可以调低问题的严重级别,忽略与项目无关的问题,也可以调高严重级别,以突出特定问题。

Lint 工具可检查您的 Android 项目源文件是否包含潜在错误,以及在正确性、安全性、性能、易用性、便利性和国际化方面是否需要优化改进。在使用 Android Studio 时,配置的 Lint 和 IDE 检查会在您每次构建应用时运行。不过,您可以手动运行检查或从命令行运行 Lint。

android studio内置了上百个lint规则,但是我们需要根据项目进行我们自己相关特色的规则定义,说白了,内置的lint规则无法满足需求时,就需要我们自定义lint了

3.自定义Lint流程

关于lint的文档其实还挺少的,而且居然没有相关的api文档,这就有些蛋疼了,那怎么搞?开坛,做法,上github,从github上google sample中下载自定义customlint的项目(项目地址),打开工程,

    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    compileOnly "com.android.tools.lint:lint-checks:$lintVersion"

依赖了两个maven库,lint-api还有lint-checks,其中lint-api就是lint相关的api,lint-checks就是android studio里自定义的一些lint规则,因为没有什么相关api文档,可以参考lint-check里面的写法

3.1创建Detector

Detector负责扫描代码,发现问题并报告。

class BitmapFactoryDetector:Detector(),Detector.UastScanner {

    companion object {

        val ISSUE=Issue.create(
            "BitmapFactoryReplace",
            "BitmapFactoryReplace",
            "使用Glide或其他第三方框架代替BitmapFactory创建Bitmap",
            Category.CORRECTNESS,
            7,
            Severity.WARNING,
            Implementation(BitmapFactoryDetector::class.java,Scope.JAVA_FILE_SCOPE)
        )

    }

    override fun getApplicableMethodNames(): List<String>? {
        return Arrays.asList("decodeResource","decodeFile","decodeResourceStream","decodeByteArray","decodeStream",
            "decodeFileDescriptor")
    }

    override fun getApplicableCallNames(): List<String>? {
        return Arrays.asList("decodeResource","decodeFile","decodeResourceStream","decodeByteArray","decodeStream",
            "decodeFileDescriptor")
    }

    override fun visitMethod(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        if(context.evaluator.isMemberInClass(method, "android.graphics.BitmapFactory")){

            context.report(ISSUE,context.getLocation(node),"使用Glide或其他第三方框架代替BitmapFactory创建Bitmap")
        }
    }

    override fun visitMethod(
        context: JavaContext,
        visitor: JavaElementVisitor?,
        call: PsiMethodCallExpression,
        method: PsiMethod
    ) {
        if(context.evaluator.isMemberInClass(method, "android.graphics.BitmapFactory")){

            context.report(ISSUE,context.getLocation(call),"使用Glide或其他第三方框架代替BitmapFactory创建Bitmap")
        }
    }

    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        if(context.evaluator.isMemberInClass(method, "android.graphics.BitmapFactory")){

            context.report(ISSUE,context.getLocation(node),"使用Glide或其他第三方框架代替BitmapFactory创建Bitmap")
        }
    }
}

这个应该是自定义Lint的核心了可以看到这个Detector继承Detector类,然后实现Scanner接口。 自定义Detector可以实现一个或多个Scanner接口,选择实现哪种接口取决于你想要的扫描范围 自定义Detector需要继承自Detector并实现 Detector.UastScanner 接口,25.2.0及之前版本的Detector.JavaPsiScanner已被弃用,UastScanner相比于JavaPsiScanner以及更老的JavaScanner,主要提供了对Kotlin支持,API更加简单,特点是成对存在(满足条件 -> visitor)此外可以lint-checks-version.jar中的各类型Detector源码可以学习其用法。 UastScanner包含13个回调方法,下面介绍常用的几个:

1.getApplicableUastTypes 此方法返回需要检查的AST节点的类型,类型匹配的UElement将会被createUastHandler(createJavaVisitor)创建的UElementHandler(Visitor)检查。

2.createUastHandler 创建一个UastHandler来检查需要检查的UElement,对应于getApplicableUastTypes

3.getApplicableMethodNames 返回你所需要检查的方法名称列表,或者返回null,相匹配的方法将通过visitMethod方法被检查

4.visitMethod 检查与getApplicableMethodNames相匹配的方法

5.getApplicableConstructorTypes 返回需要检查的构造函数类型列表,类型匹配的方法将通过visitConstructor被检查

6.visitConstructor 检查与getApplicableConstructorTypes相匹配的构造方法

7.getApplicableReferenceNames 返回需要检查的引用路径名,匹配的引用将通过visitReference被检查

8.visitReference 检查与getApplicableReferenceNames匹配的引用

9.appliesToResourceRefs 返回需要检查的资源引用,匹配的引用将通过visitResourceReference被检查

10.visitResourceReference 检查与appliesToResourceRefs匹配的资源引用

11.applicableSuperClasses 返回需要检查的父类名列表,此处需要类的全路径名 11.visitClass 检查applicableSuperClasses返回的类

这个BitmapFactoryDetector就是用来检测你在代码中是否使用BitmapFactory中的方法创建Bitmap,如果有的话就会在代码处高亮,进行一个提示"使用Glide或其他第三方框架代替BitmapFactory创建Bitmap"。

3.2创建Issue

val ISSUE=Issue.create(
            "BitmapFactoryReplace",
            "BitmapFactoryReplace",
            "使用Glide或其他第三方框架代替BitmapFactory创建Bitmap",
            Category.CORRECTNESS,
            7,
            Severity.WARNING,
            Implementation(BitmapFactoryDetector::class.java,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。声明扫描检测的范围Scope,Scope用来描述Detector需要分析时需要考虑的文件集,包括:Resource文件或目录、Java文件、Class,Gradle文件。

3.3报告Issue

定义IssueRegistry,返回一个IssueList

class EleIssueRegistry: IssueRegistry() {
    override val issues: List<Issue>
        get() = Arrays.asList(HttpFormatDetector.HTTP_FORMAT_ISSUE,HttpFormatDetector.TEST_HOST_CHANGED_ISSUE,
            BitmapFactoryDetector.ISSUE, ImageSizeDetector.ISSUE, DependenciesDetector.ISSUE)
}

在gradle文件中进行注册

jar {
    manifest {
        // Only use the "-v2" key here if your checks have been updated to the
        // new 3.0 APIs (including UAST)
        attributes("Lint-registry-v2": "com.skateboard.lintchecker.registry.EleIssueRegistry")
    }
}

3.4创建LibraryModule

创建LibraryModule后再gradle文件中引用lintchecker

dependencies {
    lintChecks project(':lintchecker')
}

编译创建的librarymodule生成aar文件

3.5在项目中使用

将生成的aar文件复制到工程的libs目录下

repositories {
    flatDir {
        dirs 'libs'
    }
}
L
dependencies {
    implementation(name: 'lintcheckeraar-release', ext: 'aar')
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

这样自定义Lint的过程就基本完成了。

4.AST与PSI

如果大家熟悉自定义idea插件,那么大家应该可能会了解psi,psi就代表一个文件的内容,推荐一个插件,psiviewer这个插件,可以看到文件的psi构造,而ast代表抽象语法树,关于这两个,以后单独抽出来讲一下。

5.参考资料

Android Studio 工具:Lint 代码扫描工具(含自定义lint)

Android自定义Lint实践

关注我的公众号