美文网首页
减包与APK-Checker原理分析

减包与APK-Checker原理分析

作者: 最喜欢小菠萝 | 来源:发表于2019-04-15 09:48 被阅读0次

    为啥要优化包体积

    • 推广成本
    • 下载转化率
    • 运行内存
    • 安装时间


      体积优化思维导图.png

    APK背景知识

    对于APK瘦身,首先我们必须了解的知识点是APK的文件结构,那么上图:


    APK文件结构.png
    • Dex : 一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
    • Res目录
      res : 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。
    • Assets文件夹 :
      存放需要打包到APK中的静态文件,assets不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。
    • Native库 :
      通常我们的so库都属于这个范畴。
    • META-INF :
      存放应用程序签名和证书的目录,签名信息可以验证 APK 文件的完整性。
    • resources.arsc :
      记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。

    由此可见:安装包的优化可以笼统的分为:资源优化、DEX文件优化两大部分


    一、资源文件减包分析

    优化思路 优化 -> 去重 -> 混淆

    1.1 优化

    在图片的格式选择上

    图片使用建议
    webP压缩效果展示.png
    此外 没有透明通道的PNG可以转换成jpg格式,有透明通道的png可以转成webP格式。以节省空间的占用

    1.2 压缩

    在Android编译过程中,下面代码中的文件格式不支持压缩:

    /* these formats are already compressed, or don't compress well */
    static const char* kNoCompressExt[] = {
        ".jpg", ".jpeg", ".png", ".gif",
        ".wav", ".mp2", ".mp3", ".ogg", ".aac",
        ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
        ".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
        ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
        ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
    };
    

    Question : 为什么谷歌官方不支持这些文件格式的压缩?

    • 1.时间与空间的收益
      如果文件是没有压缩的,系统可以利用 mmap 的方式直接读取,而不需要一次性读到内存中

    • 2.压缩效果不明显
      如上方的注释所说大部分文件压缩效果并不明显,比如jpg、png格式的图片压缩率只有3%-5%,收益不大


    1.3 去重

    各工具优缺点以及准确性分析(unUsedResource)

    Lint

    Lint中提供了unUsedResource和unUsedId去检测无用、冗余的资源。

    • 弊端
      Lint作为静态代码检查工具分析的是编译前的代码,比如Lint会忽略Proguard的代码shrink,所以Lint不能检查出这些无用代码引用的无用资源。

    Matrix-ApkChecker

    输入apk检查 解决了Lint只能检查编译前资源的缺点

    • 弊端
      类似于循环引用的资源引用方式无法被正确判定为无用资源

    1.4 混淆

    我们的项目中已经有了混淆的配置,但是并没有针对资源混淆的配置,资源混淆的思路就是把资源和文件的名字混淆成段路径:

    R.string.name -> R.string....               res/drawable/icon -> res/s/a
    

    Question : 为什么资源混淆可以减少APK体积?

    • resource.arsc的文件格式解析
    • 解析我们的apk可以发现resource.arsc与META-INF文件夹下的三个文件大小很大,原因就是他们内部保存了每个资源名称,我们在项目中有时候为了不造成冲突,就把资源名起的很长,那么这样就会导致apk的包很大,但是我们知道Android中的混淆是不会对资源文件进行混淆的,所以这时候我们就可以通过这个思路来减小包apk的大小了

    shrinkResources资源压缩功能

    在gradle中的android闭包中添加 shrinkResource true minifyEnabled true
    如果ProGuard将无用代码移除,则代码引用的资源也被标记为无用资源,然后将其移除

    • 弊端
      没有从根本上处理resource.arsc文件 较为占空间的resource.arsc仍没有得到改善
      仅将资源文件替换为空文件
      这样实际上文件数量并没有得到改善,而且resource.arsc等文件的体积也没有任何变化
    image.png

    在Android编译过程中,Java Compiler会将代码中的资源引用根据R文件直接替换为常量,而R文件中的文件资源ID默认为连续的,删除某些资源会导致ID与资源无法一一对应

    解决办法:可以使用资源混淆工具 AndResGurad 对打包好的apk进行处理

    处理后release包大小立减1M

    QQ浏览器截图20191204093020.png


    二、DEX文件减包

    DEX文件格式解析

    Dex分包
    faceBook - reDex

    三、 Matrix如何实现搜索APK中无用的资源文件

    • 首先通过读取R.txt获取apk中声明的所有资源 写入set中;
    • 通过读取smali文件中引用资源的指令 得出class中引用的资源Set;
    • 通过ApkTool解析res目录下的xml文件、AndroidManifest.xml 以及 resource.arsc 得出资源之间的引用关系;
    • 1.遍历DexFile,并使用Baskmali库将其编译成Smali文件
     private void decodeCode() throws IOException {
        for (String dexFileName : this.dexFileNameList) {
          DexBackedDexFile dexFile = DexFileFactory.loadDexFile(new File(this.inputFile, dexFileName), Opcodes.forApi(15));
    
          options = new BaksmaliOptions();
          List classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());
    
          for (ClassDef classDef : classDefs) {
            String[] lines = ApkUtil.disassembleClass(classDef, options);
            if (lines != null)
              readSmaliLines(lines);
          }
        }
        BaksmaliOptions options;
      }
    
    • 2.遍历Smali文件,找到字符串常量
     private void readSmaliLines(String[] lines) {
        if (lines == null) {
          return;
        }
        for (String line : lines) {
          line = line.trim();
          if (!Util.isNullOrNil(line))
            if (line.startsWith("const")) {
              String[] columns = line.split(",");
              if (columns.length == 2) {
                String resId = parseResourceId(columns[1].trim());
                if ((!Util.isNullOrNil(resId)) && (this.resourceDefMap.containsKey(resId)))
                  this.resourceRefSet.add(this.resourceDefMap.get(resId));
              }
            }
            else if (line.startsWith("sget")) {
              String[] columns = line.split(" ");
              if (columns.length == 3) {
                String resourceRef = parseResourceNameFromProguard(columns[2]);
                if (Util.isNullOrNil(resourceRef))
                  continue;
                if (this.styleableMap.containsKey(resourceRef))
                {
                  for (String attr : (Set)this.styleableMap.get(resourceRef))
                    this.resourceRefSet.add(this.resourceDefMap.get(attr));
                }
                else
                  this.resourceRefSet.add(resourceRef);
              }
            }
        }
      }
    
    • 3.遍历XML、resource.arsc
        private void decodeResources() throws IOException, InterruptedException, AndrolibException, XmlPullParserException {
            File manifestFile = new File(inputFile, ApkConstants.MANIFEST_FILE_NAME);
            File arscFile = new File(inputFile, ApkConstants.ARSC_FILE_NAME);
            File resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_NAME);
            if (!resDir.exists()) {
                resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_PROGUARD_NAME);
            }
    
            Map<String, Set<String>> fileResMap = new HashMap<>();
            Set<String> valuesReferences = new HashSet<>();
    
            ApkResourceDecoder.decodeResourcesRef(manifestFile, arscFile, resDir, fileResMap, valuesReferences);
    
            Map<String, String> resguardMap = config.getResguardMap();
    
            for (String resource : fileResMap.keySet()) {
                Set<String> result = new HashSet<>();
                for (String resName : fileResMap.get(resource)) {
                   if (resguardMap.containsKey(resName)) {
                       result.add(resguardMap.get(resName));
                   } else {
                       result.add(resName);
                   }
                }
                if (resguardMap.containsKey(resource)) {
                    nonValueReferences.put(resguardMap.get(resource), result);
                } else {
                    nonValueReferences.put(resource, result);
                }
            }
    
            for (String resource : valuesReferences) {
                if (resguardMap.containsKey(resource)) {
                    resourceRefSet.add(resguardMap.get(resource));
                } else {
                    resourceRefSet.add(resource);
                }
            }
    
            for (String resource : resourceRefSet) {
                readChildReference(resource);
            }
    
            for (String resource : unusedResSet) {
                if (ignoreResource(resource)) {
                    resourceRefSet.add(resource);
                    ignoreChildResource(resource);
                }
            }
        }
    
    

    参考文献:

    # Android App包瘦身优化实践-美团

    # Matrix-wiki

    # 支付宝 App 构建优化解析:Android 包大小极致压缩

    相关文章

      网友评论

          本文标题:减包与APK-Checker原理分析

          本文链接:https://www.haomeiwen.com/subject/iseiiqtx.html