2024年Android 包体积资源优化实践_android pngquant(2),扫地阿姨看完都学会了

最后

有任何问题,欢迎广大网友一起来交流,分享高阶Android学习视频资料和面试资料包~

偷偷说一句:群里高手如云,欢迎大家加群和大佬们一起交流讨论啊!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

                "pngquant",
                "--skip-if-larger --speed 1 --nofs --strip --force  --quality=75  ${imgFile.path} --output $tempFilePath"
        )
    }
    val oldSize = imgFile.length()
    val tempFile = File(tempFilePath)
    val newSize = tempFile.length()
    return if (newSize in 1 until oldSize) {
        val imgFileName: String = imgFile.path
        if (imgFile.exists()) {
            imgFile.delete()
        }
        tempFile.renameTo(File(imgFileName))
        oldSize - newSize
    } else {
        if (tempFile.exists()) {
            tempFile.delete()
        }
        0L
    }
}
return 0

}


图片的压缩收益最大,且实施简单,风险最低,是资源优化的首选。


**1.2.2 Assets图片压缩**


Assets 图片压缩的处理方式与 res 下差不多,区别仅仅在于挂载的 task 与 压缩模式不同,Assets 下单资源由于是通过 AssetsManager 按照名称获取的,且使用场景不可控,无法明确感知业务使用对格式是否有要求的前提下,同格式压缩是相对稳妥的方案。



