减包与APK-Checker原理分析

为啥要优化包体积

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


    体积优化思维导图.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 包大小极致压缩

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

推荐阅读更多精彩内容