介绍
McImage插件传送门:github.com/Aweme/McIma… McImage是一个对资源中的png和jpg图片进行压缩和图片大小检查的插件,通过pngquant算法对图片资源进行压缩,并且可以设置图片最大大小来在打包时候对所有图片资源进行检查。如果发现大于设置的大小,默认为1M,那么就会中断打包,并且提示是哪张图
与tinypng的比较
McImage插件的优势:
- tinypng是商业api,对图片压缩有限额
- github上的主流tinypng的插件并没有压缩项目中依赖的图,仅仅压缩了你项目中自己的图
- McImage可以对一个Module中所有的依赖进行处理,包括jar中的图和aar中的图
- 采用的算法pngquant仅仅是McImage的一个插件,可以随时替换算法,如果出现更加好用的算法,McImage会及时更换。主流的tinypng插件仅仅适用于tinypng
McImage插件的劣势:
- tinypng采用自研算法,压缩效率高于开源的pngquant算法,大约百分之8左右的压缩度
McImage插件对全量依赖的压缩可以尽可能的减少着百分之8左右的压缩带来的劣势
如何在打release时使用
可以在任意module的build.gradle中加入下列代码,即可在打release时才进行检查,使用默认参数。
def isRelease() {
project.gradle.startParameter.taskNames.each { arg ->
if ((arg == "assembleRelease" || arg == "aR") || arg == "ar" || arg == "resguardRelease") {
return true
}
}
return false
}
if (isRelease()) {
apply plugin: 'McImage'
}
实现原理
首先可以看看我的其他博客 Android自定义插件 来学习,如何实现一个Gradle插件
Gradle的打包流程
这是一张Android的整个打包流程图片。我们主要关注红色方框区域,这个区域做的事情是两个步骤
- 首先把一个Module的所有依赖的Mainfest,asset,resources分类合并
- 将合并好的放到指定文件夹,然后传给aapt进行打包
Hook打包流程
我们知道Android的打包使用的Gradle来进行的,上图演示了Gradle的打包流程,所有的流程都有对应的Task,这里在众多的Task中我们主要关注两个Task
mergeDebugResources
- 解压所有的aar包输出到app/build/intermediates/exploded-aar
- 把所有的资源文件合并到app/build/intermediates/res/merged/debug目录里
processDebugResources
- 调用aapt生成项目和所有aar依赖的R.java,输出到app/build/generated/source/r/debug目录
- 生成资源索引文件app/build/intermediates/res/resources-debug.ap_
- 把符号表输出到app/build/intermediates/symbols/debug/R.txt
上述两个流程就是红框圈出来的位置的两个重要的task。
那么我们应该hook哪个?首先我们看到第一个他做的事情总结到一点就是合并,第二个Task做的事情总结到一点就是处理合并。
当mergeDebugResources处理完成之后,相关文件会产生在下列目录中
可以看到res中是我们的打包好的res文件,我们处理这个目录即可。 processDebugResources稍后会使用这个目录下的文件生成相关的map,所以我们在processDebugResources执行的前一步插入我们的插件就比较恰当。
源码解析
首先整体目录
- CompressUtil:使用压缩库进行压缩的工具类
- Config:在Gradle中的配置Model
- ImagePlugin:插件主工程
- SizeUtil:检查图片大小的工具类
Config
class Config {
def maxSize = 1 * 1024 * 1024
def isCheck = true
def isCompress = true
}
首先Model中分别配置了检查文件的大小,是否检查和是否压缩,如果isCheck置为false那么将不进行检查,isCompress置为false将不进行压缩
CompressUtil
关键代码
def command = "${rootDir.getParentFile().getPath() + '/mctools/'}pngquant --skip-if-larger --speed 11 --force --output ${imgFile.getPath()} -- ${imgFile.getPath()}"
def proc = command.execute()
proc.waitFor()
通过groovy调用shell操纵pngquant的二进制库,来进行图片压缩,执行输入输出。
SizeUtil
if ((imgFile.getName().endsWith('.jpg') || imgFile.getName().endsWith('.png'))
&& !imgFile.getName().contains('.9')) {
println 'Start Check ' + imgFile.getPath()
if (imgFile.length() >= maxSize) {
return true
}
}
进行一步判断然后根据输入的文件大小来检查返回true或者false
ImagePlugin
此处是插件的入口,首先进行了一些安全检查,包括读取配置,检查渠道,读取merge目录。然后对查到的目录进行递归检查资源
//load img
dir.eachDir() { channelDir ->
channelDir.eachDir { drawDir->
def file = new File("${drawDir}")
if (file.name.contains('drawable') || file.name.contains('mipmap')) {
file.eachFile { imgFile ->
if (config.isCheck && SizeUtil.checkImgSize(imgFile, config.maxSize)) {
bigImgList.add(file.getPath() + file.getName())
}
if (config.isCompress) {
CompressUtil.compressImg(imgFile, project.projectDir)
}
}
}
}
}
插入自定义插件
project.tasks.findByName(mcPicPlugin).dependsOn processResourceTask.taskDependencies.getDependencies(processResourceTask)
processResourceTask.dependsOn project.tasks.findByName(mcPicPlugin)
原理就是更改processResources任务的依赖指针,跟链表插入节点的原理一致。
整体梳理
整体流程并不难,找到关键的Gradle Task位置,然后插入我们的task,接着对资源进行压缩和检查,即可完成所有流程。
扩展
可以自行修改CompressUtil更换压缩算法,曾经使用Google的无损压缩Guetzli和zopfli进行对jpg和png的处理,此两种算法都是无损压缩。pngquant是有损压缩,不过损失度在可接受范围内。google两个算法的劣势是压缩时间比较长,我用生产环境项目测试大概花费半个小时,而且压缩率只能到百分之30,所以权衡之后换成pngquant。