伪造出一个假的系统View | Gradle Task

4,854 阅读4分钟

前言

前一阵子帮业务同学解决了个代码问题,其实挺有意思的,就打算和大家分享下这个内容。

先简单的介绍下背景,业务同学写了个apt的框架,然后里面包含一个注解的库,而注解库中需要使用到Android源码中的View。但是因为这是一个Java Library,无法直接将安卓的源码添加到依赖中,就无法引用到View。然后他们为了解决这个问题,又创建了一个库,然后生成了一个同包名的Android View,类似下图这总,然后compileOnly这个库。

image.png

因为这个模块内有了这个View,业务同学在后续调试系统源码的时候都会进到这个造假出来的View上去了,就产生了很大的干扰作用。原因呢其实就是因为这个类呗添加到sourceSet中了,同名类的情况下会优先使用上层加载的。

这种在java库内需要造假出一些Android View,就变成了一个很好玩的东西了。接下来我们就通过gradle的一些简单的操作,来把这个坑填上。

详细代码可以看下这个 Router-Android

Gradle Java Compiler Task

build.gradle中,我们可以声明一个task任务,然后声明这个任务继承的类型,让它变成一个可以java编译代码的任务。

task("stubLib", JavaCompile::class) {
    source(file("src/stub/java"))
    classpath = project.files(getAndroidJar("32"))
    // libraries
    destinationDirectory.set(File(project.buildDir, "/tmp/stubLibs"))
}


fun getAndroidJar(compileSdkVersion: String): String {
    var androidSdkDir =
        System.getenv(com.android.tools.analytics.Environment.EnvironmentVariable.ANDROID_SDK_HOME.key)
    if (androidSdkDir.isNullOrEmpty()) {
        val propertiesFile = rootProject.file("local.properties")
        if (propertiesFile.exists()) {
            val properties = Properties()
            properties.load(propertiesFile.inputStream())
            androidSdkDir = properties.getProperty("sdk.dir")
        }
    }
    if (androidSdkDir.isNullOrEmpty()) {
        throw  StopExecutionException("please declares your 'sdk.dir' to file 'local.properties'")
    }
    val path = "platforms${File.separator}android-${compileSdkVersion}${File.separator}android.jar"
    return File(androidSdkDir.toString(), path).absolutePath
}

看起来这段代码就比较简单。首先我们声明了一个gradle task(gradle基础概念 有兴趣的可以自己去了解下),这个Task继承自JavaCompile,然后输入的是src/stub/java这个文件夹下的内容,classpath是android源代码,输出是工程的build//tmp/stubLibs文件夹。

介绍完了Task的声明之后,它会做些什么。这个声明的任务会基于他的输入内容,然后执行java编译任务,最后把.class输出到输出的文件夹下。

获取Android.jar

这个比较简单,其实Android.jar是要区分compile版本的,这些都放在android sdk下。类似这种/Android/sdk/platforms/android-32/android.jar。代码就是上面的getAndroidJar

class -> jar

上面这个JavaCompile任务负责的就是将java转变成class文件,但是并没有办法直接被工程使用。因为工程内我们只能依赖于jar或者aar的依赖方式,而没有办法使用class文件。所以我们要做的就是把这些class通过另外一个任务压缩成一个jar包。

task("stubLibsJar", Jar::class) {
    archiveBaseName.set("stub")
    archiveVersion.set("1.0")
    from(tasks.getByName("stubLib"))
    include("**/*.class")
}

这个也是Gradle内提供的一个任务,可以从类型中看出来就是一个转化Jar::class的任务。其中jar的名字叫stub,版本号1.0。内容则来自前置的任务stubLib(我们上面声明的那个任务)。然后包含里面所有的.class文件。之后把这些内容都转化成一个jar包输出。

dependencies中执行任务

上面的这个方法已经让我们可以在一个"java-library"中使用安卓编译出来的jar包了。但是我们的代码内还没有办法建立索引,因为configuration内并不存在这个jar包,我们需要把这个编译产物添加到dependencies中去才行。

dependencies {
    //  implementation fileTree(dir: 'libs', include: ['*.jar'])
    val stub = tasks.getByName("stubLibsJar").outputs.files
    compileOnly(stub)
}

先从taskManager中获取到这个任务,然后取出这个任务的output的文件,然后compileOnly这个jar。

通过这种方式我们就可以活学活用gradle的特性,先造假出一些我们想要的假的系统类,然后编译成jar包,之后仅在编译时使用这些,这样这些类在实际运行时就会被替换成android.jar中的类了。

这样一开始我们说的工程内的问题就被我们完美的规避和解决了。

结尾

本文可以当做一个gradle task的入门文章,通过几个简单的例子给大家介绍下。我之前也关注了些Gradle相关的文章,一般介绍的gradle task的文章就有点太无聊了,很难有用一个生动的例子和各位说明为什么需要task,输入输出的含义是什么,希望本文对大家有所帮助。

另外作为一个老卷逼了,最近在做整个工程的kotlin+compose+androidx的升级工作,进度还是挺顺利的,能不能顺利提桶就看这一出了啊。