<返回更多

Android图片资源检测插件实现

2023-08-25  今日头条  IT技术控
加入收藏

为什么要检测图片资源?

  1. 避免不小心把未压缩,不合适的图片资源打入apk中,造成apk过大
  2. 图片打入apk前,可以自动化转换,压缩

实现思路

  1. 思路一:使用gradle在aapt编译期,扫描汇总资源的文件夹,过滤出不符合要求的图片资源,并抛出异常中断编译
  2. 思路二:是思路一的进阶。还是在使用gradle在aapt编译期,查找有没有合适的gradle task,提供给我们遍历所有资源的机会

gradle插件实现

gradle插件实现的基础

简单对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插件,那么下面就具体介绍该插件的实现过程

想要的功能

实现过程

上面已经说了gradle插件的实现,那么我们就从apply方法开始说起。

瞄准task挂钩

既然是要hock android打包的编译过程,那就要寻找android打包时,合适的task

想hock task,首先应该拿到任务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的任务

我们想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 ->    ...  }}

我们的目标task:mergeResourcesProvider

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)}}

如果遇到文件夹,这里是一个递归调用。

如果遇到文件,就可以按照自己的规则处理了。

挂钩mergeResourcesProvider

我们task写好后,需要和mergeResourcesProvider挂钩

less复制代码mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))

使mergeResourcesTask依赖我们的mcPicTask,当mergeResourcesTask执行前,就会先执行我们的mcPicTask了!!

注意:此处直接使用mergeResourcesTask系统task依赖我们的task,我们的task执行顺序会和mergeResourcesTask原有的依赖混杂在一起,不可控。后面讲一种可控的方法

拦截图片的逻辑

这个逻辑应该实现在上面伪代码process(file:File)方法中

  1. 首先我们只需要处理图片,所以对参数file进行首轮过滤,只留下后缀名为图片的文件
  2. kotlin复制代码
  3. 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)}
  4. 需要检查图片的宽高的话,可以使用java的原生api
  5. arduino复制代码
  6. val sourceImg = ImageIO.read(FileInputStream(imgFile))if (sourceImg.height > maxHeight || sourceImg.width > maxWidth) { ...
  7. 需要过滤图片大小的话
  8. lua复制代码
  9. if (imgFile.length() >= maxSize) { LogUtil.log(SIZE_TAG, imgFile.path, true.toString()) return true}

压缩图片逻辑

这里我们只处理普通图片转换为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))

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

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和扫描合并文件夹的差异。

allRawAndroidResources提供的是未合并前的资源路径

扫描合并文件夹,扫描的是编译期merge成功后的文件夹

优化

  1. 已经扫描过的,且确认无法经过webp优化的图片,把这些名称写入一个本地文件,优化扫描速度

未来想做的事情

统计

  1. 拦截了多少图片
  2. 转换了多少图片
  3. 3. 统计各个模块的图片资源情况。在合适的时间进行预警
关键词:Android      点击(9)
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多Android相关>>>