1. 前言
项目中遇到了一个问题 :
其中一个模块MyLibrary的assets文件夹中,需要存放很多文件(每个文件对应一个功能)。
这样导致的问题是MyLibrary打出的这个aar包体积特别大。
如果把MyLibrary严谨地拆解成若干个Module又比较费时,对于现在业务现状来说也显得没那么必要。
那么能不能在上传MyLibrary这个aar的时候,自动复制相应的文件到assets目录下,打出不同功能的aar呢 ?
这就需要自己开发一个Gradle插件来完成这个功能了。
本文环境
- Android Studio 版本 : Android Studio Hedgehog | 2023.1.1
- Gradle版本 : gradle-8.2
- AGP版本 : 8.2.0
- 项目结构 : 项目有app模块和MyLibrary模块,使用build.gradle (Groovy语言),app的assets目录下,有test1.so、test2.so、test3.so这三个文件
2. 配置上传Maven仓库
首先我们把MyLibrary配置上传Maven的插件,也就是maven-publish。
对于这部分功能不了解的同学可以先看我的这篇博客 : Android Module上传到Maven仓库 及 实现同时上传到多个Maven仓库
下面我们简单讲述一下
复制maven_upload.gradle到项目根目录下
apply plugin: 'maven-publish'
//TODO 这里填你Maven仓库的地址
def RELEASE_REPOSITORY_URL = "https://devops-maven.xxxxx.com/repository/yyyyy/"
//TODO 这里填你Maven仓库的账号
def NEXUS_USERNAME = "*********"
//TODO 这里填你Maven仓库的密码
def NEXUS_PASSWORD = "*********"
afterEvaluate {
publishing {
repositories {
maven {
name("ReleaseMaven")
url = RELEASE_REPOSITORY_URL
credentials {
username = NEXUS_USERNAME
password = NEXUS_PASSWORD
}
}
}
publications {
Production(MavenPublication) {
from components.release
groupId = rootProject.ext.GROUP
artifactId = rootProject.ext.POM_ARTIFACT_ID
version = rootProject.ext.VERSION_NAME
}
}
}
}
在MyLibrary中依赖
rootProject.ext.GROUP = "com.heiko.mytest"
rootProject.ext.POM_ARTIFACT_ID = "mylibrary"
rootProject.ext.VERSION_NAME = "1.0.0"
apply from: "${
project.rootDir}/maven_upload.gradle"
Sync一下,可以看到gradle中多了publishing这个文件夹,里面的publishProductionPublicationToReleaseMavenRepository就是用来将MyLibrary打包并上传到Maven仓库的Gradle命令了。
3. Gradle相关操作
3.1 复制文件
from是原目录,into是目标目录,include可以指定需要复制的文件,onlyIf可以用来判断是否执行复制任务。
task copyFiles(type: Copy) {
from 'src/main/assets'
into 'build/outputs/assets'
//include 'test1.txt','test2.txt' //指定文件名
include '**/*.txt' //根据*匹配符合要求的文件
onlyIf {
true
}
}
3.2 删除文件
task myDeleteFilesInDir(type: Delete) {
//delete 'src/main/assets/test1.txt' //删除test1.txt
//delete 'src/main/assets/test1.txt' //删除文件夹下所有的文件,assets文件也会被删除
delete fileTree('src/main/assets') //删除文件夹下所有的文件,assets文件不会被删除
}
3.3 dependsOn
dependsOn表示一个任务需要另一个任务先完成,可以理解为依赖于或需要先做,这意味着在执行这个任务之前,它所依赖的任务必须首先执行。
例如,如果你有一个任务叫做compile,它需要在clean任务之后执行,就可以像这样声明依赖关系:
task clearTask {
doLast {
println("执行 clear.doLast")
}
}
task compileTask {
doLast {
println("执行 compile.dolast")
}
}
compileTask.dependsOn(clearTask)
这样,每当你运行compile任务时,Gradle会首先运行clean任务。
执行compile任务的日志如下
> Task :app:clearTask
执行 clear.doLast> Task :app:compileTask
执行 compile.dolastBUILD SUCCESSFUL in 510ms
更通俗的理解 :
dependsOn就像是做饭的顺序:你首先需要准备食材,然后才能开始烹饪。同样地,如果你有一个任务依赖于另一个任务,那么你需要在开始当前任务之前先完成那个依赖任务。如果没有这种依赖关系,那么任务可能会在错误的时机执行,导致结果不正确或者出现错误。
//准备食材任务
task prepareFood(){
}
//做饭任务
task cooking(){
}
//做饭任务 依赖于 准备食材任务
cooking.dependsOn(prepareFood)
3.4 finalizedBy
finalizedBy
用于指定一个任务
在另一个任务完成之后
执行,可以理解为在完成后执行
。
在Gradle
中,finalizedBy
用于指定一个或多个任务,这些任务将在关联任务执行完毕后执行,无论关联任务是否成功。可以把这个理解为一种清理或收尾的工作。
举个例子,我们有一个任务A,它被finalizedBy
任务B,那就意味着在任务A执行完之后,无论任务A是否成功,任务B都会被执行。
这就好比一个厨师在做完一道菜(任务A
)之后,无论这道菜是否做得成功,他都需要清理厨房(任务B
),那么清理厨房这个步骤就是做菜这个任务的finalizedBy
。
3.5 mustRunAfter和shouldRunAfter
在Gradle
中,mustRunAfter
是用来定义任务执行的顺序的。如果你有两个任务,比如说任务A
和任务B
,你希望无论何时,只要这两个任务都被执行,任务A
都必须在任务B
之后执行,那么你就可以在任务A
上调用 mustRunAfter
方法并传入任务B
。
举个例子,如下代码:
taskA.mustRunAfter taskB
这段代码的意思就是,如果这两个任务都在执行队列中,那么无论何时,任务A
都必须在任务B
之后执行。
需要注意的是,mustRunAfter
不会强制执行任务B
。如果任务B
没有被加入到执行队列中,那么任务A
也可以独立执行。同样,如果任务A
没有被加入到执行队列中,那么任务B
也可以独立执行。
这与 dependsOn
不同。dependsOn
会创建一个强制的依赖关系,也就是说,任务A
依赖于任务B
,那么只要任务A
需要执行,任务B
就必须要先执行。而 mustRunAfter
只是定义了一个任务的执行顺序,并不会创建一个依赖关系。
shouldRunAfter
和mustRunAfter
是一样的,只不过mustRunAfter
如果导致了循环依赖,Gradle
会抛出一个错误,并导致构建失败。而shouldRunAfter
不会抛出异常,Gradle
会忽略这个顺序要求,不会导致构建失败。
4. 新建插件
4.1 创建插件类
在MyLibrary
的build.gradle
中,增加如下代码,创建插件类 : MyPlugin
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println "---- MyPlugin.apply ----"
project.afterEvaluate {
//在Gradle项目配置阶段完成后会被调用
//后续代码放在这里...
}
}
}
4.2 应用这个插件
在MyLibrary
的build.gradle
中添加这行代码,表示应用这个插件
apply plugin : MyPlugin
在Sync
一下,可以看到会打印出日志,表明我们配置这个插件成功了
---- MyPlugin.apply ----
5. 实现初步的依赖
5.1 在app的assets中添加文件
在app
的assets
目录下,添加test1.so
、test2.so
、test3.so
这三个文件
5.2 MyLibrary新建assets目录
在MyLibrary
下新建assets
文件夹
5.3 在MyPlugin中实现复制文件的Task
注意
: 后面的这些操作,代码都放在MyPlugin
的apply
方法的project.afterEvaluate {}
里
def myCopyFiles = project.task("myPublishCopyTask", type: Copy) {
from project.fileTree('../app/src/main/assets')
include 'test1.so', 'test2.so'
into 'src/main/assets'
}
myCopyFiles.group = "publishingV2"
Sync
一下项目,可以在Android Studio
的Gradle Tab
中,看到publishingV2
文件夹,里面有myPublishCopyTask
这个Task。
我们点击这个Task
,可以发现,app
的assets
目录下的test1.so
和test2.so
已经被复制到MyLibrary
的assets
目录下了
5.4 在MyPlugin中实现删除文件的Task
//原本task名叫做myPublishDeleteTask,但是后面关联了其他Task后,这个task的名称起做publishAutoCopyTask是比较合适的
def myDeleteFilesInDir = project.task("publishAutoCopyTask", type: Delete) {
delete project.fileTree('src/main/assets')
}
myDeleteFilesInDir.group = "publishingV2"
Sync
一下项目,可以在Android Studio
的Gradle Tab
中,看到publishingV2
文件夹,里面有publishAutoCopy
这个Task
。
我们点击publishAutoCopy
这个Task
,可以发现,MyLibrary
的assets
目录下的test1.so
和test2.so
已经被删除了
5.5 关联Task
这里,我们调用packageReleaseAssets.mustRunAfter(myCopyFiles)
,表明调用packageReleaseAssets
之前,必定会调用myCopyFiles
,否则就会抛出异常。
然后调用myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask)
,表示运行myDeleteFilesInDir
的时候,会先去执行myCopyFiles
和publishTask
。
def packageReleaseAssets = project.tasks.findByName("packageReleaseAssets")
def publishTask = "publishProductionPublicationToReleaseMavenRepository"
packageReleaseAssets.mustRunAfter(myCopyFiles)
myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask)
5.6 来看一下整体的代码
这部分代码都在MyLibrary
的build.gradle
中
rootProject.ext.GROUP = "com.heiko.mytest"
rootProject.ext.POM_ARTIFACT_ID = "mylibrary"
rootProject.ext.VERSION_NAME = "1.0.0"
apply from: "${project.rootDir}/maven_upload.gradle"
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println "---- MyPlugin.apply ----"
project.afterEvaluate {
def myCopyFiles = project.task("myPublishCopyTask", type: Copy) {
from project.fileTree('../app/src/main/assets')
include 'test1.so', 'test2.so'
into 'src/main/assets'
}
myCopyFiles.group = "publishingV2"
//原本task名叫做myPublishDeleteTask,但是后面关联了其他Task后,这个task的名称起做publishAutoCopy是比较合适的
def myDeleteFilesInDir = project.task("publishAutoCopyTask", type: Delete) {
delete project.fileTree('src/main/assets')
}
myDeleteFilesInDir.group = "publishingV2"
def packageReleaseAssets = project.tasks.findByName("packageReleaseAssets")
def publishTask = "publishProductionPublicationToReleaseMavenRepository"
packageReleaseAssets.mustRunAfter(myCopyFiles)
myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask)
}
}
}
apply plugin: MyPlugin
5.7 调用myPublishDeleteTask
这个时候,我们在Gradle
中执行myPublishDeleteTask
这个任务,会发现 自动复制文件 -> 打包 -> 上传到Maven仓库 -> 删除文件
这个链路,都自动执行了。
5.7.1 查看Maven仓库
我们来查看下Maven
仓库,可以看到已经上传到maven
仓库了
5.7.2 下载aar文件
下载这个版本的aar
文件
接着拖到Android Studio
中,就可以看到assets
里已经有test1.so
和test2.so
了
6. 实现动态配置
接着我们就可以来实现动态的配置了。
要实现动态配置,就需要在app
的build.gradle
中配置名称和要复制的文件名称,像下面这样 :
apply plugin: MyPlugin
publishAutoCopy {
//要复制的源文件目录
sourcePath = '../app/src/main/assets'
//要复制的目标文件目录
targetPath = 'src/main/assets'
publishItem {
//名称
name = 'weather'
//要复制的文件名
sourceFiles = ['test1.so', 'test2.so']
}
publishItem {
//名称
name = 'ocr'
//要复制的文件名
sourceFiles = ['test3.so']
}
}
那么需要怎么将这些配置传递到MyPlugin
插件中呢 ?
6.1 编写传参类
class PublishAutoCopyItem {
String name
ArrayList<String> sourceFiles
}
class PublishAutoCopyExtension {
private Project project
String targetPath
String sourcePath
List<PublishAutoCopyItem> items = []
PublishAutoCopyItem publishItem(Closure closure) {
PublishAutoCopyItem myItem = new PublishAutoCopyItem()
project.configure(myItem, closure)
items << myItem
return myItem
}
PublishAutoCopyExtension(Project project) {
this.project = project
}
}
6.2 在Plugin中获取传参
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def extension = project.extensions.create("publishAutoCopy", PublishAutoCopyExtension)
project.afterEvaluate {
def sourcePath = extension.sourcePath ?: ""
def targetPath = extension.targetPath ?: ""
def items = extension.items
println("sourcePath:" + sourcePath + " targetPath:" + targetPath)
for (final def item in items) {
println("item.name:" + item.name+" sourceFiles:"+item.sourceFiles)
}
//省略了之前写的代码...
}
}
}
6.3 在Build.gradle中配置
这个配置在MyLibrary
的build.gradle
中即可
publishAutoCopy {
//要复制的源文件目录
sourcePath = '../app/src/main/assets'
//要复制的目标文件目录
targetPath = 'src/main/assets'
publishItem {
//名称
name = 'weather'
//要复制的文件名
sourceFiles = ['test1.so', 'test2.so']
}
publishItem {
//名称
name = 'ocr'
//要复制的文件名
sourceFiles = ['test3.so']
}
//更多的配置可以在这里增加...
}
6.4 Sync下项目
然后我们Sync
下项目,可以看到Gralde
打印出了如下的日志
> Configure project :NcnnLibrary
sourcePath:../app/src/main/assets targetPath:src/main/assets
item.name:weather sourceFiles:[test1.so, test2.so]
item.name:ocr sourceFiles:[test3.so]
这样,我们就将配置传递给我们自定义的MyPlugin
插件了
6.5 将参数传递给Task
将这些参数传递给Task,最终代码如下
project.afterEvaluate {
def names = new ArrayList<>()
def sourcePath = extension.sourcePath ?: ""
def targetPath = extension.targetPath ?: ""
for (final def item in extension.items) {
def name = item.name ?: ""
def sourceFiles = item.sourceFiles ?: [""]
if (name.isEmpty()) return
names.add(name)
def nameCapitalize = name.capitalize()
def myCopyFiles = project.task("myPublish${nameCapitalize}CopyTask", type: Copy) {
from project.fileTree(sourcePath)
include sourceFiles
into targetPath
}
def myDeleteFilesInDir = project.task("publish${nameCapitalize}AutoCopy", type: Delete) {
delete project.fileTree(targetPath)
}
myDeleteFilesInDir.group = "publishingv2"
def packageReleaseAssets = project.tasks.findByName("packageReleaseAssets")
def publishTask = "publish${nameCapitalize}PublicationToReleaseMavenRepository"
packageReleaseAssets.mustRunAfter(myCopyFiles)
myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask)
}
project.rootProject.ext["${project.name}_NAMES"] = names
}
Sync
项目后,可以发现有publishOcrAutoCopy
和publishWeatherAutoCopy
两个Task
了
6.6 修改 maven_upload.gradle
myArtifactId
现在通过rootProject.ext.POM_ARTIFACT_ID
进行传递,所以我们现在需要修改maven_upload.gradle
apply plugin: 'maven-publish'
//TODO 这里填你Maven仓库的地址
def RELEASE_REPOSITORY_URL = "https://devops-maven.xxxxx.com/repository/yyyyy/"
//TODO 这里填你Maven仓库的账号
def NEXUS_USERNAME = "*********"
//TODO 这里填你Maven仓库的密码
def NEXUS_PASSWORD = "*********"
afterEvaluate {
publishing {
repositories {
maven {
name("ReleaseMaven")
url = RELEASE_REPOSITORY_URL
credentials {
username = NEXUS_USERNAME
password = NEXUS_PASSWORD
}
}
}
publications {
List<String> names
def namesKey = "${project.name}_NAMES"
if (rootProject.ext.has(namesKey)) {
names = rootProject.ext[namesKey]
} else {
names = new ArrayList()
names.add("")
}
for (final def itemName in names) {
def publicationName = itemName.capitalize()
create(publicationName, MavenPublication) {
def myArtifactId
if (name.isEmpty()) myArtifactId = rootProject.ext.POM_ARTIFACT_ID
else myArtifactId = rootProject.ext.POM_ARTIFACT_ID + "-" + itemName
from components.release
groupId = rootProject.ext.GROUP
artifactId = myArtifactId
version = rootProject.ext.VERSION_NAME
}
}
}
}
}
6.7 动态配置完成
到这里,我们就完成动态配置了 : 我们想要打包带某些文件的aar
,就点击对应的Task
进行打包就行了
- 想打带
test1.so
和test2.so
的包,那么就点击publishWeatherAutoCopy
- 想打带
test3.so
的包,那么就点击publishOcrAutoCopy
7. 将MyPlugin移到独立的gradle文件中
我们还可以将MyPlugin
相关的代码移到一个单独的gradle
文件中,比如publish_auto_copy.gradle
,具体方式和maven_upload.gradle
类似。
这样,我们就可以在build.gradle
中,使用apply from: "${project.rootProject.rootDir}/publish_auto_copy.gradle"
这一句代码,就应用MyPlugin
插件了。接着,在build.gradle
中配置好MyPlugin
的参数就行了。
apply from: "${project.rootProject.rootDir}/publish_auto_copy.gradle"
publishAutoCopy {
//要复制的源文件目录
sourcePath = '../app/src/main/assets'
//要复制的目标文件目录
targetPath = 'src/main/assets'
publishItem {
//名称
name = 'weather'
//要复制的文件名
sourceFiles = ['test1.so', 'test2.so']
}
publishItem {
//名称
name = 'ocr'
//要复制的文件名
sourceFiles = ['test3.so']
}
//更多的配置可以在这里增加...
}
至此,我们就完成了打包自动复制文件插件的全部功能了。