简单对gradle插件实现进行复习
上面讲到要配置一个入口类,这个入口类就是实现了Plugin接口的类,它有一个override fun apply(project: Project)方法,就是我们插件开始执行的地方,相当于main函数,参数project就是整个工程的配置文件
可以使用以下方法,从我们使用插件的地方获取到对插件的配置
Python/ target=_blank class=infotextkey>Python复制代码project.extensions.create("config", Config::class.JAVA)mConfig = project.property("config") as Config
Config是一个java bean数据类
"config"是我们在build中的配置名称
上面说了为什么要实现这样一个插件和该如何实现一个gradle插件,那么下面就具体介绍该插件的实现过程
上面已经说了gradle插件的实现,那么我们就从apply方法开始说起。
既然是要hock android打包的编译过程,那就要寻找android打包时,合适的task
在android插件编译生成apk的过程中,有好多task都可以生成apk,它们的名字基于Build Types 和 Product Flavor 生成。那么我们怎么拿到具体生成apk的task组呢?
为了解决这个问题。android插件有几个属性,就是我们平常配置的变体(所谓的环境),androd中有三类变体
这三个对象都是实现了BaseVariant(BaseVariantImpl为实现这个接口的抽象类)接口的类的对象的集合
属性名
属性类型
说明
name
String
Variant 的名字,唯一
description
String
Variant 的描述说明
dirName
String
Variant 的子文件夹名,唯一。可能有不止一个子文件夹,例如 “debug/flavor1”
baseName
String
Variant 输出的基础名字,必须唯一
outputFile
File
Variant 的输出,该属性可读可写
processManifest
ProcessManifest
处理 Manifest 的 task
aidlCompile
AidlCompile
编译 AIDL 文件的 task
renderscriptCompile
RenderscriptCompile
编译 Renderscript 文件的 task
mergeResources
MergeResources
合并资源文件的 task
mergeAssets
MergeAssets
合并 assets 的 task
processResources
ProcessAndroidResources
处理并编译资源文件的 task
generateBuildConfig
GenerateBuildConfig
生成 BuildConfig 类的 task
javaCompile
JavaCompile
编译 Java 源代码的 task
processJavaResources
Copy
处理 Java 资源的 task
assemble
DefaultTask
Variant 的标志性 assemble task
因为我们的插件应该可以应用在主工程或者模块包上的,所以当我们插件运行后,我们要检测当前使用我们插件的模块是主工程,还是模块包
kotlin复制代码val hasAppPlugin = project.plugins.hasPlugin("com.android.application")val variants = if (hasAppPlugin) { (project.property("android") as AppExtension).applicationVariants} else { (project.property("android") as LibraryExtension).libraryVariants}
我们想hock住android插件运行的task任务,就需要一个重要的gradle回调
erlang复制代码project.afterEvaluate{...}
afterEvaluate该方法就是整个gradle配置文件配置成功后的回调,证明此时配置已检查完毕,所有task已经就绪,已经可以开始按指定顺序运行task了,那么我就需要在这个回调里办事!
Grade 执行顺序
执行setting,检测所有module,为每个模块配置project
加载build.properties,生成task执行链表和配置
执行某个指定task,然后会先执行该task所依赖的task
配置完成后,开始遍历variants中所有的变体
arduino复制代码project.afterEvaluate { variants.all { variant -> ... }}
mergeResourcesProvider这个任务就是android插件合并所有module中资源的task,看名字就知道了。
我们可以从变体中获取这个task对象
ini复制代码val mergeResourcesTask = variant.mergeResourcesProvider.get()
那么,我们自己的任务呢?
gradle api提供给我们可以在代码中生成task的方法
ini复制代码val mcPicTask = project.task("CheckBigImage${variant.name.capitalize()}")
使用project.task("taskname")来生成一个我们自己需要执行的task
然后我们编写这个task的逻辑,也是本插件的逻辑
复制代码mcPicTask.doLast {...}
variant里面有各种对象,allRawAndroidResources恰好就是我们需要的。它只有3.3以上才会有。
ini复制代码val dir = variant.allRawAndroidResources.files
这个dir对象,就是android所有文件资源的files集合
ok。让我们遍历这个文件list吧!
scss复制代码for (channelDir: File in dir) {check(channelDir)}fun check(file: File) { if(file.isDirectory) { check(file)} else { process(file)}}
如果遇到文件夹,这里是一个递归调用。
如果遇到文件,就可以按照自己的规则处理了。
我们task写好后,需要和mergeResourcesProvider挂钩
less复制代码mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
使mergeResourcesTask依赖我们的mcPicTask,当mergeResourcesTask执行前,就会先执行我们的mcPicTask了!!
注意:此处直接使用mergeResourcesTask系统task依赖我们的task,我们的task执行顺序会和mergeResourcesTask原有的依赖混杂在一起,不可控。后面讲一种可控的方法
这个逻辑应该实现在上面伪代码process(file:File)方法中
这里我们只处理普通图片转换为webp的压缩。jpg,png的自压缩原理相同,就不复述了
想压缩转换webp图片,需要用到转换工具
google提供的有一套命令行转换工具:cwebp ,各个平台都有,我们去下载一套,放在我们的主工程文件夹下就可以了
这里需要注意的是:为了方便,如果把cwebp命令行程序放在环境变量下,那么执行命令时,拼接命令时,直接拼接cwebp就好。
如果使用工程目录下的cwebp,执行前,需要在cwebp命令前面拼接它所在的工程目录。
使用
lua复制代码project.rootDir.path
可以获取工程的根目录
可以使用java的api
scss复制代码Runtime.getRuntime().exec(cmd)
现在可以愉快的转换图片了
bash复制代码Tools.cmd("cwebp", "${imgFile.path} -o ${webpFile.path} -m 6 -quiet")
转换后,记得把原图删掉
优化点:
有的图片转换后比以前还大,这里需要注意
第一次扫描过后的无法优化的图片,可以存在一个text文本当中,第二次执行时,就不要去转换了
在linux系统上,创建和删除文件都需要权限,如果没有权限就会失败。这时需要先判断当前的操作系统是不是linux,如果是,可以执行chmod 755 -R ${FileUtil.getRootDirPath()}添加权限
这里可以优化一下,在我们的mcPicTask前面再加一个task,用来添加权限,这个task只对文件夹进行递归添加就可以了,比一个一个文件要来的快。
因为我们不清楚系统的task(mergeResourcesTask)都依赖了哪些,那么如何在依赖上再加依赖,如何插入task呢?
gradle api提供给了我们一个方法,
xxx.taskDependencies.getDependencies(xxx)可以获取自己的依赖树
在这里就是
scss复制代码(project.tasks.findByName(chmodTask.name) asTask).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))
让chmodTask依赖mergeResourcesTask的依赖。假如mergeResourcesTask是A,chmodTask是B。A依赖一个系统的C。那么上面的代码就是让B依赖了C。这时的task图就是 B->C,A->C
接下来我们再把mcPicTask(简称为D)也依赖进来
arduino复制代码(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)
这时就是D->B->C,A->C
最后,回到我们刚刚拦截图片的逻辑的最后代码
less复制代码mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
就变成了A->D->B->C,也就是mergeResourcesTask->mcPicTask->chmodTask->原依赖task,依赖和执行顺序是相反的。
正常的代码就是
scss复制代码(project.tasks.findByName(chmodTask.name) asTask).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
直接使用
mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))插入task。执行顺序打印
......
Task :app:mainApkListPersistenceDebug UP-TO-DATE
Task :app:CheckBigImageDebug
Task :app:generateDebugResValues UP-TO-DATE Task :app:generateDebugResources UP-TO-DATE Task :app:mergeDebugResources
......
而使用正规的插入法顺序
Task :app:mainApkListPersistenceDebug UP-TO-DATE Task :app:generateDebugResValues UP-TO-DATE Task :app:generateDebugResources UP-TO-DATE Task :app:chmodDebug
Task :app:CheckBigImageDebug
Task :app:mergeDebugResources
我们上面的例子,都是基于比较最新的gradle和android gradle tools版本(>3.3),android插件直接提供给了我们allRawAndroidResources,方便无比,直接在merge前遍历它就好了。
那么3.3之前的版本呢?就是我们最初的设想了,在合并完各个module资源后,扫描merge文件夹!这里又有aapt和aapt2的差异
关掉aapt2
ini复制代码android.enableAapt2=false
在mergeDebugResources后,processDebugResources前扫描文件夹
前面说过,mergeDebugResources是合并所有module的资源文件到固定目录
那么processDebugResources是什么呢?就是处理这些已经合并完成的文件,生成R.id,资源索引之类的文件
那么我们的任务就必须插入到processDebugResources前面,而不是mergeDebugResources了
仔细翻了翻MergeResources里面的方法,有一个getResSet和computeResourceSetList看起来有点意思。那么computeResourceSetList中又调用了getResSet。最后发现computeResourceSetList果然可以获取所有文件列表。
less复制代码/*** Computes the list of resource sets to be used during execution based all the inputs.*/@VisibleForTesting@NonNullList<ResourceSet> computeResourceSetList()
注释也很有意思,有道翻译一下:根据所有输入计算执行期间使用的资源集列表。
鉴于该方法是友元方法,就使用反射获取。
因为3.3之后,aapt2是强制开启的,并且aapt2 merge后的文件不是原文件了哦!注意aapt1合并后,还是正常的xxx.png。aapt2合并后的文件扩展名为flat
所以,方法一不支持大于3.3的gradle版本。方法二支持。可以平滑过渡到新版本。鉴于新版本的gradle直接提供了allRawAndroidResources这样的方法,所以在3.3以上,直接使用它就可以了
allRawAndroidResources提供的是未合并前的资源路径
扫描合并文件夹,扫描的是编译期merge成功后的文件夹