美文网首页
减包与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