【插件&热修系列】Shadow源码解析之插件打包流程

545 阅读4分钟

引言

上一阶段,我们学习了sample-manager模块的设计理念和具体实现:

(1)【插件&热修系列】Shadow源码解析之sample-manager(一)

(2)【插件&热修系列】Shadow源码解析之sample-manager(二)

到这里,我们的热修已经完成了《宿主》 和 《管理插件》模块

那么下一个阶段就是插件模块了,比如:

  • 多个业务插件生产流程中是怎么打成zip发布的?
  • 每个插件是怎么写的?
  • 插件里面的组件是怎么支持的?
  • .........

下面我们将以《插件生产流程中是怎么打成zip发布的》内容,开启新的征程

概要

111.png

这个是shadow插件zip包里面的内容,有4个插件,1个配置文件

222.png

配置文件如上,可以看出是插件的一些版本信息等

本章将学习如何用gradle插件生成这些信息

实现流程

1.实现目的

本次主要实现把《前面讲到的 sample-manager.apk》 进行打包成zip

2.效果展示

执行打包任务 333.png 然后会生产zip包到指定目录 44.png

3.插件定义/上传/使用相关gradle配置

(1)插件定义

555.png

这里使用的定义方式是新版本的方式,和以前讲的不太一样

新方式叫Composing builds,以前的方式叫 buildSrc

主要区别是:

  • buildSrc,A change in buildSrc causes the whole project to become out-of-date(意思就是只要有变化了,那么整个工程都会编译,效率不高),具体细节戳这里>>>
  • Composing builds,A composite build is simply a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single projects, complete builds are included.(意思是复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects,特点是其编译速度快),具体戳这里>>>

除了看官方文档外,网上也有比较多不错的介绍,如字节同学的分享

(2)插件上传 666.png 这里用了比较简单的上传方式,上传到本地工程的repo下

(3)插件引入

777.png

(4)插件使用

888.png

我们使用配置的是把sample-manager输出的apk打包成zip

4.工程简介

222.png

这个是把官方的代码合并之后的效果,原官方结构如下:

111.png

5.源码解析

(1)寻找本地androidJar,然后自定义 classPool,完成disable_shadow_transform 开关功能

999.png

因为开关功能不是本章重点,所以实现暂时注释,了解下其代码大概作用即可

(2)自定义拓展

9991.png

ShadowExtension,是一个集合的实现

open class ShadowExtension {
        var transformConfig = TransformConfig()
        fun transform(action: Action<in TransformConfig>) {
            action.execute(transformConfig)
        }
}

class TransformConfig {
    var useHostContext: Array<String> = emptyArray()
}

PackagePluginExtension,具体样子如下:

open class PackagePluginExtension {
    var loaderApkProjectPath = ""
    var runtimeApkProjectPath = ""
    var archivePrefix = ""
    var archiveSuffix = ""
    var destinationDir = ""
    var uuid = ""
    var version: Int = 0
    var uuidNickName = ""
    var compactVersion: Array<Int> = emptyArray()
    var buildTypes: NamedDomainObjectContainer<PluginBuildType>
}

里面的 PluginBuildType 如下:

open class PluginBuildType {
    var name = ""
    var loaderApkConfig: Tuple2<String, String> = Tuple2("", "")
    var runtimeApkConfig: Tuple2<String, String> = Tuple2("", "")
    lateinit var pluginApks: NamedDomainObjectContainer<PluginApkConfig>
}

里面的 PluginApkConfig 如下:

open class PluginApkConfig {
    var name = ""
    var partKey = ""
    var businessName = ""
    var apkName = ""
    var apkPath = ""
    var buildTask = ""
    var dependsOn: Array<String> = emptyArray()
    var hostWhiteList: Array<String> = emptyArray()
}

(3)task创建

 //根据配置的 pluginTypes 创建执行任务
val tasks = mutableListOf<Task>()
for (i in buildTypes) {
     //组织apk和json等插件为zip
     val task = createPackagePluginTask(project, i)
     tasks.add(task)
}

根据 buildTypes 来构建任务(createPackagePluginTask)

buildTypes是上面的PluginBuildType,本例子只有一个debug类型

然后我们来看下 createPackagePluginTask 的具体实现:

