Android瘦身不反彈最佳實(shí)踐

業(yè)界方案

在網(wǎng)上隨便搜索一下就能發(fā)現(xiàn)瘦身有好多方案,但是實(shí)踐一下就能發(fā)現(xiàn)好多都不靠譜

方案 作用 瘦身效果
proguard 代碼混淆 效果明顯
abiFilter "armeabi" 去除其他平臺(tái)so 效果明顯
resConfigs "zh" 語言文件去除 0.1M
shrinkResources 無用資源去除需維護(hù)keep文件 1M
TinyPng 圖片壓縮,賬號(hào)收費(fèi) 3M
ThinR 移除R文件 0.3M
AndResGuard 資源混淆白名單維護(hù)難 資源混淆0.3M,7zip壓縮2M
webp android兼容性差 不推薦
Lint 無用資源去除 有可能刪除getIdentifier調(diào)用的資源 不推薦
redex 安全風(fēng)險(xiǎn)高,對(duì)于加固、熱修復(fù)等功能有影響 未實(shí)踐
so動(dòng)態(tài)加載 風(fēng)險(xiǎn)高,大部分so都需要實(shí)時(shí)加載 未實(shí)踐
加固 隱藏dex 1M
重復(fù)資源優(yōu)化 對(duì)比資源文件 md5,刪除重復(fù)文件和resources.arsc中的定義 0.2M
移除TINY_PNG文件 通過android-chunk-utilsresources.arsc中對(duì)應(yīng)的定義和文件移除,風(fēng)險(xiǎn)高 美團(tuán)文章一帶而過,我實(shí)踐一下,實(shí)際代碼特別復(fù)雜,arsc文件索引value要重新計(jì)算,減小0.1M都不到

方案實(shí)踐

Smallapk Gradle插件減小APK體積25%

apply plugin: 'smallapk'

動(dòng)態(tài)資源查找

其他方案網(wǎng)上都有,我重點(diǎn)講講SmallApk插件怎么解決getIdentifier方法帶來的動(dòng)態(tài)資源問題。

  1. ShrinkResources只能去除小部分無用資源的問題
  2. 解決AndResGuard需要配置白名單的問題

首先需要了解ShrinkResources的原理:

通過ResourceUseModel建立一個(gè)資源引用樹,找到有可能是resource.getIdentifier調(diào)用的資源標(biāo)記為reachable,找到無用資源并替換成tiny的小文件

用這種方式查找到的動(dòng)態(tài)資源會(huì)特別多,因?yàn)橛谜齽t表達(dá)式匹配了所有的字符串,那么如何精確找到動(dòng)態(tài)資源呢,你會(huì)發(fā)現(xiàn)android源碼里面寫著Todo,哈哈。

 @Override
                public void visitMethodInsn(int opcode, String owner, String name,
                        String desc, boolean itf) {
                    super.visitMethodInsn(opcode, owner, name, desc, itf);
                    if (owner.equals("android/content/res/Resources")
                            && name.equals("getIdentifier")
                            && desc.equals(
                            "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I")) {        
                          mFoundGetIdentifier = true; 
                        // TODO: Check previous instruction and see if we can find a literal
                        // String; if so, we can more accurately dispatch the resource here
                        // rather than having to check the whole string pool!
                    }
       
                }

那就只能自己想個(gè)方案找到getIdentifier引用的所有資源了。
先來看看效果,這個(gè)是getIdentifier的多種調(diào)用方式


這個(gè)是用SmallApk插件找到的動(dòng)態(tài)資源

這個(gè)是找到的動(dòng)態(tài)資源調(diào)用關(guān)系圖

那么SmallApk是怎么做的呢


思路和android源碼ResourceUsageAnalyzer是一樣的,都是匹配字符串常量,唯一的區(qū)別就是加入了方法有向圖搜索節(jié)點(diǎn),排除大部分無用字符串。

首先形成調(diào)用有向圖

/**
 * KeepResUsageVisitor會(huì)把methodNode、constantNode、fieldNode、classNode調(diào)用關(guān)系轉(zhuǎn)換成有向圖
 */
