为什么要检测图片资源?

  1. 避免不小心把未紧缩,不适宜的图片资源打入apk中,造成apk过大

  2. 图片打入apk前,能够主动化转化,紧缩

完结思路

  1. 思路一:运用gradle在aapt编译期,扫描汇总资源的文件夹,过滤出不符合要求的图片资源,并抛出异常中断编译

  2. 思路二:是思路一的进阶。仍是在运用gradle在aapt编译期,查找有没有适宜的gradle task,供给给咱们遍历一切资源的机会

gradle插件完结

gradle插件完结的根底

简单对gradle插件完结进行温习

插件建立

  • 新建一个模块

  • 装备好该模块的上传装备(mvn.gradle)

  • 在build中,对gradleApi进行依靠

    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文件,在里边装备上你的插件入口类

    例:

    implementation-class=com.xxx.checkbigimage.image.ImagePlugin
    

插件的根本完结

上面讲到要装备一个入口类,这个入口类便是完结了Plugin接口的类,它有一个override fun apply(project: Project)办法,便是咱们插件开端履行的当地,相当于main函数,参数project便是整个工程的装备文件

能够运用以下办法,从咱们运用插件的当地获取到对插件的装备

project.extensions.create("config", Config::class.java)mConfig = project.property("config") as Config

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

由于咱们的插件应该能够应用在主工程或许模块包上的,所以当咱们插件运转后,咱们要检测当前运用咱们插件的模块是主工程,仍是模块包

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回调

project.afterEvaluate{...}

afterEvaluate该办法便是整个gradle装备文件装备成功后的回调,证明此刻装备已查看完毕,一切task已经就绪,已经能够开端按指定次序运转task了,那么我就需求在这个回调里就事!

Grade 履行次序

  1. 履行setting,检测一切module,为每个模块装备project

  2. 加载build.properties,生成task履行链表和装备

  3. 履行某个指定task,然后会先履行该task所依靠的task

装备完结后,开端遍历variants中一切的变体

project.afterEvaluate {  variants.all { variant ->    ...  }}
咱们的方针task:mergeResourcesProvider

mergeResourcesProvider这个使命便是android插件兼并一切module中资源的task,看姓名就知道了。

咱们能够从变体中获取这个task方针

val mergeResourcesTask = variant.mergeResourcesProvider.get()

那么,咱们自己的使命呢?

gradle api供给给咱们能够在代码中生成task的办法

val mcPicTask = project.task("CheckBigImage${variant.name.capitalize()}")

运用project.task(“taskname”)来生成一个咱们自己需求履行的task

然后咱们编写这个task的逻辑,也是本插件的逻辑

mcPicTask.doLast {...}

variant里边有各种方针,allRawAndroidResources刚好便是咱们需求的。它只要3.3以上才会有。

val dir = variant.allRawAndroidResources.files

这个dir方针,便是android一切文件资源的files集合

ok。让咱们遍历这个文件list吧!

for (channelDir: File in dir) {check(channelDir)}fun check(file: File) { if(file.isDirectory) {   check(file)} else {   process(file)}}

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

假如遇到文件,就能够按照自己的规则处理了。

挂钩mergeResourcesProvider

咱们task写好后,需求和mergeResourcesProvider挂钩

mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))

使mergeResourcesTask依靠咱们的mcPicTask,当mergeResourcesTask履行前,就会先履行咱们的mcPicTask了!!

留意:此处直接运用mergeResourcesTask体系task依靠咱们的task,咱们的task履行次序会和mergeResourcesTask原有的依靠稠浊在一起,不可控。后面讲一种可控的办法

阻拦图片的逻辑

这个逻辑应该完结在上面伪代码process(file:File)办法中

  1. 首要咱们只需求处理图片,所以对参数file进行首轮过滤,只留下后缀名为图片的文件

    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)}
    
  2. 需求查看图片的宽高的话,能够运用java的原生api

    val sourceImg = ImageIO.read(FileInputStream(imgFile))if (sourceImg.height > maxHeight || sourceImg.width > maxWidth) {  ...
    
  3. 需求过滤图片巨细的话

    if (imgFile.length() >= maxSize) {   LogUtil.log(SIZE_TAG, imgFile.path, true.toString())   return true}
    

紧缩图片逻辑

这儿咱们只处理一般图片转化为webp的紧缩。jpg,png的自紧缩原理相同,就不复述了

想紧缩转化webp图片,需求用到转化东西

google供给的有一套指令行转化东西:cwebp ,各个渠道都有,咱们去下载一套,放在咱们的主工程文件夹下就能够了

这儿需求留意的是:为了方便,假如把cwebp指令行程序放在环境变量下,那么履行指令时,拼接指令时,直接拼接cwebp就好。

假如运用工程目录下的cwebp,履行前,需求在cwebp指令前面拼接它地点的工程目录。

运用

project.rootDir.path

能够获取工程的根目录

怎么履行指令行程序呢?

能够运用java的api

Runtime.getRuntime().exec(cmd)

现在能够愉快的转化图片了

Tools.cmd("cwebp", "${imgFile.path} -o ${webpFile.path} -m 6 -quiet")

转化后,记得把原图删掉

优化点:

  1. 有的图片转化后比以前还大,这儿需求留意

  2. 第一次扫描往后的无法优化的图片,能够存在一个text文本傍边,第二次履行时,就不要去转化了

体系兼容

在linux体系上,创立和删除文件都需求权限,假如没有权限就会失败。这时需求先判断当前的操作体系是不是linux,假如是,能够履行chmod 755 -R ${FileUtil.getRootDirPath()}增加权限

这儿能够优化一下,在咱们的mcPicTask前面再加一个task,用来增加权限,这个task只对文件夹进行递归增加就能够了,比一个一个文件要来的快。

由于咱们不清楚体系的task(mergeResourcesTask)都依靠了哪些,那么怎么在依靠上再加依靠,怎么插入task呢?

gradle api供给给了咱们一个办法,xxx.taskDependencies.getDependencies(xxx)能够获取自己的依靠树

在这儿便是

(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)也依靠进来

(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)

这时便是D->B->C,A->C

最终,回到咱们刚刚阻拦图片的逻辑的最终代码

mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))

就变成了A->D->B->C,也便是mergeResourcesTask->mcPicTask->chmodTask->原依靠task,依靠和履行次序是相反的。

正常的代码便是

(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

android.enableAapt2=false

mergeDebugResources后,processDebugResources前扫描文件夹

前面说过,mergeDebugResources是兼并一切module的资源文件到固定目录

那么processDebugResources是什么呢?便是处理这些已经兼并完结的文件,生成R.id,资源索引之类的文件

那么咱们的使命就必须插入到processDebugResources前面,而不是mergeDebugResources

办法二

仔细翻了翻MergeResources里边的办法,有一个getResSet和computeResourceSetList看起来有点意思。那么computeResourceSetList中又调用了getResSet。最终发现computeResourceSetList公然能够获取一切文件列表。

/*** 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供给的是未兼并前的资源途径

  • 源码依靠的module,编译时,会获取该文件的真实途径

  • aar依靠的途径,会获取到aar-cache的途径

  • 所以:假如开启主动转化webp功用你会发现:你本地源代码中的png,都转成了webp

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

  • 不会影响源代码

优化

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

未来想做的事情

计算

  1. 阻拦了多少图片

  2. 转化了多少图片

    3. 计算各个模块的图片资源情况。在适宜的时间进行预警