/ImageDeflated

android 图片 瘦身 插件,支持 图片压缩,并自动转为 webp

Primary LanguageJavaApache License 2.0Apache-2.0

目前用在享耀云商 这个项目中有问题,只有一点点 图片 转了 webp,fuck 得看看

ImageDeflated

android 图片 瘦身 插件,支持 图片压缩,并自动转为 webp

注意

  • 目前只测试了 gradle 插件版本为 3.6.3 的情况,其他版本不保证能成功运行
  • gradle.propertiy 中 不能配置
org.gradle.parallel=true
org.gradle.caching=true

功能点记录

  • 用 tiny 压缩再 转webp 可以更大限度的 减少图片体积
  • 忽略.9 文件
  • 默认 图片超过 10kb 进行 tiny压缩,超过1kb 转webp,所以会发现 APk 里并不是所有都是 webp格式
  • 动态依赖 来优雅的处理 cwebp.exe 这个依赖
  • whiteList 支持 通配符 ? + *
?	Zero or one character
*	Zero or more of character
+	One or more of character

使用

  1. 根目录下build.gradle 中添加依赖
buildscript {
    repositories {
        maven { url 'https://jitpack.io' }
        ...
    }
    dependencies {
        classpath 'com.github.NiLuogege:ImageDeflated:1.0.1'
        ...
    }
}
  1. app的build.gradle 中引用插件并进行配置
apply plugin: 'ImageDeflated'

...


imageDeflated {
    tiny {
        open = true
        key = "xxx填入自己的keyxxx"
        threshold = 1024 * 11
        whiteList = [
                "ic_launcher.png",
                "ic_launcher_round.png",
                "white*.png",
        ]
    }

    webp {
//        artifact = "com.niluogege.tools:cwebp:1.2.0" 
        path = "E:\\libwebp-1.2.0\\bin\\cwebp.exe" 
        open = true
        quality = 80
        whiteList = [
                "ic_launcher.png",
                "ic_launcher_round.png",
                "logo*.png",
        ]
    }
}