class KeepResUsageVisitor extends ClassVisitor {

    private String className;

    public KeepResUsageVisitor() {
        super(Opcodes.ASM5);
    }

    @Override
    public void visit(int version, int access, String name, String signature,
                      String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        className = name;
    }

    @Override
    public MethodVisitor visitMethod(int access, final String name,
                                     String desc, String signature, String[] exceptions) {
        String methodName = name;

        return new MethodVisitor(Opcodes.ASM5) {

            @Override
            public void visitLdcInsn(Object cst) {
                super.visitLdcInsn(cst);
                if (cst instanceof String) {//常量節(jié)點(diǎn)
                    String constant = (String) cst;
                      GraphNode caller = new GraphNode();
                        caller.putClass(className);
                        caller.putMethod(methodName);
                        caller.putConstant(constant);
                        GraphNode called = new GraphNode();
                        called.putClass(className);
                        called.putMethod(methodName);
                        GraphHolder.addNode(caller, called);
                }
            }

            @Override
            public void visitFieldInsn(int opcode, String owner, String name, String desc) {
                super.visitFieldInsn(opcode, owner, name, desc);//變量節(jié)點(diǎn)

                GraphNode caller = new GraphNode();
                caller.putClass(owner);
                caller.putField(name);
                GraphNode called = new GraphNode();
                called.putClass(className);
                called.putMethod(methodName);
                GraphHolder.addNode(caller, called);

            }

            @Override
            public void visitMethodInsn(int opcode, String owner, String name,
                                        String desc, boolean itf) {//方法節(jié)點(diǎn)
                super.visitMethodInsn(opcode, owner, name, desc, itf);
                GraphNode caller = new GraphNode();
                caller.putClass(className);
                caller.putMethod(methodName);
                GraphNode called = new GraphNode();
                called.putClass(owner);
                called.putMethod(name);
                GraphHolder.addNode(caller, called);
            }

        };
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature,
                                   Object value) {
        final String field = name;
        if (value instanceof String) {//變量節(jié)點(diǎn)
            String constant = (String) value;
             GraphNode caller = new GraphNode();
                caller.putClass(className);
                caller.putField(field);
                caller.putConstant(constant);
                GraphNode called = new GraphNode();
                called.putClass(className);
                called.putField(field);
                GraphHolder.addNode(caller, called);
        }
        return new FieldVisitor(Opcodes.ASM5) ;
    }

}

接著找到getIdentifier的方法節(jié)點(diǎn)

 @Override
            public void call(GraphNode caller, GraphNode called) {
                if (called.getClassName().equals("android/content/res/Resources")
                        && called.getMethod().equals("getIdentifier")) {
                    if (!caller.getClassName().startsWith("android/support/v7")) {
                        dynamicCallGraph.add(caller);
                    }
                }
            }

然后找到所有調(diào)用getIdentifier的字符串常量

private void addCodeStrings() {
        mLogPrinter.println("Dynamic String---->CodeString:");
        List<GraphNode> list  = new ArrayList<>();
        Set<String> codeStrings  = new HashSet<>();
        for (GraphNode callGraph : dynamicCallGraph) {
            Collection<GraphCall> set = GraphHolder.findParentNode(callGraph);
            if (set != null) {
                for (GraphCall call : set) {
                    GraphNode caller = call.getCaller();
                    String value = caller.getConstant();
                    if (value != null) {
                        list.add(caller);
                        codeStrings.add(value);
                    }
                }
            }
        }

    }

最后匹配字符串常量找到動(dòng)態(tài)資源

                // getResources().getIdentifier("ic_video_codec_" + codecName, "drawable", ...)
                for (Resource resource : mModel.getResources()) {
                    if (resource.name.startsWith(name)) {
                        mDynamicUsed.add(resource);
                    }
                }

找到動(dòng)態(tài)資源以后就能去解決AndResGuardShrinkResources的問題了

解決ShrinkResources只能去除小部分無用資源的問題,只要把找到的動(dòng)態(tài)資源文件寫入到/build/intermediates/res/merged/release/raw/keep.xml

