项目地址:github.com/didi/booste…java
重复 Assets:具备不一样的命名但内容(md5sum)相同的 assets 文件android
Assets 去重:去除重复的 assets,使得 APK 中同内容的 assets 仅保留一份git
通常 assets 出现大量重复的状况是很少见的,只有像滴滴这样多业务线的大致量 APP 才有可能。然而很是不幸的是,咱们确实遇到了这样的问题,虽然对包体积的影响不是很明显(也就几百 KB),可是 几百 KB 对于作字节码优化的同窗来讲,简直是要了老命了,蚊子肉也是肉啊。github
去重的关键在于拦截对 assets 的访问,没错,就是 AssetManager,Booster 的方案就是经过 Transformer 替换 AssetManager 的方法调用指令为 Booster 注入的 ShadowAssetManager,不啰嗦了,先上代码:app
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 映射表了:ide
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。google
Booster 的 assets 去重方案主要分为如下3步:spa
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
替代,具体思路以下:
AssetManager.open()
方法,在 ShadowAssetManager.open()
方法中返回 ZipInputStream
;以上整个过程对于 APP 来讲彻底透明,简直完美!