一般 assets 出现大量重复的情况是不多见的,只有多业务线的大体量 APP 才有可能遇到。然而非常不幸的是,我们就遇到了这样的问题,虽然对包体积的影响不是很明显(也就几百 KB),但是几百 KB 对于做包体积优化的同学来说,蚊子肉也是肉啊。
如何去重?
去重的关键在于拦截对 assets 的访问,没错,就是 AssetManager
,Booster 的方案就是通过 Transformer 替换 AssetManager
的方法调用指令为 Booster 注入的 ShadowAssetManager
,不啰嗦了,先上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public final class ShadowAssetManager {
private static final Map<String, String> DUPLICATED_ASSETS = new ArrayMap<String, String>();
public static InputStream open(final AssetManager am, final String shadow) throws IOException { final String name = DUPLICATED_ASSETS.get(shadow); return am.open(null != name && name.trim().length() > 0 ? name : shadow); }
private ShadowAssetManager() { }
}
|
就这么简单么?当然不是,上面的 DUPLICATED_ASSETS
还是空的呢,接下来就需要在构建期间构建这个重复 assets 映射表了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| fun BaseVariant.removeDuplicatedAssets(): Map<String, String> { val output = mergeAssets.outputDir val assets = output.search().groupBy(File::md5).values.filter { it.size > 1 }.map { duplicates -> val head = duplicates.first() duplicates.takeLast(duplicates.size - 1).map { it to head }.toMap(mutableMapOf()) }.reduce { acc, map -> acc.putAll(map) acc }
assets.keys.forEach { it.delete() }
return assets.map { it.key.toRelativeString(output) to it.value.toRelativeString(output) }.toMap() }
|
然后,在 Transformer 中修改 ShadowAssetManager
,在它的 clinit 中将上面构建好的 assets 映射表添加到 DUPLICATED_ASSETS 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| class ShadowAssetManagerTransformer : ClassTransformer {
private lateinit var mapping: Map<String, String>
override fun transform(context: TransformContext, klass: ClassNode): ClassNode { if (klass.name == SHADOW_ASSET_MANAGER) { klass.methods.find { "${it.name}${it.desc}" == "<clinit>()V" }?.let { clinit -> klass.methods.remove(clinit) }
klass.defaultClinit.let { clinit -> clinit.instructions.apply { add(TypeInsnNode(Opcodes.NEW, "java/util/HashMap")) add(InsnNode(Opcodes.DUP)) add(MethodInsnNode(Opcodes.INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false)) add(VarInsnNode(Opcodes.ASTORE, 0)) mapping.forEach { shadow, real -> add(VarInsnNode(Opcodes.ALOAD, 0)) add(LdcInsnNode(shadow)) add(LdcInsnNode(real)) add(MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/HashMap", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", false)) add(InsnNode(Opcodes.POP)) } add(VarInsnNode(Opcodes.ALOAD, 0)) add(MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Collections", "unmodifiableMap", "(Ljava/util/Map;)Ljava/util/Map;", false)) add(FieldInsnNode(Opcodes.PUTSTATIC, SHADOW_ASSET_MANAGER, "DUPLICATED_ASSETS", "Ljava/util/Map;")) add(InsnNode(Opcodes.RETURN)) } } } else { klass.methods.forEach { method -> method.instructions?.iterator()?.asSequence()?.filterIsInstance(MethodInsnNode::class.java)?.filter { ASSET_MANAGER == it.owner && "open(Ljava/lang/String;)Ljava/io/InputStream;" == "${it.name}${it.desc}" }?.forEach { it.owner = SHADOW_ASSET_MANAGER it.desc = "(L$ASSET_MANAGER;Ljava/lang/String;)Ljava/io/InputStream;" it.opcode = Opcodes.INVOKESTATIC } } }
return klass } }
|
以上 ShadowAssetManagerTransformer
的作用便是改写 ShadowAssetManager
的静态块,往 DUPLICATED_ASSETS
中添加重复 assets 的映射关系,反编译后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public final class ShadowAssetManager {
private static final Map<String, String> DUPLICATED_ASSETS;
static { Map<String, String> var0 = new HashMap<String, String>();
var0.put("assets-1-1", "assets-1"); var0.put("assets-1-2", "assets-1"); var0.put("assets-1-3", "assets-1");
var0.put("assets-2-1", "assets-2"); var0.put("assets-2-2", "assets-2");
......
var0.put("assets-N-1", "assets-N"); var0.put("assets-N-2", "assets-N"); ...... var0.put("assets-N-n", "assets-N");
DUPLICATED_ASSETS = Collections.unmodifiableMap(var0) }
}
|
美中不足
本方案能解决大部分的重复 assets 问题,但是字体除外——因为字体的加载并不是通过 Java 层的 AssetManager
完成的,有兴趣的同学可以研究一下 Typeface。
总结
Booster 的 assets 去重方案主要分为以下 3 步:
- 根据 assets 的 md5sum 进行分组,建立重复 assets 的映射关系;
- 替换所有类中调用
AssetManager.open(String): InputStream
的指令为调用 ShadowAssetManager.open(AssetManager, String): InputStream
;
- 修改
ShadowAssetManager
的静态块,将重复 assets 的映射关系加入到 ShadowAssetManager.DUPLICATED_ASSETS
中;
扩展用法
通过拦截 AssetManager.open(String): InputStream
不仅可以实现 assets 的去重,还能对 assets 进行压缩,达到减小包体积的目的,原理很简单,主要是利用了 AssetManager.open(String)
方法的返回值是 InputStream
的特点,完全可以用 ZipInputStream
替代,具体思路如下:
- 在 mergeAssets 之后,对 assets 进行 ZIP 压缩;
- 拦截
AssetManager.open()
方法,在 ShadowAssetManager.open()
方法中返回 ZipInputStream
;
以上整个过程对于 APP 来说完全透明,简直完美!