本文来自jsonchao的投稿,个人微信:bcce5360
本文承接上文深入探索 Android 包体积优化(匠心制作一)
三、资源瘦身方案探索
总所周知,Android 构建工具链中使用了 AAPT 工具来对资源进行处理,Manifest、Resources、Assets 的资源经过相应的 ManifesMerger、ResourcesMerger、AssetsMerger 资源合并器将多个不同 moudule 的资源合并为了 MergedManifest、MergedResources、MergedAssets。然后,它们被 AAPT 处理后生成了 R.java、Proguard Configuration、Compiled Resources。如下图左上方所示:
其中 Proguard Configuration、Compiled Resources 的 作用 如下所示:
Proguard Configuration:这是AAPT工具为Manifest中声明的四大组件与布局文件中使用的各种Views所生成的混淆配置,该文件通常存放在
${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt。
Compiled Resources:它是一个Zip格式的文件,这个文件的路径通常为 在经过 zip 解压之后,可以发现它 包含了res、AndroidManifest.xml和resources.arsc 这三部分。并且,从上面的 APK 构建流程中可以得知,Compiled Resources 会被 apkbuilder 打包到 APK 包中,它其实就是 APK 的资源包。因此,我们可以 通过 Compiled Resources 文件来修改不同后缀文件资源的压缩方式来达到瘦身效果的。但是需要注意的是,resources.arsc 文件最好不要压缩存储,如果压缩会影响一定的性能,尤其是在冷启动时间方面造成的影响。并且,如果在 Android 6.0 上开启了 android:extractNativeLibs=”false” 的话,So 文件也不能被压缩。
${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/resources-${flavorName}-${buildType}-stripped.ap_。
1、冗余资源优化
1、使用 Lint 的 Remove Unused Resource
APK 的资源主要包括图片、XML,与冗余代码一样,它也可能遗留了很多旧版本当中使用而新版本中不使用的资源,这点在快速开发的 App 中更可能出现。我们可以通过点击右键,选中 Refactor,然后点击 Remove Unused Resource => preview 可以预览找到的无用资源,点击 Do Refactor 可以去除冗余资源。如下图所示:
需要注意的,Android Lint 不会分析 assets 文件夹下的资源,因为 assets 文件可以通过文件名直接访问,不需要通过具体的引用,Lint 无法判断资源是否被用到。
2、优化 shrinkResources 流程真正去除无用资源
resources.arsc 中可能会存在很多 无用的资源映射,我们可以使用 android-arscblamer,它是一个命令行工具,能够 解析 resources.arsc 文件并检查出可以优化的部分,比如一些空的引用。
此外,当我们通过 shrinkResources true 来 开启资源压缩,资源压缩工具只会把无用的资源替换成预定义的版本而不是移除。那么,如何高效地对无用资源自动进行去除呢?
我们可以 在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现自动去除无用资源,具体的实现原理如下:
1)、首先,收集 Compiled Resources 中被替换的预定义版本的资源名称
通过查看 Zip 格式资源包中每个 ZipEntry 的 CRC-32 checksum 来寻找被替换的预定义资源,预定义资源的 CRC-32 定义在 ResourceUsageAnalyze 中,如下所示:
// A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
public static final long TINY_PNG_CRC = 0x88b2a3b0L;
// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
public static final long TINY_9PNG_CRC = 0x1148f987L;
// The XML document <x/> as binary-packed with AAPT
public static final long TINY_XML_CRC = 0xd7e65643L;
2)、然后,使用 android-chunk-utils 把 resources.arsc 中对应的定义移除。
3)、最后,删除资源包中对应的资源文件即可。
2、重复资源优化
在大型 App 项目的开发中,一个 App 一般会有多个业务团队进行开发,其中每个业务团队在资源提交时的资源名称可能会有重复的,这将会 引发资源覆盖的问题,因此,每个业务团队都会为自己的 资源文件名添加前缀。这样就导致了这些资源文件虽然 内容相同,但因为 名称的不同而不能被覆盖,最终都会被集成到 APK 包中。这里,我们还是可以 在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现重复资源的去除,具体放入实现原理可细分为如下三个步骤:
1)、首先,通过资源包中的每个ZipEntry的CRC-32 checksum来筛选出重复的资源。
2)、然后,通过android-chunk-utils修改resources.arsc,把这些重复的资源都重定向到同一个文件上。
3)、最后,把其它重复的资源文件从资源包中删除,仅保留第一份资源。
具体的实现代码如下所示:
variantData.outputs.each {
def apFile = it.packageAndroidArtifactTask.getResourceFile();
it.packageAndroidArtifactTask.doFirst {
def arscFile = new File(apFile.parentFile, "resources.arsc");
JarUtil.extractZipEntry(apFile, "resources.arsc", arscFile);
def HashMap<String, ArrayList<DuplicatedEntry>> duplicatedResources = findDuplicatedResources(apFile);
removeZipEntry(apFile, "resources.arsc");
if (arscFile.exists()) {
FileInputStream arscStream = null;
ResourceFile resourceFile = null;
try {
arscStream = new FileInputStream(arscFile);
resourceFile = ResourceFile.fromInputStream(arscStream);
List<Chunk> chunks = resourceFile.getChunks();
HashMap<String, String> toBeReplacedResourceMap = new HashMap<String, String>(1024);
// 处理arsc并删除重复资源
Iterator<Map.Entry<String, ArrayList<DuplicatedEntry>>> iterator = duplicatedResources.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, ArrayList<DuplicatedEntry>> duplicatedEntry = iterator.next();
// 保留第一个资源,其他资源删除掉
for (def index = 1; index < duplicatedEntry.value.size(); ++index) {
removeZipEntry(apFile, duplicatedEntry.value.get(index).name);
toBeReplacedResourceMap.put(duplicatedEntry.value.get(index).name, duplicatedEntry.value.get(0).name);
}
}
for (def index = 0; index < chunks.size(); ++index) {
Chunk chunk = chunks.get(index);
if (chunk instanceof ResourceTableChunk) {
ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk;
StringPoolChunk stringPoolChunk = resourceTableChunk.getStringPool();
for (def i = 0; i < stringPoolChunk.stringCount; ++i) {
def key = stringPoolChunk.getString(i);
if (toBeReplacedResourceMap.containsKey(key)) {
stringPoolChunk.setString(i, toBeReplacedResourceMap.get(key));
}