【小木箱成长营】包体积优化系列教程:
前言
大家都知道 png 是比较占用App体积的,有没有工具可以在打包前(比如 assembleDebug、assembleRelease)自动去转化所有的 png 图片,包括第三方依赖库里面的呢?之前不经意间发现有一个神器cwebp 转化工具,是不是可以借鉴这种工具自己写个Plugin完成图片转换,同时支持检查大图片,图片大小可配置。话不多说,说干就干~
编写插件前,需要思考几个业务痛点
- 怎么拿到所有的 res 资源呢?
- 自动化转换工具Task 的执行时机点?
- 如何检查大图片,并配置图片大小,自动化开启图片转换开关?
鉴于问题1,我们可以参考McImage, 其实也很简单,就是一个 Gradle API,看链接文档的文档即可
鉴于问题2,该 Task 的执行时机其实是依赖于 MergeResources Task
鉴于问题3, 我们可以通过 Gradle API 自定义API 设置开关,图片最大体积,给图片添加白名单
convert2WebpConfig{ enableWhenDebug true maxSize 1024*1024 // 1M whiteList ["xxx.png","xxx.png"] //... }
图片格式转换开发流程
第一步: 新建Gradle Plugin 工程
第二步: 添加Png转Webp配置
第三步: 针对com.android.application和com.android.library配置Plugin
实现自定义属性图片转换器开关配置,图片最大体积配置,图片添加白名单配置
第五步: 将mac版本和windows版本图片转换工具移到 tool/cwebp 目录下,并添加可执行程序
第六步 添加 auto.service 方便在编译期间动态添加依赖
kapt "com.google.auto.service:auto-service:1.0-rc4" implementation "com.google.auto.service:auto-service:1.0-rc4" compileOnly "com.android.tools.build:gradle:4.0.1" testCompileOnly "com.android.tools.build:gradle:4.0.1"
利用AutoService注解,使用的反射去实例化对象 VariantProcessor
,动态注册Convert2WebpTask任务,后期使用该注解处理器处理Convert2WebpTask任务
@AutoService(VariantProcessor::class) class Convert2WebpVariantProcessor : VariantProcessor { override fun process(variant: BaseVariant) { val variantData = (variant as ApplicationVariantImpl).variantData val tasks = variantData.scope.globalScope.project.tasks val convert2WebpTask = tasks.findByName("convert2Webp") ?: tasks.create( "convert2Webp", Convert2WebpTask::class.java ) val mergeResourcesTask = variant.mergeResourcesProvider.get() mergeResourcesTask.dependsOn(convert2WebpTask) } }
第七步 Convert2WebpTask任务 执行
7.1 检查tools 路径下是否有 webp工具
7.2 如果配置属性配置开关为false,中断任务
if (!config.enableWhenDebug) { return@all }
7.3 拿到所有的Android资源文件,遍历资源文件,将满足条件的大图添加到大图列表
val dir = variant.allRawAndroidResources.files /** * 遍历资源文件目录 */ for (channelDir in dir) { traverseResDir(channelDir, imageFileList, cacheList, object : IBigImage { override fun onBigImage(file: File) { bigImageList.add(file.absolutePath) } }) } private fun traverseResDir( file: File, imageFileList: ArrayList<File>, cacheList: ArrayList<String>, iBigImage: IBigImage ) { if (cacheList.contains(file.absolutePath)) { return } else { cacheList.add(file.absolutePath) } if (file.isDirectory) { file.listFiles()?.forEach { if (it.isDirectory) { traverseResDir(it, imageFileList, cacheList, iBigImage) } else { filterImage(it, imageFileList, iBigImage) } } } else { filterImage(file, imageFileList, iBigImage) } }
7.4 过滤不合规的图片文件
- 如果添加了图片白名单或者文件不是图片格式,过滤
- 如果图片尺寸合规,并且图片是大图,大图白名单没有图片,添加到大图列表
- 否则添加到图片目录
/** * 过滤图片 */ private fun filterImage(file: File, imageFileList: ArrayList<File>, iBigImage: IBigImage) { // 如果添加了图片白名单或者文件不是图片格式,过滤 if (config.whiteList.contains(file.name) || !ImageUtil.isImage(file)) { return } // 如果图片尺寸合规,并且图片是大图,大图白名单没有图片 if ((config.isCheckSize && ImageUtil.isBigSizeImage(file, config.maxSize)) && !config.bigImageWhiteList.contains(file.name) ) { // 添加到大图列表 iBigImage.onBigImage(file) } // 将图片添加到图片图片目录 imageFileList.add(file) }
7.5 检查大图,并且将图片引用找出来
private fun checkBigImage() { if (bigImageList.size != 0) { val stringBuffer = StringBuffer("Big Image Detector! ") .append("ImageSize can't over ${config.maxSize / 1024}kb.\n") .append("To fix this exception, you can increase maxSize or config them in bigImageWhiteList\n") .append("Big Image List: \n") for (fileName in bigImageList) { stringBuffer.append(fileName) stringBuffer.append("\n") } throw GradleException(stringBuffer.toString()) } }
7.6 处理图片压缩任务
private fun dispatchOptimizeTask(imageFileList: java.util.ArrayList<File>) { if (imageFileList.size == 0 || bigImageList.isNotEmpty()) { return } val coreNum = Runtime.getRuntime().availableProcessors() if (imageFileList.size < coreNum) { for (file in imageFileList) { optimizeImage(file) } } else { val results = ArrayList<Future<Unit>>() val pool = Executors.newFixedThreadPool(coreNum) val part = imageFileList.size / coreNum for (i in 0 until coreNum) { val from = i * part val to = if (i == coreNum - 1) imageFileList.size - 1 else (i + 1) * part - 1 results.add(pool.submit(Callable<Unit> { for (index in from..to) { optimizeImage(imageFileList[index]) } })) } for (f in results) { try { f.get() } catch (e: Exception) { println("EHiPlugin Convert2WebpTask#dispatchOptimizeTask() execute wrong.") } } } } /** * 压缩图片 */ private fun optimizeImage(file: File) { val path: String = file.path if (File(path).exists()) { oldSize += File(path).length() } ImageUtil.convert2Webp(file) calcNewSize(path) }
7.7 计算压缩前后图片大小,以及压缩耗时时间
private fun optimizeImage(file: File) { val path: String = file.path if (File(path).exists()) { oldSize += File(path).length() } ImageUtil.convert2Webp(file) calcNewSize(path) } dispatchOptimizeTask(imageFileList) println("Before optimize Size: ${oldSize / 1024}kb") println("After optimize Size: ${newSize / 1024}kb") println("Optimize Size: ${(oldSize - newSize) / 1024}kb") println("CostTotalTime: ${System.currentTimeMillis() - startTime}ms") println("------------------------------------")
总结
本文主要是在打包前对App做了一次图片全量替换,图片转换方式借助的是Google开源工具cwebp,当然我们可以通过白名单方式规范图片尺寸大小和插件开关,如果你掌握本文内容,不仅会对你们公司应用瘦身有所帮助,同时也能弥补你对 Gradle Plugin 知识的渴望~