Android 图片资源检测插件实现
为什么要检测图片资源?
避免不小心把未压缩,不合适的图片资源打入 apk 中,造成 apk 过大
图片打入 apk 前,可以自动化转换,压缩
实现思路
思路一:使用 gradle 在 aapt 编译期,扫描汇总资源的文件夹,过滤出不符合要求的图片资源,并抛出异常中断编译
思路二:是思路一的进阶。还是在使用 gradle 在 aapt 编译期,查找有没有合适的 gradle task,提供给我们遍历所有资源的机会
gradle 插件实现
gradle 插件实现的基础
简单对 gradle 插件实现进行复习
插件搭建
新建一个模块
配置好该模块的上传配置(mvn.gradle)
在 build 中,对 gradleApi 进行依赖
scss 复制代码
apply plugin: 'kotlin' //插件如果使用kotlin实现,需要依赖kotlindependencies { implementation gradleApi() implementation localGroovy() implementation 'com.android.tools.build:gradle:3.4.2'}
在 main 下面新建 resources.META-INF.gradle-plugins 文件夹
在该文件夹中创建一个和 module 同名的.properties 文件,在里面配置上你的插件入口类
例:
arduino 复制代码
implementation-class=com.xxx.checkbigimage.image.ImagePlugin
插件的基本实现
上面讲到要配置一个入口类,这个入口类就是实现了 Plugin 接口的类,它有一个 override fun apply(project: Project)方法,就是我们插件开始执行的地方,相当于 main 函数,参数 project 就是整个工程的配置文件
可以使用以下方法,从我们使用插件的地方获取到对插件的配置
Config 是一个 java bean 数据类
"config"是我们在 build 中的配置名称
这样一个简单 gradle 插件就实现了
图片资源检测插件实现
上面说了为什么要实现这样一个插件和该如何实现一个 gradle 插件,那么下面就具体介绍该插件的实现过程
想要的功能
检测和拦截功能
检测是否有大小超标的图片
检测是否有宽高超标的图片
拦截非 webp 资源,并进行提示
自动化压缩
自动压缩 png,jpg 等资源
白名单设置
一些统计功能
实现过程
上面已经说了 gradle 插件的实现,那么我们就从 apply 方法开始说起。
瞄准 task 挂钩
既然是要 hock android 打包的编译过程,那就要寻找 android 打包时,合适的 task
想 hock task,首先应该拿到任务 task 集合
在 android 插件编译生成 apk 的过程中,有好多 task 都可以生成 apk,它们的名字基于 Build Types 和 Product Flavor 生成。那么我们怎么拿到具体生成 apk 的 task 组呢?
为了解决这个问题。android 插件有几个属性,就是我们平常配置的变体(所谓的环境),androd 中有三类变体
applicationVariants(只适用于 app plugin)
libraryVariants(只适用于 library plugin)
testVariants(app、library plugin 均适用)
这三个对象都是实现了 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
因为我们的插件应该可以应用在主工程或者模块包上的,所以当我们插件运行后,我们要检测当前使用我们插件的模块是主工程,还是模块包
找到想要 hock 的任务
我们想 hock 住 android 插件运行的 task 任务,就需要一个重要的 gradle 回调
afterEvaluate 该方法就是整个 gradle 配置文件配置成功后的回调,证明此时配置已检查完毕,所有 task 已经就绪,已经可以开始按指定顺序运行 task 了,那么我就需要在这个回调里办事!
Grade 执行顺序
执行 setting,检测所有 module,为每个模块配置 project
加载 build.properties,生成 task 执行链表和配置
执行某个指定 task,然后会先执行该 task 所依赖的 task
配置完成后,开始遍历 variants 中所有的变体
我们的目标 task:mergeResourcesProvider
mergeResourcesProvider 这个任务就是 android 插件合并所有 module 中资源的 task,看名字就知道了。
我们可以从变体中获取这个 task 对象
那么,我们自己的任务呢?
gradle api 提供给我们可以在代码中生成 task 的方法
使用 project.task("taskname")来生成一个我们自己需要执行的 task
然后我们编写这个 task 的逻辑,也是本插件的逻辑
variant 里面有各种对象,allRawAndroidResources 恰好就是我们需要的。它只有 3.3 以上才会有。
这个 dir 对象,就是 android 所有文件资源的 files 集合
ok。让我们遍历这个文件 list 吧!
如果遇到文件夹,这里是一个递归调用。
如果遇到文件,就可以按照自己的规则处理了。
挂钩 mergeResourcesProvider
我们 task 写好后,需要和 mergeResourcesProvider 挂钩
使 mergeResourcesTask 依赖我们的 mcPicTask,当 mergeResourcesTask 执行前,就会先执行我们的 mcPicTask 了!!
注意:此处直接使用 mergeResourcesTask 系统 task 依赖我们的 task,我们的 task 执行顺序会和 mergeResourcesTask 原有的依赖混杂在一起,不可控。后面讲一种可控的方法
拦截图片的逻辑
这个逻辑应该实现在上面伪代码 process(file:File)方法中
首先我们只需要处理图片,所以对参数 file 进行首轮过滤,只留下后缀名为图片的文件
kotlin 复制代码
fun isImage(file: File): Boolean { return (file.name.endsWith(Const.JPG) || file.name.endsWith(Const.PNG) || file.name.endsWith(Const.JPEG) || file.name.endsWith(Const.GIF) || file.name.endsWith(Const.WEB_P) ) && !file.name.endsWith(Const.DOT_9PNG)}
需要检查图片的宽高的话,可以使用 java 的原生 api
arduino 复制代码
val sourceImg = ImageIO.read(FileInputStream(imgFile))if (sourceImg.height > maxHeight || sourceImg.width > maxWidth) { ...
需要过滤图片大小的话
lua 复制代码
if (imgFile.length() >= maxSize) { LogUtil.log(SIZE_TAG, imgFile.path, true.toString()) return true}
压缩图片逻辑
这里我们只处理普通图片转换为 webp 的压缩。jpg,png 的自压缩原理相同,就不复述了
想压缩转换 webp 图片,需要用到转换工具
google 提供的有一套命令行转换工具:cwebp ,各个平台都有,我们去下载一套,放在我们的主工程文件夹下就可以了
这里需要注意的是:为了方便,如果把 cwebp 命令行程序放在环境变量下,那么执行命令时,拼接命令时,直接拼接 cwebp 就好。
如果使用工程目录下的 cwebp,执行前,需要在 cwebp 命令前面拼接它所在的工程目录。
使用
可以获取工程的根目录
如何执行命令行程序呢?
可以使用 java 的 api
现在可以愉快的转换图片了
转换后,记得把原图删掉
优化点:
有的图片转换后比以前还大,这里需要注意
第一次扫描过后的无法优化的图片,可以存在一个 text 文本当中,第二次执行时,就不要去转换了
系统兼容
在 linux 系统上,创建和删除文件都需要权限,如果没有权限就会失败。这时需要先判断当前的操作系统是不是 linux,如果是,可以执行 chmod 755 -R ${FileUtil.getRootDirPath()}添加权限
这里可以优化一下,在我们的 mcPicTask 前面再加一个 task,用来添加权限,这个 task 只对文件夹进行递归添加就可以了,比一个一个文件要来的快。
因为我们不清楚系统的 task(mergeResourcesTask)都依赖了哪些,那么如何在依赖上再加依赖,如何插入 task 呢?
gradle api 提供给了我们一个方法,xxx.taskDependencies.getDependencies(xxx)可以获取自己的依赖树
在这里就是
让 chmodTask 依赖 mergeResourcesTask 的依赖。假如 mergeResourcesTask 是 A,chmodTask 是 B。A 依赖一个系统的 C。那么上面的代码就是让 B 依赖了 C。这时的 task 图就是 B->C,A->C
接下来我们再把 mcPicTask(简称为 D)也依赖进来
这时就是 D->B->C,A->C
最后,回到我们刚刚拦截图片的逻辑的最后代码
就变成了 A->D->B->C,也就是 mergeResourcesTask->mcPicTask->chmodTask->原依赖 task,依赖和执行顺序是相反的。
正常的代码就是
Tips
直接使用 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 版本差异
我们上面的例子,都是基于比较最新的 gradle 和 android gradle tools 版本(>3.3),android 插件直接提供给了我们 allRawAndroidResources,方便无比,直接在 merge 前遍历它就好了。
那么 3.3 之前的版本呢?就是我们最初的设想了,在合并完各个 module 资源后,扫描 merge 文件夹!这里又有 aapt 和 aapt2 的差异
方法一
关掉 aapt2
在 mergeDebugResources 后,processDebugResources 前扫描文件夹
前面说过,mergeDebugResources 是合并所有 module 的资源文件到固定目录
那么 processDebugResources 是什么呢?就是处理这些已经合并完成的文件,生成R.id,资源索引之类的文件
那么我们的任务就必须插入到 processDebugResources 前面,而不是 mergeDebugResources 了
方法二
仔细翻了翻 MergeResources 里面的方法,有一个 getResSet 和 computeResourceSetList 看起来有点意思。那么 computeResourceSetList 中又调用了 getResSet。最后发现 computeResourceSetList 果然可以获取所有文件列表。
注释也很有意思,有道翻译一下:根据所有输入计算执行期间使用的资源集列表。
鉴于该方法是友元方法,就使用反射获取。
因为 3.3 之后,aapt2 是强制开启的,并且 aapt2 merge 后的文件不是原文件了哦!注意 aapt1 合并后,还是正常的 xxx.png。aapt2 合并后的文件扩展名为 flat
所以,方法一不支持大于 3.3 的 gradle 版本。方法二支持。可以平滑过渡到新版本。鉴于新版本的 gradle 直接提供了 allRawAndroidResources 这样的方法,所以在 3.3 以上,直接使用它就可以了
allRawAndroidResources 和扫描合并文件夹的差异。
allRawAndroidResources 提供的是未合并前的资源路径
源码依赖的 module,编译时,会获取该文件的真实路径
aar 依赖的路径,会获取到 aar-cache 的路径
所以:如果开启自动转换 webp 功能你会发现:你本地源代码中的 png,都转成了 webp
扫描合并文件夹,扫描的是编译期 merge 成功后的文件夹
不会影响源代码
优化
已经扫描过的,且确认无法经过 webp 优化的图片,把这些名称写入一个本地文件,优化扫描速度
未来想做的事情
统计
拦截了多少图片
转换了多少图片
3. 统计各个模块的图片资源情况。在合适的时间进行预警
评论