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

業(yè)界方案

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

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

方案實(shí)踐

Smallapk Gradle插件減小APK體積25%

apply plugin: 'smallapk'

動態(tài)資源查找

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

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

首先需要了解ShrinkResources的原理:

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

用這種方式查找到的動態(tài)資源會特別多,因?yàn)橛谜齽t表達(dá)式匹配了所有的字符串寡键,那么如何精確找到動態(tài)資源呢掀泳,你會發(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插件找到的動態(tài)資源

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

那么SmallApk是怎么做的呢


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

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

/**
 * KeepResUsageVisitor會把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);
                    }
                }
            }
        }

    }

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

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

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

解決ShrinkResources只能去除小部分無用資源的問題沙廉,只要把找到的動態(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需要配置白名單的問題憨颠,只要把動態(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會混淆資源文件名氮趋,xml資源文件里面也使用了文件名的字符串辜御,那為什么apk沒有崩潰鸭你?
    因?yàn)榫幾g完以后布局xml文件里變成了int常量,AndResGuard修改的是字符串,int索引沒變


  2. proguard也會去除R文件袱巨,那為什么用ThinR還會減小包體積袜茧?
    因?yàn)閍ar包里不存在R.class的,app打包的時(shí)候會重新生成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再壓縮會去俺夕,你會發(fā)現(xiàn)這個(gè)apk已經(jīng)沒法安裝了,為什么贱鄙,照理說不做任何操作應(yīng)該不影響apk簽名呀劝贸?
    因?yàn)镸AC解壓縮的時(shí)候會存在.DS_Store文件,直接壓縮會把外面的文件夾目錄也壓縮進(jìn)去


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


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

  6. android gradle插件默認(rèn)是開啟v2簽名的曾我,為什么在我們的app里面用修改meta-inf文件的方式加入渠道號還可以運(yùn)行?
    因?yàn)槲覀兿燃庸蹋缓笾匦聉1簽名健民,再打渠道包抒巢,運(yùn)氣好,剛好繞過了v2簽名的坑秉犹,哈哈

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筑辨,一起剝皮案震驚了整個(gè)濱河市俺驶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖暮现,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件还绘,死亡現(xiàn)場離奇詭異,居然都是意外死亡栖袋,警方通過查閱死者的電腦和手機(jī)拍顷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來塘幅,“玉大人昔案,你說我怎么就攤上這事〉缦保” “怎么了踏揣?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長匾乓。 經(jīng)常有香客問我捞稿,道長,這世上最難降的妖魔是什么拼缝? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任娱局,我火速辦了婚禮,結(jié)果婚禮上咧七,老公的妹妹穿的比我還像新娘铃辖。我一直安慰自己,他們只是感情好猪叙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布娇斩。 她就那樣靜靜地躺著,像睡著了一般穴翩。 火紅的嫁衣襯著肌膚如雪犬第。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天芒帕,我揣著相機(jī)與錄音歉嗓,去河邊找鬼。 笑死背蟆,一個(gè)胖子當(dāng)著我的面吹牛鉴分,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播带膀,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼志珍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了垛叨?” 一聲冷哼從身側(cè)響起伦糯,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后喂击,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡淤翔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年翰绊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旁壮。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡监嗜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寡具,到底是詐尸還是另有隱情,我是刑警寧澤稚补,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布童叠,位于F島的核電站,受9級特大地震影響课幕,放射性物質(zhì)發(fā)生泄漏厦坛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一乍惊、第九天 我趴在偏房一處隱蔽的房頂上張望杜秸。 院中可真熱鬧,春花似錦润绎、人聲如沸撬碟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呢蛤。三九已至,卻和暖如春棍郎,著一層夾襖步出監(jiān)牢的瞬間其障,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工涂佃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留励翼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓辜荠,卻偏偏與公主長得像汽抚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子伯病,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容