基于 Facebook Redex 实现 Android APK 的压缩和优化

96
asce1885
0.1 2016.04.14 22:17* 字数 1934

@author ASCE1885的 Github 简书 微博 CSDN
本文由于潜在的商业目的,不开放全文转载许可,谢谢!

最近 Facebook 开源了一个名为 Redex[1] 的工具包,专门用于 Android 字节码的优化,经过 Redex 转换后的 APK,体积变得更小,运行速度变得更快。Redex 基于管道的方式来优化 Android 的 .dex 文件,一个源 .dex 文件通过管道进行一系列的自定义转换后,将得到一个优化的 .dex 文件。接下来将带大家简单快速的了解 Redex 是什么,以及它的基本原理和使用方法,更详细内容可以参见《optimizing-android-bytecode-with-redex》[2]

转换的时机

我们知道 Android 的编译过程首先是通过 javac 工具将 .java 文件编译成 .class 文件,接着将所有的 .class 文件合并成 Dalvik 虚拟机的可执行文件 .dex,最后再跟其他资源等文件一起压缩成 APK 文件,大致流程如下所示:

Redex 选择基于字节码文件而不是 Java 源码进行优化,是因为字节码相比 Java 源码而言,可以进行更为全局的,类与类之间的优化,而不是单个类文件的局部优化;选择基于 dex 字节码而不是 Java 字节码进行优化,是因为某些优化只能在 dex 文件中进行。

管道的思想

鉴于随着时间的推移,开发人员可能会不断得到新的优化 idea,为了方便的将新的优化点加入既有的代码中,同时也方便不同开发人员并行开发优化点, 所以 Redex 选择基于管道的思想来实现 dex 的优化,这样每一个优化的 idea 可以通过插件的形式集成到管道中,实现即插即用,也不会影响其他的优化插件,整体优化流程如下所示:

减少字节码的意义

减少 APP 中字节码的大小有很多好处,最明显的是可以减少 APP 下载和安装的时间,也可以减少 APP 安装后占用的存储空间。同时,理论上更少的字节码也意味着需要执行的指令更少,需要加载进内存的代码页发生缺页的情况也更少,这些显然对于资源密集型使用场景例如应用冷启动起到很好的性能优化作用。在 Redex 中,基于管道的思想实现了一系列旨在减少和优化字节码的转换器,下面来了解其中的三种。

混淆和压缩

代码混淆广泛应用于 Web 开发语言例如 Javascript 和 HTML中,在不改变功能的前提下,通过使用无意义和简短的字符来替换完整的字符串信息,从而减少总的代码大小,Android 中使用 Proguard 也达到类似的目的。

在开发阶段,代码中可读性强的字符串信息是非常重要的,例如类的完整路径,源文件的路径,函数名称等等。但对于编译后的字节码而言,这些完整的字符串信息占用了较大的空间,更重要的是,虚拟机运行字节码的时候并不关心这些字符串信息是否可读性强,abc()MyFooSuccessCallback() 这两者对虚拟机的处理来说并无区别。因此,我们就可以将字节码中可读性强但占用空间的字符串替换成无意义但简短的字符串,如下所示:

跟 Java 层代码使用 Proguard 混淆后需要生成 mapping.txt 文件类似,字节码的混淆也需要生成对应的映射文件,以便当 APP 运行时出现问题需要定位的时候,可以将混淆后的日志信息通过映射文件还原成可读的字符串信息,映射文件内容类似这样:

使用内联函数

内联函数是在编译期间将函数体直接嵌入该函数的调用处,也就是在编译时不具备函数的性质,不存在执行函数调用产生的开销,从而得到提高代码运行性能的目的,同时,如果正确的应用它,还可以减小编译后生成的文件大小。软件工程的最佳实践鼓励开发者要具备封装的思想,要明确类的职责,这样往往会导致需要对一个类按功能和职责进行拆分等操作。在实际开发中,这种思想是很重要的,但同时在最终生成的字节码中也留有进一步优化的空间。

最简单的一个例子是适配器类型的函数,这些函数通常是用来封装一些小函数,以提供更简洁统一的 API 接口,或者是由于参数列表不同而存在的多个重载函数,亦或者是setter/getter 函数等,这些函数在最终生成的 APK 中有的可能根本不会被调用到。因此在 .dex 文件阶段对这些函数进行内联操作,是很大的一个优化点。

无用代码的消除

在大型项目的源代码中,不可避免的会存在很多无用的代码,移除这些无用代码在减少最终 APK 的大小的同时也不会带来任何副作用。在某些方面,无用代码的移除类似于标记清除(mark-sweep)垃圾回收算法。我们从某个明确会调用的入口,例如 MainActivity 开始遍历各个条件分支和函数调用,在生成的图中标记访问到的代码,在遍历了所有的条件分支和函数调用之后,就可以判定那些没有被标记的函数是无用的代码,可以安全的删除,如下所示:

当然,理论上很简单,但在 Android 中还需要处理类似反射,或者 XML 布局文件中对代码的引用等异常情况。

Redex 的集成和使用

在使用 Redex 之前,首先要配置好编译环境,Redex 目前支持 Mac OSX 和 Linux 系统,Windows 同学请掩泪飘过~,下面以 Mac OSX 为例说明。

依赖的安装

打开 Terminal 窗口,执行以下的 HomeBrew 命令安装 Redex 的依赖:

brew install autoconf automake libtool python3
brew install boost double-conversion gflags glog libevent

下载,构建和安装

Redex 的依赖成功安装后,接着使用 Git 将 Redex 的源码 checkout 到电脑上,由于 Redex 是以子模块的方式引入 folly[3] 的,因此需要执行如下命令初始化子模块:

git submodule update --init

最后,通过 autoreconfmake 命令来编译 Redex:

autoreconf -ivf && ./configure && make && make install

成功后,就可以开始使用 Redex 来转换现有的 APK 文件中的 .dex 文件了。

使用

Redex 的使用很简单,如下所示,只需指定源 APK 和 生成的 APK 的路径就可以:

redex path/to/your.apk -o path/to/output.apk

更多干货,欢迎关注我的微信公众号


  1. https://github.com/facebook/redex

  2. https://code.facebook.com/posts/1480969635539475/optimizing-android-bytecode-with-redex

  3. https://github.com/facebook/folly

Web note ad 1