首页 Android正文

Android瘦身方案实战详解

yuange Android 2019-03-10 1043 0 Android瘦身

业界方案

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

方案作用瘦身效果
proguard代码混淆效果明显
abiFilter "armeabi"去除其他平台so效果明显
resConfigs "zh"语言文件去除0.1M
shrinkResources无用资源去除需维护keep文件1M
TinyPng图片压缩,账号收费3M
ThinR移除R文件0.3M
AndResGuard资源混淆白名单维护难资源混淆0.3M,7zip压缩2M
webpandroid兼容性差不推荐
Lint 无用资源去除有可能删除getIdentifier调用的资源不推荐
redex安全风险高,对于加固、热修复等功能有影响未实践
so动态加载风险高,大部分so都需要实时加载未实践
加固隐藏dex1M
重复资源优化对比资源文件 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签名



作者:sunshine8
链接:https://www.jianshu.com/p/e853c0467775
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


评论

在线客服-可直接交谈

您好!有什么需要可以为您服务吗?