需要注意下:
    - tiny 的 key需要自己去[官网](https://tinypng.com/developers)申请。申请后直接替换即可。
    - cwebp 下载地址 https://storage.googleapis.com/downloads.webmproject.org/releases/webp/index.html 我使用的是 `libwebp-1.2.0-windows-x64`
  1. 继承完成 运行 assembleDebug 看效果 运行完成后会在 app/build/imageDeflated 下输出 record.md 用来记录这次图片的压缩情况。

待做功能

  • 处理 assert 目录下的 图片资源 (flutter 的图片资源会 打包到这里)
  • 自动区分那些图片 不适合转为 webp(或者控制转为webp的质量??)
  • 无须tiny key ,无限量 tiny
  • 目前 无法处理 aar 中的图片,看有没有什么方案可以做

changeLog

  • v1.0.0 完成 图片 自动压缩和 自动转webp

学到的

1. merge***Resources Task 中 会使用 aapt2 将资源(图片,布局(xml),values) 解析为一个扩展名为 .flat 的中间二进制文件。具体为
  • 位于 res/values/ 目录下 的 XML 资源文件(如 String 和 Style),会被解析为 *.arsc.flat 作为扩展名的资源表
  • 除 res/values/ 目录下的文件以外的其他所有文件都将转换为扩展名为 *.flat 的二进制 XML 文件
  • 所有 PNG 文件都会被压缩,并采用 *.png.flat 扩展名。如果选择不压缩 PNG,您可以在编译期间使用 --no-crunch 选项。

然后还要注意 AAPT2 输出的文件不是可执行文件,后面在链接阶段会使用这些二进制文件作为输入来生成 APK, 不是可执行文件的意思就是说,图片已经不是真正的图片了,已经不能直接通过图片被打开了

2. hook MergeResources 的 getConfiguredResourceSets() 方法 获取 输入资源路径

    private static void createTask(Project project, variant) {
        def variantName = variant.name.capitalize()
        def taskName = "imageDeflate$variantName"
        if (project.tasks.findByName(taskName) == null) {
            def imageDeflatedTask = project.tasks.create(taskName, ImageDeflatedTask)
            MergeResources mergeResourcesTask = variant.mergeResourcesProvider.get()
            mergeResourcesTask.dependsOn(imageDeflatedTask)


            String outputDirPath = mergeResourcesTask.getOutputDir().getAsFile().get().getAbsolutePath()
            String generatedPngsOutputDir = mergeResourcesTask.getGeneratedPngsOutputDir().getAbsolutePath()

            println("mergeResourcesTask=${mergeResourcesTask.getName()} \n" +
                    "outputDir=${outputDirPath} \n" +
                    "generatedPngsOutputDir=${generatedPngsOutputDir} \n" +
                    "mergedNotCompiledResourcesOutputDirectory=${mergeResourcesTask.getMergedNotCompiledResourcesOutputDirectory().toString()} \n" +
//                    "mergedNotCompiledResourcesOutputDirectory=${mergeResourcesTask.getMergedNotCompiledResourcesOutputDirectory().getAsFile().get().getAbsolutePath()} \n" +
                    "")

            mergeResourcesTask.doFirst {
                println("doFirst")

                Class clazz = mergeResourcesTask.getClass()

                Method getPreprocessor = ReflectUtil.getDeclaredMethodRecursive(clazz, "getPreprocessor")
                Method getConfiguredResourceSets = ReflectUtil.getDeclaredMethodRecursive(clazz, "getConfiguredResourceSets", ResourcePreprocessor.class)

                ResourcePreprocessor preprocessor = (ResourcePreprocessor) getPreprocessor.invoke(mergeResourcesTask);
                List<ResourceSet> resourceSets = (List<ResourceSet>) getConfiguredResourceSets.invoke(mergeResourcesTask, preprocessor);

                for (ResourceSet resourceSet : resourceSets) {
                    System.out.println("rs= " + resourceSet.toString());
                }

//                Deflateder.deflate(outputDirPath, generatedPngsOutputDir, mergeResourcesTask)
            }

            mergeResourcesTask.doLast {
                println("doLast")
            }
        }
        
        //输出
        rs= GeneratedResourceSet{androidx.core:core:1.3.2$Generated, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\150ab35489a9920dd53304c7947ce1b1\core-1.3.2\res]}
        rs= ResourceSet{androidx.core:core:1.3.2, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\150ab35489a9920dd53304c7947ce1b1\core-1.3.2\res]}
        rs= GeneratedResourceSet{androidx.appcompat:appcompat-resources:1.2.0$Generated, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\e2e828aeb528ae4b9bb5f5f252ab3c3b\jetified-appcompat-resources-1.2.0\res]}
        rs= ResourceSet{androidx.appcompat:appcompat-resources:1.2.0, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\e2e828aeb528ae4b9bb5f5f252ab3c3b\jetified-appcompat-resources-1.2.0\res]}
        rs= GeneratedResourceSet{androidx.appcompat:appcompat:1.2.0$Generated, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\df20a55592f948e0f00115dd862e269d\appcompat-1.2.0\res]}
        rs= ResourceSet{androidx.appcompat:appcompat:1.2.0, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\df20a55592f948e0f00115dd862e269d\appcompat-1.2.0\res]}
        rs= GeneratedResourceSet{androidx.constraintlayout:constraintlayout:2.0.4$Generated, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\10464ef207cad4d8a3ea58f3282aaf3d\constraintlayout-2.0.4\res]}
        rs= ResourceSet{androidx.constraintlayout:constraintlayout:2.0.4, sources=[D:\softCacheData\.gradle\caches\transforms-2\files-2.1\10464ef207cad4d8a3ea58f3282aaf3d\constraintlayout-2.0.4\res]}
        rs= GeneratedResourceSet{main$Generated, sources=[E:\111work\code\code_me\myGitHub\ImageDeflated\app\src\main\res, E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\rs\debug, E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\resValues\debug]}
        rs= ResourceSet{main, sources=[E:\111work\code\code_me\myGitHub\ImageDeflated\app\src\main\res, E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\rs\debug, E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\resValues\debug]}
        rs= GeneratedResourceSet{debug$Generated, sources=[E:\111work\code\code_me\myGitHub\ImageDeflated\app\src\debug\res]}
        rs= ResourceSet{debug, sources=[E:\111work\code\code_me\myGitHub\ImageDeflated\app\src\debug\res]}




我们可以 压缩完原有图片后 再修改 ResourceSet 中的路径 为压缩文件的路径 这样就 完成了hook

3. 反射task 的时候 需要在 gradle 中,不能再 java中,因为拿不到具体的类名
4. 名为 main$Generated 的 GeneratedResourceSet 中有一条路径会 指向 主工程的 main/res ,而 名为 main ResourceSet 也有指向 main\res

我现在的做法是 重复处理 如下:

GeneratedResourceSet{main$Generated, 
sources=[E:\111work\code\code_me\myGitHub\ImageDeflated\app\src\main\res, 
E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\rs\debug, 
E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\resValues\debug]}, 
ResourceSet{main, 
sources=[E:\111work\code\code_me\myGitHub\ImageDeflated\app\src\main\res, 
E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\rs\debug, 
E:\111work\code\code_me\myGitHub\ImageDeflated\app\build\generated\res\resValues\debug]}
5. png 转 webp 后 图片竟然变大了?

原因是Webp并不支持灰度图带上透明通道这种类型,带上透明通道就将格式固定成了RGBA格式。因此导致了要保存的数据变大。 参考:https://developer.aliyun.com/article/74634 目前的解决方案是将这类图片 添加到 白名单中即可。

参考