Booster 系列之——Assets去重(chóng)

項目地址: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.javagoogle

總結

Booster 的 assets 去重方案主要分爲如下3步:spa

  1. 根據 assets 的 md5sum 進行分組,創建重複 assets 的映射關係;
  2. 替換全部類中調用 AssetManager.open(String): InputStream 的指令爲調用 ShadowAssetManager.open(AssetManager, String): InputStream
  3. 修改 ShadowAssetManager 的靜態塊,將重複 assets 的映射關係加入到 ShadowAssetManager.DUPLICATED_ASSETS 中;

擴展

經過攔截 AssetManager.open(String): InputStream 不只能夠實現 assets 的去重,還能對 assets 進行壓縮,達到減少包體積的目的,原理很簡單,主要是利用了 AssetManager.open(String) 方法的返回值是 InputStream 的特色,徹底能夠用 ZipInputStream 替代,具體思路以下:

  1. mergeAssets 以後,對 assets 進行 ZIP 壓縮;
  2. 攔截 AssetManager.open() 方法,在 ShadowAssetManager.open() 方法中返回 ZipInputStream

以上整個過程對於 APP 來講徹底透明,簡直完美!

相關文章
相關標籤/搜索