val mergeAssets = project.tasks.getByName(“mergeKaTeX parse error: Expected '}', got 'EOF' at end of input: …ompress ignore:originalPath”)
}
!filter
}.forEach { file ->
val originalPath = file.absolutePath.replace(task.outputDir.get().toString() + “/”, “”)
val reduceSize = CompressUtil.compressImg(file)
if (reduceSize > 0) {
assetsShrinkLength += reduceSize
assetsList.add(“ o r i g i n a l P a t h = > r e d u c e [ originalPath => reduce[ originalPath=>reduce[{byteToSize(reduceSize)}]”)
}
}
println(“assets optimized:${byteToSize(assetsShrinkLength)}”)
}


#### **1.3 资源去重**


相较于压缩,资源的去重需要对arsc文件格式有一点了解。为了便于理解,这里先对arsc二进制文件进行一点简单的介绍。`resource.arsc`文件是Apk打包过程中的产生的一个资源索引文件,它是一个二进制文件,源码ResourceTypes.h 定义了其数据结构。通过学习`resource.arsc`文件结构,可以帮助我们深入了解apk包体积优化中使用到的 重复资源删除、资源文件名混淆 技术。


![](https://img-blog.csdnimg.cn/img_convert/69bd5c942e165188aa61dc861d554303.webp?x-oss-process=image/format,png)


将apk使用AS 打开也能看到`resource.arsc`中存储的信息


![](https://img-blog.csdnimg.cn/img_convert/82e0e6b217f3d87fb20462b074136c4d.webp?x-oss-process=image/format,png)


说回到资源去重,去重打原理很简单,找到资源文件目录下相同的文件,然后删除掉重复的文件,最后到 `arsc` 中修改记录,将删除的文件索引名称进行替换。


由于删除重复资源在 `arsc` 中只是对常量池中路径替换,并没有删除 `arsc` 中的记录,也没有修改PackageChunk 中的常量池内容,也就是对应上图中的 Name 字段,故而重复资源的删除安全性比较高。


下面介绍下具体实施方案:


* 第一步遍历ap文件,通过 crc32 算法找出相同文件。之所以选择 crc32 是因为 gralde 的 entry file 自带 crc32 值,不需要进行额外计算,但是 crc32 是有冲突风险的,故而又对 crc32 的重复结果进行 md5 二次校验。
* 第二步则是对原始重复文件的删除
* 第三步修改 ResourceTableChunk 常量池内容,进行资源重定向



// 查询重复资源
val groupResources = ZipFile(apFile).groupsResources()
// 获取
val resourcesFile = File(unZipDir, “resources.arsc”)
val md5Map = HashMap<String, HashSet>()
val newResouce = FileInputStream(resourcesFile).use { stream ->
val resouce = ResourceFile.fromInputStream(stream)
groupResources.asSequence()
.filter { it.value.size > 1 }
.map { entry ->
entry.value.forEach { zipEntry ->
if (whiteList.isEmpty() || !whiteList.contains(zipEntry.name)) {
val file = File(unZipDir, zipEntry.name)
MD5Util.computeMD5(file).takeIf { it.isNotEmpty() }?.let {
val set = md5Map.getOrDefault(it, HashSet())
set.add(zipEntry)
md5Map[it] = set
}
}
}
md5Map.values
}
.filter { it.size > 1 }
.forEach { collection ->
// 删除多余资源
collection.forEach { it ->
val zips = it.toTypedArray()
// 所有的重复资源都指定到这个第一个文件上
val coreResources = zips[0]
for (index in 1 until zips.size) {
// 重复的资源
val repeatZipFile = zips[index]
result?.add(“${repeatZipFile.name} => c o r e R e s o u r c e s . n a m e r e d u c e [ {coreResources.name} reduce[ coreResources.namereduce[{byteToSize(repeatZipFile.size)}]”)
// 删除解压的路径的重复文件
File(unZipDir, repeatZipFile.name).delete()
// 将这些重复的资源都重定向到同一个文件上
resouce
.chunks
.filterIsInstance()
.forEach { chunk ->
val stringPoolChunk = chunk.stringPool
val index = stringPoolChunk.indexOf(repeatZipFile.name)
if (index != -1) {
// 进行剔除重复资源
stringPoolChunk.setString(index, coreResources.name)
}
}
}
}
}

resouce

}


**1.4 资源混淆**


资源混淆则是在资源去重打基础上更进一步,与代码混淆的思路一致,用长路径替换短路径,一来减小文件名大小,二来降低arsc中常量池中二进制文件大小。


长路径替换短路径修改 ResourceTableChunk 即可,与重复资源处理如出一辙。


同时我们发现 PackageChunk 中常量池中字段还是原来的内容,但是并不影响apk的运行。因为通过getDrawable(R.drawable.xxx)方式加载的资源在编译后对应的是getDrawable(0x7f08xxxx)这种16进制的内容,其实就是与 arsc 中的 ID 对应,用不上 Name 字段。而通过getResources().getIdentifier()方式调用的我们通过白名单keep住了,Name 字段在这里也是可以移除的。



    val resourcesFile = File(unZipDir, "resources.arsc")
    val newResouce = FileInputStream(resourcesFile).use { inputStream ->
        val resouce = ResourceFile.fromInputStream(inputStream)
        resouce
            .chunks
            .filterIsInstance<ResourceTableChunk>()
            .forEach { chunk ->
                val stringPoolChunk = chunk.stringPool
                // 获取所有的路径
                val strings = stringPoolChunk.getStrings() ?: return@forEach

                for (index in 0 until stringPoolChunk.stringCount) {
                    val v = strings[index]

                    if (v.startsWith("res")) {
                        if (ignore(v, context.proguardResourcesExtension.whiteList)) {
                            println("resProguard  ignore  $v ")
                            // 把文件移到新的目录
                            val newPath = v.replaceFirst("res", whiteTempRes)
                            val parent = File("$unZipDir${File.separator}$newPath").parentFile
                            if (!parent.exists()) {
                                parent.mkdirs()
                            }
                            keeps.add(newPath)
                            // 移动文件
                            File("$unZipDir${File.separator}$v").renameTo(File("$unZipDir${File.separator}$newPath"))
                            continue
                        }
                        // 判断是否有相同的
                        val newPath = if (mappings[v] == null) {
                            val newPath = createProcessPath(v, builder)
                            // 创建路径
                            val parent = File("$unZipDir${File.separator}$newPath").parentFile
                            if (!parent.exists()) {
                                parent.mkdirs()
                            }
                            // 移动文件
                            val isOk =
                                File("$unZipDir${File.separator}$v").renameTo(File("$unZipDir${File.separator}$newPath"))
                            if (isOk) {
                                mappings[v] = newPath
                                newPath
                            } else {
                                mappings[v] = v
                                v
                            }
                        } else {
                            mappings[v]
                        }
                        strings[index] = newPath!!
                    }
                }

                val str2 = mappings.map {
                    val startIndex = it.key.lastIndexOf("/") + 1
                    var endIndex = it.key.lastIndexOf(".")

                    if (endIndex < 0) {
                        endIndex = it.key.length
                    }
                    if (endIndex < startIndex) {
                        it.key to it.value
                    } else {

// val vStartIndex = it.value.lastIndexOf(“/”) + 1
// var vEndIndex = it.value.lastIndexOf(“.”)
// if (vEndIndex < 0) {
// vEndIndex = it.value.length
// }
// val result = it.value.substring(vStartIndex, vEndIndex)
// 使用相同的字符串,以减小体积
it.key.substring(startIndex, endIndex) to “du”
}
}.toMap()

                // 修改 arsc PackageChunk 字段
                chunk.chunks.values.filterIsInstance<PackageChunk>()
                    .flatMap { it.chunks.values }
                    .filterIsInstance<StringPoolChunk>()
                    .forEach {
                        for (index in 0 until it.stringCount) {
                            it.getStrings()?.forEachIndexed { index, s ->
                                str2[s]?.let { result ->
                                    it.setString(index, result)
                                }
                            }
                        }
                    }

                // 将 mapping 映射成 指定格式文件,供给反混淆服务使用
                val mMappingWriter: Writer = BufferedWriter(FileWriter(file, false))
                val packageName = context.proguardResourcesExtension.packageName
                val pathMappings = mutableMapOf<String, String>()
                val idMappings = mutableMapOf<String, String>()
                mappings.filter { (t, u) -> t != u }.forEach { (t, u) ->
                    result?.add(" $t => $u")
                    compress[t]?.let {
                        compress[u] = it
                        compress.remove(t)
                    }
                    val pathKey = t.substring(0, t.lastIndexOf("/"))
                    pathMappings[pathKey] = u.substring(0, u.lastIndexOf("/"))
                    val typename = t.split("/")[1].split("-")[0]
                    val path1 = t.substring(t.lastIndexOf("/") + 1, t.indexOf("."))
                    val path2 = u.substring(u.lastIndexOf("/") + 1, u.indexOf("."))
                    val path = "$packageName.R.$typename.$path1"
                    val pathV = "$packageName.R.$typename.$path2"
                    if (idMappings[path].isNullOrEmpty()) {
                        idMappings[path] = pathV
                    }

总结

最后对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值