如何将Booster中的图片压缩任务 移植到AGP8.0中?

409 阅读3分钟

AGP8.0+ 如何 处理图片压缩任务

上一篇文章中,我们知道如何寻找mergeRes的任务hook点 这篇文章就基于上篇文章的结论 来做一下 编译期压缩图片的任务

有了doLast的回调函数,那我们其实要做的就是在doLast回调中完成我们的图片压缩任务即可

编译期处理图片压缩任务的流程

总体来说还是比较简单的, 找到mergeRes 任务的文件夹,然后遍历该文件夹下的flat 文件, 利用aapt2 可执行文件 调用命令 把flat文件 还原成 原始的图片文件, 对原始的图片文件进行webp压缩 压缩结束以后 再利用aapt2 重新把webp 文件 给还原成flat 文件即可

这里面大部分的代码其实booster中都有,大家只要耐心移植即可,唯一的几个注意点 我们单独拿出来说

webp压缩的可执行文件呢?

原有的 booster的处理流程中 是有一个installWebp的任务的,这个任务其实就是将 webp的可执行文件在任务执行期 从 task本身的代码路径中 拷贝到 build目录下

对于我们来说 如果你不是对外提供sdk的话,仅仅是为了移植到agp8.0+中 其实完全没有这么复杂,你直接将这哥webp的压缩执行文件 拷贝到你项目目录下 即可

image.png

在实际执行的时候 主要关注下 判断当前执行环境是哪个操作系统即可

这里booster代码中有现成的,自行参考

object OS {

    val name: String = System.getProperty("os.name", "").lowercase()

    val arch: String = System.getProperty("os.arch", "").lowercase()

    val uname = try {
        "uname -a".execute().stdout.trim().lowercase()
    } catch (e: Throwable) {
        arch
    }

    val version = object : Comparable<String> {

        private val version = System.getProperty("os.version", "").lowercase()

        override fun compareTo(other: String): Int {
            val part1 = version.split("[\._\-]".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
            val part2 = other.split("[\._\-]".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()

            var idx = 0
            while (idx < part1.size && idx < part2.size) {
                val p1 = part1[idx]
                val p2 = part2[idx]
                val cmp = if (p1.matches("\d+".toRegex()) && p2.matches("\d+".toRegex())) {
                    p1.toInt().compareTo(p2.toInt())
                } else {
                    part1[idx].compareTo(part2[idx])
                }

                if (cmp != 0) {
                    return cmp
                }
                ++idx
            }

            if (part1.size == part2.size) {
                return 0
            }

            val left = part1.size > idx
            val parts = if (left) part1 else part2
            while (idx < parts.size) {
                val p = parts[idx]
                val cmp = if (p.matches("\d+".toRegex())) {
                    p.toInt().compareTo(0)
                } else {
                    1
                }

                if (cmp != 0) {
                    return if (left) cmp else -cmp
                }
                ++idx
            }

            return 0
        }

        override fun toString() = this.version
    }

    val executableSuffix = if (isWindows()) ".exe" else ""

    fun isLinux() = name.startsWith("linux", true)

    fun isMac() = name.startsWith("mac")

    fun isWindows() = name.startsWith("windows")

}

如何寻找aapt2 文件

这一步稍微复杂一点,这里我们参考 polyfill的 方案来实现

fun Variant.getBuildToolInfo(project: Project): Provider<BuildToolInfo> {
    val plugin = when (this) {
        is ApplicationVariant -> {
            project.plugins.getPlugin(com.android.build.gradle.internal.plugins.AppPlugin::class.java)
        }

        is LibraryVariant -> {
            project.plugins.getPlugin(com.android.build.gradle.internal.plugins.LibraryPlugin::class.java)
        }

        else -> {
            throw UnsupportedOperationException("Can not find corresponding plugin associated to $this.")
        }
    }
    val sdkLoaderServiceLazy = ReflectionKit.getField(
        BasePlugin::class.java,
        plugin, "versionedSdkLoaderService$delegate"
    ) as Lazy<VersionedSdkLoaderService>
    return sdkLoaderServiceLazy.value.versionedSdkLoader.get().buildToolInfoProvider
}
object ReflectionKit {

    fun <T> getField(clazz: Class<T>, instance: T, fieldName: String): Any {
        val field = clazz.declaredFields.first { it.name == fieldName }
        field.isAccessible = true
        return field.get(instance) as Any
    }

}

然后我们定义一个 map,key是 varintName,value就是 aapt2的位置

val buildTools = mutableMapOf<String,String>()
androidComponents.onVariants { variant ->

    val buildToolInfo = variant.getBuildToolInfo(target).get()
    buildTools[variant.name] = buildToolInfo.getPath(BuildToolInfo.PathId.AAPT2)
}

这里因为要使用到aapt2相关的引用 我们需要额外引入2个依赖

implementation("com.didiglobal.booster:booster-aapt2:5.1.0")
compileOnly("com.android.tools:sdklib:31.8.0")

到这一步 其实我们最关键的地方就全部完成了

收尾工作

我们现在有了 aapt2的路径,webp的路径也有了,其他的工作就比较简单了。

考虑到我们需要将flat文件 还原成png 然后再压缩成webp ,为了不污染原有的mergesRes目录,我们需要独立设置一个 缓存目录, 这样我们可以将flat中的png 图片 还原到 这个缓存目录下,然后压缩的过程在这个缓存目录下 如果压缩后的webp 大于 png ,那么就再写回 mergeRes中的flat文件,否则 就不处理

project.buildDir.file("intermediates")
    .file("compressed_res_cwebp", varintKey, "compressWebpCacheDir")

遍历flat文件的时候注意了 我们只处理图片文件

fun isImage(file: File): Boolean {
    return (file.name.endsWith(Constants.JPG) ||
            file.name.endsWith(Constants.PNG) ||
            file.name.endsWith(Constants.JPEG)
            ) && !file.name.endsWith(Constants.DOT_9PNG)
}

flat文件中提取图片原始文件

fun File.getImgFileFormFlat(target: File):File {
    BinaryParser(this).use { container ->
        val flatHeader = container.parseHeader()
        val type = container.readInt()
        val length = container.readLong()
        val headerSize = container.let {
            val size = it.readInt()
            val p = it.tell()
            val imgDataSizeByteArray = it.readBytes(8)
            it.seek(p)
            size
        }
        val dataSize = container.readLong()
        val imgHeader = container.readBytes(headerSize)
        val headerPadding = container.readBytes((4 - container.tell() % 4) % 4)
        val imgArray = container.readBytes(dataSize.toInt())
        target.writeBytes(imgArray)
        return target
    }
}