static void writeKeepXml(Set<ResourceUsageModel.Resource> list, File keepFile) {
    if (list == null || list.size() == 0) {
        return
    }
    StringBuffer buffer = new StringBuffer()
    list.each { value ->
        buffer.append(“@“ + value.type.getName() + “/“ + value.name)
        buffer.append(“,”)
    }
    buffer.deleteCharAt(buffer.length() - 1)
    def builder = new groovy.xml.StreamingMarkupBuilder()
    builder.encoding = “UTF-8”
    def result = builder.bind {
        mkp.xmlDeclaration()
        mkp.declareNamespace(‘tools’: ‘http://schemas.android.com/tools’)
        resources(‘tools:shrinkMode’: ‘strict’, ‘tools:keep’: buffer)
    }
    def writer = new FileWriter(keepFile)
    writer << result

}

解決AndResGuard需要配置白名單的問題,只要把動(dòng)態(tài)資源加入到白名單就可以

 Set<String> keepResSet = new HashSet<>();
        if (mDynamicUsed != null){
            for (Resource resource : mDynamicUsed) {
                keepResSet.add("R."+resource.type.getName()+"."+resource.name);
            }
        }
resproguardTask.setWhiteList(keepResSet)

你問我答

  1. AndResGuard會(huì)混淆資源文件名,xml資源文件里面也使用了文件名的字符串,那為什么apk沒有崩潰?
    因?yàn)榫幾g完以后布局xml文件里變成了int常量,AndResGuard修改的是字符串,int索引沒變


  2. proguard也會(huì)去除R文件,那為什么用ThinR還會(huì)減小包體積?
    因?yàn)閍ar包里不存在R.class的,app打包的時(shí)候會(huì)重新生成lib庫的R文件,但是因?yàn)樯蒷ib庫的class文件時(shí)R文件的變量不是final,所以aar里面是直接引用引用了lib.R.id,
    然后proguard判斷l(xiāng)ib庫R文件是有引用關(guān)系的不能去除,ThinR相當(dāng)于接著把lib庫里面的R文件刪除



  3. 在mac上解壓縮apk再壓縮會(huì)去,你會(huì)發(fā)現(xiàn)這個(gè)apk已經(jīng)沒法安裝了,為什么,照理說不做任何操作應(yīng)該不影響apk簽名呀?
    因?yàn)镸AC解壓縮的時(shí)候會(huì)存在.DS_Store文件,直接壓縮會(huì)把外面的文件夾目錄也壓縮進(jìn)去


  4. 重新壓縮apk以后體積會(huì)小,為什么apk自己不是壓縮過了嗎?
    因?yàn)槟J(rèn)圖片是不壓縮的


  5. shrinkResources不是刪除了無用資源嗎,那為什么我用Lint去刪除無用資源,包體積還是會(huì)變?。?br> 一個(gè)是資源問題,一個(gè)是代碼問題。
    資源問題:shrinkResources匹配字符串常量得到的無用資源會(huì)比較少,而lint掃描會(huì)只掃描硬靜態(tài)引用資源,這樣掃描的資源文件會(huì)比較多
    代碼問題:lint還會(huì)刪掉java文件,而shrinkResources只會(huì)去除無用資源,雖然android源碼里面二次打包TWO_PASS_AAPT,但是默認(rèn)沒開啟

  6. android gradle插件默認(rèn)是開啟v2簽名的,為什么在我們的app里面用修改meta-inf文件的方式加入渠道號(hào)還可以運(yùn)行?
    因?yàn)槲覀兿燃庸?,然后重新v1簽名,再打渠道包,運(yùn)氣好,剛好繞過了v2簽名的坑,哈哈

  7. zipalign會(huì)影響v1簽名和v2簽名嗎?
    請(qǐng)?jiān)趘1簽名后使用zipalign,v2簽名前使用zipalign,v1簽名和v2簽名可以同時(shí)存在,不能只用v2簽名,因?yàn)樵?.0手機(jī)只會(huì)校驗(yàn)v1簽名

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容