internal fun createPackagePluginTask(project: Project, buildType: PluginBuildType): Task {

    /**
     * 目录
     * */
    val targetConfigFile = File(project.projectDir.absolutePath + "/generatePluginConfig/${buildType.name}/config.json")

    /**
     * 1)依赖于 createGenerateConfigTask
     * 2)压缩zip包
     * */
    return project.tasks.create("package${buildType.name.capitalize()}Plugin", Zip::class.java) {
        System.err.println("PackagePluginTask task start run...")

        //runtime apk file
        val runtimeApkName: String = buildType.runtimeApkConfig.first
        var runtimeFile: File? = null
        if (runtimeApkName.isNotEmpty()) {
            System.err.println("runtime apk...")
            runtimeFile = ShadowPluginHelper.getRuntimeApkFile(project, buildType, false)
        }

        //loader apk file
        val loaderApkName: String = buildType.loaderApkConfig.first
        var loaderFile: File? = null
        if (loaderApkName.isNotEmpty()) {
            System.err.println("loader apk...")
            loaderFile = ShadowPluginHelper.getLoaderApkFile(project, buildType, false)
        }

        //config file
        //不存在:/Users/yabber/AndroidStudioProjects/DemoHot/sample-plugin-app/build + /intermediates
        //val targetConfigFile = File(project.buildDir.absolutePath + "/intermediates/generatePluginConfig/${buildType.name}/config.json")
        //val targetConfigFile = File(project.projectDir.absolutePath + "/generatePluginConfig/${buildType.name}/config.json")
        val result = targetConfigFile.parentFile.mkdirs()
        System.err.println("mkdirs config.json parentFile dir, result = " + result)
        System.err.println("targetConfigFile = " + targetConfigFile.absoluteFile)

        //all plugin apks
        val pluginFiles: MutableList<File> = mutableListOf()
        for (i in buildType.pluginApks) {
            val file = ShadowPluginHelper.getPluginFile(project, i, false)
            System.err.println("pluginFile = " + file.absoluteFile)
            pluginFiles.add(file)
        }

        it.group = "plugin"
        it.description = "打包插件"
        it.outputs.upToDateWhen { false }
        if (runtimeFile != null) {
            pluginFiles.add(runtimeFile)
        }
        if (loaderFile != null) {
            pluginFiles.add(loaderFile)
        }
        it.from(pluginFiles, targetConfigFile)//  from

        val packagePlugin = project.extensions.findByName("packagePlugin")
        val extension = packagePlugin as PackagePluginExtension

        val suffix = if (extension.archiveSuffix.isEmpty()) "" else extension.archiveSuffix
        val prefix = if (extension.archivePrefix.isEmpty()) "plugin" else extension.archivePrefix
        if (suffix.isEmpty()) {
            it.archiveName = "$prefix-${buildType.name}.zip"
        } else {
            it.archiveName = "$prefix-${buildType.name}-$suffix.zip"
        }
        //destinationDir
        it.destinationDir = File(if (extension.destinationDir.isEmpty()) "${project.rootDir}/build" else extension.destinationDir)
        System.err.println("destinationDir = " + it.destinationDir.absoluteFile)

        System.err.println("PackagePluginTask task end...")
    }.dependsOn(createGenerateConfigTask(project, buildType))
}

这里做了如下几件事:

(1)创建任务 package${buildType.name.capitalize()}Plugin

(2)准备插件apk File,如:runtime apk / loader apk / sample-manager apk / 业务插件 apk

qqq.png

(3)准备好一系列参数,然后把插件拷贝到指定目录

66776677.png

(4)创建另外一个任务,并且让上面任务依赖这个任务

090909.png

下面我们来看下另外一个任务(createGenerateConfigTask)做了啥?

private fun createGenerateConfigTask(project: Project, buildType: PluginBuildType): Task {
    System.err.println("GenerateConfigTask task run ... ")

    /**
     * 目录
     * */
    val targetConfigFile = File(project.projectDir.absolutePath + "/generatePluginConfig/${buildType.name}/config.json")

    val packagePlugin = project.extensions.findByName("packagePlugin")
    val extension = packagePlugin as PackagePluginExtension

    //runtime apk build task
    val runtimeApkName = buildType.runtimeApkConfig.first
    var runtimeTask = ""
    if (runtimeApkName.isNotEmpty()) {
        runtimeTask = buildType.runtimeApkConfig.second
        System.err.println("runtime task = $runtimeTask")
        //println("runtime task = $runtimeTask")
    }


    //loader apk build task
    val loaderApkName = buildType.loaderApkConfig.first
    var loaderTask = ""
    if (loaderApkName.isNotEmpty()) {
        loaderTask = buildType.loaderApkConfig.second
        System.err.println("loader task = $loaderTask")
        //println("loader task = $loaderTask")
    }

    //插件工程任务,如::sample-manager:assembleDebug
    val pluginApkTasks: MutableList<String> = mutableListOf()
    for (i in buildType.pluginApks) {
        val task = i.buildTask
        System.err.println("pluginApkProjects task = $task")
        //println("pluginApkProjects task = $task")
        pluginApkTasks.add(task)
    }

    /**
     * 1)依赖于  pluginApkTasks
     * */
    val task = project.tasks.create("generate${buildType.name.capitalize()}Config") {
        it.group = "plugin"
        it.description = "生成插件配置文件"
        it.outputs.file(targetConfigFile)
        it.outputs.upToDateWhen { false }
    }
            .dependsOn(pluginApkTasks)//依赖于插件工程任务,如::sample-manager:assembleDebug
            .doLast {

                System.err.println("generate json Config task begin")
                //println("generateConfig task begin")
                val json = extension.toJson(project, loaderApkName, runtimeApkName, buildType)
                System.err.println("json = " + json)

                val bizWriter = BufferedWriter(FileWriter(targetConfigFile))
                bizWriter.write(json.toJSONString())
                bizWriter.newLine()
                bizWriter.flush()
                bizWriter.close()

                System.err.println("generateConfig task done")
            }
    if (loaderTask.isNotEmpty()) {
        task.dependsOn(loaderTask)
    }
    if (runtimeTask.isNotEmpty()) {
        task.dependsOn(runtimeTask)
    }
    return task
}

主要做了几件事:

(1)参数准备,如:拓展里面读取参数

111.png

(2)创建任务,执行json文件生成,并且任务在指定的任务后面执行(如:在sample-manager:assembleDebug后)

333.png

到这里,可以看到任务时序大致是这样的:

  • 先是插件生成apk,如:sample-manager:assembleDebug后
  • 然后是根据生成的apk,生成json文件
  • 最后是把apk和json打包成zip

源码地址

github.com/DaviAndorid…

结尾

哈哈,该篇就写到这里(一起体系化学习,一起成长)

Tips

深挖安卓移动领域,沉淀安卓应用等技术,微信“ DaviZgx ”,欢迎添加进群交流