Android瘦身不反弹最佳实践

业界方案

在网上随便搜索一下就能发现瘦身有好多方案,但是实践一下就能发现好多都不靠谱

方案 作用 瘦身效果
proguard 代码混淆 效果明显
abiFilter "armeabi" 去除其他平台so 效果明显
resConfigs "zh" 语言文件去除 0.1M
shrinkResources 无用资源去除需维护keep文件 1M
TinyPng 图片压缩,账号收费 3M
ThinR 移除R文件 0.3M
AndResGuard 资源混淆白名单维护难 资源混淆0.3M,7zip压缩2M
webp android兼容性差 不推荐
Lint 无用资源去除 有可能删除getIdentifier调用的资源 不推荐
redex 安全风险高,对于加固、热修复等功能有影响 未实践
so动态加载 风险高,大部分so都需要实时加载 未实践
加固 隐藏dex 1M
重复资源优化 对比资源文件 md5,删除重复文件和resources.arsc中的定义 0.2M
移除TINY_PNG文件 通过android-chunk-utilsresources.arsc中对应的定义和文件移除,风险高 美团文章一带而过,我实践一下,实际代码特别复杂,arsc文件索引value要重新计算,减小0.1M都不到

方案实践

Smallapk Gradle插件减小APK体积25%

apply plugin: 'smallapk'

动态资源查找

其他方案网上都有,我重点讲讲SmallApk插件怎么解决getIdentifier方法带来的动态资源问题。

  1. ShrinkResources只能去除小部分无用资源的问题
  2. 解决AndResGuard需要配置白名单的问题

首先需要了解ShrinkResources的原理:

通过ResourceUseModel建立一个资源引用树,找到有可能是resource.getIdentifier调用的资源标记为reachable,找到无用资源并替换成tiny的小文件

用这种方式查找到的动态资源会特别多,因为用正则表达式匹配了所有的字符串,那么如何精确找到动态资源呢,你会发现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!
                    }
       
                }

那就只能自己想个方案找到getIdentifier引用的所有资源了。
先来看看效果,这个是getIdentifier的多种调用方式


这个是用SmallApk插件找到的动态资源

这个是找到的动态资源调用关系图

那么SmallApk是怎么做的呢


思路和android源码ResourceUsageAnalyzer是一样的,都是匹配字符串常量,唯一的区别就是加入了方法有向图搜索节点,排除大部分无用字符串。

首先形成调用有向图

/**
 * KeepResUsageVisitor会把methodNode、constantNode、fieldNode、classNode调用关系转换成有向图
 */
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) {//常量节点
                    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);//变量节点

                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) {//方法节点
                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) {//变量节点
            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的方法节点

 @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);
                    }
                }
            }

然后找到所有调用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);
                    }
                }
            }
        }

    }

最后匹配字符串常量找到动态资源

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

找到动态资源以后就能去解决AndResGuardShrinkResources的问题了

解决ShrinkResources只能去除小部分无用资源的问题,只要把找到的动态资源文件写入到/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需要配置白名单的问题,只要把动态资源加入到白名单就可以

 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没有崩溃?
    因为编译完以后布局xml文件里变成了int常量,AndResGuard修改的是字符串,int索引没变


  2. proguard也会去除R文件,那为什么用ThinR还会减小包体积?
    因为aar包里不存在R.class的,app打包的时候会重新生成lib库的R文件,但是因为生成lib库的class文件时R文件的变量不是final,所以aar里面是直接引用引用了lib.R.id,
    然后proguard判断lib库R文件是有引用关系的不能去除,ThinR相当于接着把lib库里面的R文件删除



  3. 在mac上解压缩apk再压缩会去,你会发现这个apk已经没法安装了,为什么,照理说不做任何操作应该不影响apk签名呀?
    因为MAC解压缩的时候会存在.DS_Store文件,直接压缩会把外面的文件夹目录也压缩进去


  4. 重新压缩apk以后体积会小,为什么apk自己不是压缩过了吗?
    因为默认图片是不压缩的


  5. shrinkResources不是删除了无用资源吗,那为什么我用Lint去删除无用资源,包体积还是会变小?
    一个是资源问题,一个是代码问题。
    资源问题:shrinkResources匹配字符串常量得到的无用资源会比较少,而lint扫描会只扫描硬静态引用资源,这样扫描的资源文件会比较多
    代码问题:lint还会删掉java文件,而shrinkResources只会去除无用资源,虽然android源码里面二次打包TWO_PASS_AAPT,但是默认没开启

  6. android gradle插件默认是开启v2签名的,为什么在我们的app里面用修改meta-inf文件的方式加入渠道号还可以运行?
    因为我们先加固,然后重新v1签名,再打渠道包,运气好,刚好绕过了v2签名的坑,哈哈

  7. zipalign会影响v1签名和v2签名吗?
    请在v1签名后使用zipalign,v2签名前使用zipalign,v1签名和v2签名可以同时存在,不能只用v2签名,因为在7.0手机只会校验v1签名

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容