一般 assets 出现大量重复的情况是不多见的,只有像滴滴这样多业务线的大体量 APP 才有可能。然而非常不幸的是,我们确实遇到了这样的问题,虽然对包体积的影响不是很明显(也就几百 KB),但是 几百 KB 对于做字节码优化的同学来说,简直是要了老命了,蚊子肉也是肉啊。
去重的关键在于拦截对 assets 的访问,没错,就是 AssetManager ,Booster 的方案就是通过 Transformer 替换 AssetManager 的方法调用指令为 Booster 注入的 ShadowAssetManager ,不啰嗦了,先上代码:
public final class ShadowAssetManager { /** * Shadow Asset => Real Asset */ 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 映射表了:
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
中:
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 的映射关系,反编译后的代码如下:
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.java。
Booster 的 assets 去重方案主要分为以下3步:
AssetManager.open(String): InputStream
的指令为调用 ShadowAssetManager.open(AssetManager, String): InputStream
; ShadowAssetManager
的静态块,将重复 assets 的映射关系加入到 ShadowAssetManager.DUPLICATED_ASSETS
中;