2024年安卓最全Android 包体积资源优化实践_android pngquant,面试突击 百度网盘

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter,


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

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

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

1.1 插件环境配置

插件首先会初始化环境配置,如果机器上未安装运行环境则会去oss下载对应的可执行文件。

1.2 图片压缩

在开发阶段,开发同学首先会通过TinyPNG等工具主动对图片进行压缩,而对于三方库和一些业务遗漏处理的图片则会在打包的时候通过gradle插件进行压缩。

图片压缩插件使用 cwebp 对图片进行webp转换,使用 guetzli 对JPEG进行压缩,使用pngquant对PNG 进行压缩,使用 gifsicle 对gif进行压缩。在实施对过程中,对于 res 目录下的文件优先使用 webp 处理,对assets 目录下的文件则进行同格式压缩。下面先介绍下资源压缩插件的工作模式和原理。

1.2.1 Res图片压缩
  • 第一步,找到并遍历 ap_ 文件

这里对 ap_ 文件进行一下简单介绍,ap_ 文件是由 AAPT2 生成的,AAPT2(Android 资源打包工具)是一种构建工具,Android Studio 和 Android Gradle 插件使用它来编译和打包应用的资源。AAPT2 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。

AAPT2这个工具在打包过程中主要做了下列工作:把"assets"和"res/raw"目录下的所有资源进行打包(会根据不同的文件后缀选择压缩或不压缩),而"res/“目录下的其他资源进行编译或者其他处理(具体处理方式视文件后缀不同而不同,例如:”.xml"会编译成二进制文件,".png"文件会进行优化等等)后才进行打包;会对除了assets资源之外所有的资源赋予一个资源ID常量,并且会生成一个资源索引表resources.arsc;编译AndroidManifest.xml成二进制的XML文件;把上面3个步骤中生成结果保存在一个*.ap_文件,并把各个资源ID常量定义在一个 R.java\ R.txt中;

  • 第二步,解压 ap_ 文件,找到 res/drawable 、res/mipmap 、res/raw 目录下的图片进行压缩
fun compressImg(imgFile: File): Long {
    if (ImageUtil.isJPG(imgFile) || ImageUtil.isGIF(imgFile) || ImageUtil.isPNG(imgFile)) {
        val lastIndexOf = imgFile.path.lastIndexOf(".")
        if (lastIndexOf < 0) {
            println("compressImg ignore ${imgFile.path}")
            return 0
        }
        val tempFilePath =
                "${imgFile.path.substring(0, lastIndexOf)}_temp${imgFile.path.substring(lastIndexOf)}"

        if (ImageUtil.isJPG(imgFile)) {
            Tools.cmd("guetzli", "--quality 85 ${imgFile.path} $tempFilePath")
        } else if (ImageUtil.isGIF(imgFile)) {
            Tools.cmd("gifsicle", "-O3 --lossy=25 ${imgFile.path} -o $tempFilePath")
        } else if (ImageUtil.isPNG(imgFile)) {
            Tools.cmd(
                    "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("merge${variantName}Assets")
mergeAssets.doLast { task ->
    (task as MergeSourceSetFolders).outputDir.asFileTree.files.filter {
        val originalPath = it.absolutePath.replace(task.outputDir.get().toString() + "/", "")
        val filter = context.compressAssetsExtension.whiteList.contains(originalPath)
        if (filter) {
            println("Assets compress 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("$originalPath => reduce[${byteToSize(reduceSize)}]")
        }
    }
    println("assets optimized:${byteToSize(assetsShrinkLength)}")
}

1.3 资源去重

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

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

说回到资源去重,去重打原理很简单,找到资源文件目录下相同的文件,然后删除掉重复的文件,最后到 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<ZipEntry>>()
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} => ${coreResources.name}    reduce[${byteToSize(repeatZipFile.size)}]")
                    // 删除解压的路径的重复文件
                    File(unZipDir, repeatZipFile.name).delete()
                    // 将这些重复的资源都重定向到同一个文件上
                    resouce
                        .chunks
                        .filterIsInstance<ResourceTableChunk>()
                        .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]
                            }


## 文末

我总结了一些**Android核心知识点**,以及一些最新的大厂面试题、知识脑图和视频资料解析。

以后的路也希望我们能一起走下去。**(谢谢大家一直以来的支持)**

部分资料一览:

*   **330页PDF Android学习核心笔记(内含8大板块)**

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

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

*   **Android学习的系统对应视频**

*   **Android进阶的系统对应学习资料**

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

*   **Android BAT大厂面试题(有解析)**

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


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

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

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

9)]

*   **Android BAT大厂面试题(有解析)**

[外链图片转存中...(img-snujCo1d-1715730961019)]


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

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值