由于文章特别长,建议先收藏再阅读。
在 Android 性能优化的知识体系当中,包体积优化一直被排在优先级比较低的位置,从而导致很多开发同学对自身应用的大小并不重视。在项目发展的历程中,一般可划分为如下三个阶段:
初创期1 3 % ) A F T c T => 成长期` % k 7 => 成熟期
通常| y B V @ - Z 来说y | a q } 6 l M,当应用处于成长期的中后阶段时,才会考虑去做系统的包体积优化,因此,只有在这个阶段及之后,包体积优化带来的收益才是可观的。
那么,包体积优化能够给我们带来哪些 收益 呢?如何全面对应用的包体积进行 系统分析 及 针对性优化呢?在这篇文章中,我们将一起进行深入地分析与探索。
1、瘦身优化及 Apk 分析A e { & A 3 J 6 +方案介绍
1、瘦@ I ) K身优势
我们首先来介绍下,为什么我们需要做 APK 的瘦身优化?
主要x m t i有 三个方面 的原因:
(1)下载转化率
Al q e qPK 瘦身优化在实际的项目中优先级是比较低的,因为) a q i ]做了之后它的好处不是那么明显,尤其是那些还没有到 稳定期 的项目,我们都知道,App 的发展历程是从 项目初期 => 成长期 => 稳定期,对于处于 发展初期与成长期 的项目而言,可能会做 启动优化、卡顿优化,但是一般不会做 瘦身优化,瘦身优化 最主要的好处是对应用 下载转化率 的影响,它是 App 业务运营q + F * H -的重要W , ( =指3 f k标之一,在项目精细化运营的阶段是非常重要q - y - h v的。
因为如果你的 App 与其它同类型的 App 相比 Apk 体积要更小的话,那么你的 App 下载率就可能要高一些。i 4 N而且,包体积越小,用户下载等待的时间也会越短,所以下载转换成功率也就越高。所j 4 i P :以,安装包大小与下载转化率的关系 大致是成反比 的,即安装包越大,下载转换率就越小。
一个 80MB 的应用,用户即使点了下载,也可能因为网络速度慢、突然反悔导致下载失败。而对于一个 20MB 的应用,用户点了下载之后,在犹豫要不要下的时候可能就已经下载完了。
而且,现在很多l ! h } } & + !大型的 App 一般都会有一个 Lite 版本的 App,这个也是出于下载转化率方面的考虑。
2应用市场
Google Play 应用市场强制要求( D ^ n [ n f .超过 100MB 的应用只能使用 APK 扩展文件方式 上传 6 n m + h h。当使用 APK 扩展文件方式 上传时,Google Pla) ! . a e [ W $y 会为我们的应用 托管 扩展文件,并将其 免费提供 给设备。
扩展文件将保存到设备H ] a P p { * u `的共享存储位; D z b n . f w置O B ; T $(SD 卡或可安装 USB 的分区;也称为“外部”存储),应用可以在其中访问它们。在大多数设备上,Google Play 会在下载 APK 的同时下载扩展文件,因此应用在用户首次打开时便拥有了所需D l r ; M $ k | G的一切。
但是,在某些情况下,我们的应用必须在应用启动时从 Google Play 下载文件。如果您想避免使用扩展文件,并且想要应用程序的下载大小大于4 H o100 MB,则应该使用 Android App Bundles 上传应用程序,此时应用程序最多可提供150 MB的压缩下载大小。
Androidq y Z G m App Bundles 就是 Android 应用程序捆绑包,它能够让 App 以 添加动态功能模块的方式 去解决 APZ 4 # WK 大小$ X M较大的问题。如下,就是由一个基本模块和两个动态功能模块组成的 Android App Bundle APK 的; m V J ` Q ;组2 m 8 w y成结构图:
3、渠道合作商的要求
此外,还有一个原因,当我们的 App 做大之后,可能需要跟各个手机厂q D . P商合作预装,这] ? L V些 渠道合作商会对你的 App 做详细的要求,只有达到相应的要求后才允许你的 App 预装到手机上。而且,越大的W @ 4 } b & O D C App 其单价成本也会越高。所以,瘦身也是我们项目做大之后一定会遇到的一个问题| E J 4 G。
体积过大对 App 性能的影响
此外,包体积除了会影响 应用的下载转化率 之外,主要还会对 App 三个方面 的性能有一定的影响,如下所示:
1)安装时间:比如 文件拷贝、Library 解压,并且,在编译 ODEX 的时候,特! o A ^别是对于 Android 5.0 和 6.0 系统来说,耗费的时间比较久,而 Android 7.0 之后有了 混合编译,所以还可以接受。最后,App 变大后,其 签名校验 的时间也会变长。
2)运行时内存:Resource 资源、Library 以及 Dex 类加载都会占用应用的一部分内存。
3)ROM 空间:如果应用的安装包大小为 50MB,那么启动解压之后很可能就已经超过 100MB 了。并且,如果 闪存空间不足,很可Y _ # J a _ G 5能出现“写入放大”E ( q H f ; : c u的情况t G J d,它是闪存和固态硬盘(SSD)中一种不良的现象,闪存在可重新写入数据前必须先擦除,而擦除操作的粒? Q 4 E 6 # :度与写入操作相比低得多,执行这些操作就会多次移动(或改写)用户数据和元数据。
因此,S m @ Q要改写数据,就需要读取闪存某些已使用的部分,更新它们,并写入到新的位置,如果新位置在之前已被使用过,还需连同先擦除;由于) + P Y ^ = } = d闪存的这种工作方式V * q,必须擦除改写的闪存部分比新数据实际需要的S f I G c大得多。即最终可能导! x L V 2 H {致实际写入的物理资料量是写入资料量的多倍。
2、Q ? y [APK 组成
我们都知道,Android 项目最终会编译成一个 .apk 后缀的文件,实际上它就是一个 压缩包。因此,它内部还有很多不同类型的文件,这些文件,按照大小,共分为如下几类:
1)代码相关:clas^ N 4 2 : Y D )ses.dex,我们在项目中所编写的 java 文件,经过编译之后会生成一个 .class 文件,而这些所有的 .clap : * s O * L B -ss 文件呢,它最A z G 0 Q ` X A C终会经过 dx 工具编译生成一个 classm v tes.dex。
2)资源相关:res、assets、编译后的二进制资源文件 ru # 9 Kesources.arsc 和 清单文件 等等。res 和 assL u : qets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的- 6 f c资源 ID,而 assets 不会自动生成对应的 ID= V 6 u,而H a c L是通过 AssetManager 类的接口来获取。此外,每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些P g Q 7 c : y ` ID 和 资源文件位置对应关系 的文件。
3)So 相关:lib 目录下的文件,这块文件的优化空间其实非常大。
此外,还有 META-INF,它存S S g 1 n o G u &放了应用的 签名信息,其中主要有 3个文件,如下所示:
MANIFEST.MF:其中每一个资源文件都有一个对应的 SHA-256-Digest(SHA1) 签名,MANIFE0 N W i LST.q O z 3 aMF 文件的 SHA2569 W P 2 5 3 T ,(SHA1) 经过 base64 编码的结果即为 CERTR C 8 4 l u |.SFB g 3 中的 SHA256(SHA1)-Digest-Manifest 值。
CERT.SF:除了Y ~ f ? 4开头处定义的 SHA256(SHA1)-Digest-Manifest 值,后面几项的值是对 MANIFEST.MF 文件中的每项再次 SHA256(SHA1) 经过 base64 编码后的值。
CERT.RSA:其中包含了公钥、加密算法等信息。首= X T /先,对前一步生成的MANIFESd K 4 J 0T.MF使用了SHA256(SHA1)-RSA算法,用开发者私钥签名。然后,在安装时使用x r w公钥解密。最后,将% H + + Z其{ / F C p 4 1 e #与未加密的摘要信息(MANIFEST.MF文件)进行对比,如O n b , ~ Q 2果相符,则表明内容没有被修改。
代码瘦身方案探索
在讲解如何对 Dex 进行优化之前,可能有很多同学对 Dex 还没有足够的了解,这里我们就先详细地了解下 Dex。
1、( - 4 GDex 探秘
1)Dex 是 Android 系统的可执行文件,包含 应用程序的全部操作指令以及运行时数据。因为 Da} b Flvik 是一种针对嵌入式设备而特殊设计的 Java 虚拟机,所以 Dex 文件与标准的 Class 文件在结构设计上有着本质的区别。
2)当 Java 程序被编译成 clasF s ? V ;s 文件之后,还需要使用 dx 工具将所有的 class 文件整合到一个 dex 文Y M M | Q c 5件中,这样 dex 文件C L r ` t ? c就将原来每个 cz i - xlass 文f a K } 7 .件中都有的共有信息合成了一体,这样做的目的是 保证其中的每个类都能够共享数据,这在一定程度上 降低了信息冗余,同时也使得 文件结构更加紧凑。
与传统 jar 文件相比,Dex 文1 3 q n Y z & q S件的大小能j ^ | Z & & . (够缩减 50% 左右。关于 Class 文件与 Dex 文件的结果对比图如下所示:
如果想深入地了解 Dex 文件格式,可以参见Google 官方教程 - Dex格式h e _ Z ^ t。
Dex 一般在应用包体积中占据了不少比重,并且,Dex 数量越多,App 的安装时间也会越长。所以,? o [ N % u r优化它们可以说是 重中之重。下面,我们就来看看有哪些方式可以优化 Dex 这部分R O a &的体积。
2、ProGuard
混淆这里就不赘述了,大家应该比较熟悉,原文有g : 4 W比较详细的介绍。
3、D8 与 Rs m ` a Z8 优化
D8 优化
D8 的 优化效果 总的来说可以归结为如下 四点:
- Dex的编译时间更短。
- de3 p / Z O + l +x文件更小。
- D8 编译的 .dex 文件拥有更好的运行时性能。
- 包含 Java 8 语言支持的处理。
在 Android Sg : Z a s Btudio 3.0需要主动在 gradle.properties 文件中新增:
androig M 2 O 8 _d.enableD8&{ : O a $nbsp;= true
Android Studio 3.1 或之后的版本 D8 将会被作为默认的 Dex 编译器。
R8 优化
R8 官方文档(目前已经开# n + # R # L源)
https://r8.googlesource.com/r8
- R8 是 Pro( Q 3 - - G x c Iguard 压缩与优化部分的替代品,并且它仍然使用与 Proguard 一样的 keep 规则。如果我们仅仅想在 Android Studio 中使用 R8,当我们在 build.gradle6 V w J K o X 中打开混淆的时候,R8 就已经默认集成进 Android GrA Y &adle plugin 中了。
如果我们当前使用的是 Android Studio 3.4 或 Android Gradle 插件 3.4.0 及其更高版本,R8 会作6 7 1 v / W T U为默认编译器。G V 3否则,我们 必须要在 gradle.properties 中配置如下代码让 App 的混淆去支持 R8,如下所F q 5 8 q } b示:
android.enableR8=true
android.enableR8.libraries=t8 - E i 0rue
那么,R8 与混淆相比优势在哪里呢?
- ProGuG } 9 S M $ x [ard 和 R8 都应用了基本名称混淆:它们 都使用简短,无意义的名称重命名类,字段和方法。他们还可以 删除调试属性。
- 但是,R8 在 inline 内联容器类中更有效,并且在删除未使用的类,字段和方法上则更具侵W r K = 2略性。例如,R8 本身集成在 ProGuard V6.1.1 版本中,在压缩 apk 的大小方面,与 ProGuard 的 8.5% 相比,使用 R8 apk 尺寸减小了约 10%。并且,随着 Kotlk y } ^ 8 1 B : xin 现在成为 AndU ~ D Hroid 的第一语言,R8 进行了 ProGuard 尚未提供的一些 Kotlin 的特定的优化。
从表面上看,ProGuard 和 R8 非常相似。它们都使用相同的配置,因此在它们之间进行C 5 + Y # 4 }切换很容易。放大来看的话,它们之间也存在一些差异。R8 能更好地内联容器类,从而避免了对象分配。但是 ProGuard 也有其自身的优势,具体有如下几点:
1)、ProGuard 在将枚举类型简化为原始整数方面会更加强大。它还传递常量方法参数,这通常对于使用应用程序的特定设置调用的通用库很有用。
ProGuard 的多次优化遍历通常可以产生一系列优化。例如,第一遍可以传递一个常量方. ` 4 D P法参数,以便下一遍可以删除该参数并进一步传递该值。删除日志代码时,多8 ! V d , a次传递的效果尤其明显。ProGuard 在u c 6 7 { 4 R B 9删除所有跟踪(包括组成日志消息的字符串操作)方面更有效。
2)、ProGuard 中应用的模式匹配算法可以识别和替换短指令序列,从而提高代码效率并为更多优化打开了机会。在优@ * 4 D + b & [ J化遍历的顺序中,尤其是数学运算和字符串运算可从中受益。
3、最后,ProGua ? 6 7 qrd 具有独特的能力来优化使用 GSON 库将对象序列化或反序列化为 JSON 的代码。该库严重依赖反射,这很方便,但效率低下。而 Pro} X m u * Q 0Guard 的优化功能可以 通过更高效,直接的访问I } 4 E q /方式 来代替它。
R8 优化实战
接下W [ s , |来,我们就来看看 AwesomK - u ^ s p 8 P +e-WanAndroid 使用 R8 后,K N 9 cAPK 体积的变化,如下图所示:
- 可以看到,相较于仅@ X z u K i 5 *使用混淆后的 APK 而言,大小减少了 0.1MB,Dex 部分的优化效果大概为 5%,APK 整体的压缩效果也有 1.5% 左右。
- 虽然从减少的 APK 大小来看,0.1MB 很少,但是比例并不小,如果你负责的是一个像微信、淘宝等规模的 App,它们的J & V b } ,体积一般都将近 100MB,使用 R8 后也能减小 1.5M] + m 0 0 |B 的大小。
- D8 与 R8 的作用非常强大,而 Jake Wharton 大神最近一年多也i } 4在研究 D8 与 R8 的知识,如果想对 D8 与 R8T A # K 5 y 0 的实现细节有更多地了解,可以看看他的 个人博客。
4、去除 debI b V @ cug 信息与行号信息
- 在讲解什么是 deup t I ^ G & 9bg 信息与行号信息之前,我们需要先了解 Dex 的一些知识。
- 我们都知道,JVM 运行时加载的是 .class[ Y a L U A m 文件,而 Android 为了使包大小更加紧凑、运行时更加高效就发明了 Dalvik 和 ART 虚拟机,两种虚拟机运行的都是 .dex 文件,当然 ART 虚拟机还可以同时运行 oat 文件。
- 所以 Dex 文件里的信息内容和 Class 文件包含的信息是一样的,不同的是 Dex 文件对 Class 中的信息做了去重,一个 Dex 包含了很多的 Class 文件,并且在结构上有比较大的差异,Class 是流式的结构,N { Y w ADex 是分区结构,Dex 内部的K Y - b I X X = x各个区块间通过 offset 来进行索j Q l Y N引。
为了在应用出现问题时,我们能在调试的时候去显示相应的调试信息或者上报 craY k 3 F X U Y o (sh 或者主动获取D O ^ x调用堆栈的时候能通过 debugItM , [ n Nem 来获取对应的行号,我们都会在混淆配置中加上下面的规则:
-keepattributes SourceFile, LineNumberTable
这样就会保留 Dex 中的 debug 与行号信息,此时的 Dex 结构图 如下所示:
从图中可以看到,Dex 文件的结构主要分为 四大块:header 区,索引区,data 区,map 区。而我们的 debug 与行号信息q ; d 1 [就保存在 data 区中的 debugItems 区域。
而 debug_items 里面主要包含了 两种信息,如下g 0 ? D n : G s b所示:
- 调试的信息:包含函数的参数和所有的局部变量。
- 排查问题的信息:包含所有的指令集行号与源文C G [件行号的对应关系。
根据 Google 官方的数据,debugItem 一般占 Dex 的比例有 5% 左右,如果我们能去除 debug 与行号信息,就能更进一步对 D| P J } + I fex 进行瘦身,但是会失去调试信息的功能,那么,有什么方式可以去掉 debugItem,同时又能让 crash 上报的时候能拿到正确的行号呢?
我们可以尝试直接修改 Dex 文件,保留一小块 debugItem,让系统查找E { ;行号的时候指令集行号和源文件行号保持一致,这样任何监控上报的O k行号都b u q - #直接/ + j + M b变成了指令集行号。
每一个方法都会有一个 debugInfoItem,每一个 debuginfoItem 里面都有一个指令集行号和源文件行号的映r { g 射关系,这了我们直接把多余的 debugInfoItem 全部删掉,只保留了一个 debugInfoItem,4 m h 2 = B ;这样所有的方法都会指向同一个 debugInfou 7 X k 9 I OItem,并且这个 debugInfoItem 中的指令集L k $ | D ? j行号和源文件行号保持一致,这样不管用什么方式来查找行号,拿到的[ : D Y都是指令集行号。
需要注意的是,采用这种方案 需要兼容所有虚拟机的查找方式,因此 仅仅保留一个 debugInfoItem 是不够的,需要对 debugInfoItem 进行分区,并且 debugInfoItem 表不能太大。
关于如何去除 Dex 中的 Debug 信息是通过 ReDex 的 StripDeb) I A N n 2 @ugInfoPass 来完成的,其配置如下g 9 w E所示:
{
\"redex\" :&nbR t . 3 ( m ` 7 [sp;5 L 2 I v h n w {
&n/ F V J h ` $bsp; \"passes\" : 4 c D[
&nG [ k p 8 %bsp; % p P { D = F \"StripDebugIV ` m C b ZnfoPass\",
 8 P a ) B; \"RegAllocPass\"8 c 7 5 & 5 A ^
&nbsU ] o n ~ A ! z op; ]
l 0 v K J n : 7 M&nP O ! 6 n Ebsp; },
\"StripDebugInfoPass\" : {
\"drR Q Dop_all_dbg_info\" : falsq 2 = ( n ! We,
&nh ! Y = 2 * gbsp;&n8 l ` G d s dbsp; \"drop_locq 0 l * m h o 8 al_variables\" : true,
\"drop_lin% i ze_numbers\" : false,
&nbsV p . F yp; \"drop_src_files\" : falF u ~ L :se* % 9 ^,
&A L . d B C 1 ` Dnbsp; &u / l A g tnbsp;\"use_whitelist\0 M x 7 C y k i" : false,
&@ } . x 6 0 $ d dnbsp; \"cls_whitelist\"&R K - 2 Xnbsp;: [],
\"method_whiteli~ 1 n M ; K / Dst\" : [],
w N , t * ) \"drop_prologuy . S +e_end\" : true,
&nbb C g 4 zsp; \"drop_epilogue_begin; _ p\" : true,
\"drop_all_dbg_info_if_empty\" : true
z p v 1 + (},
\"RegAllocPass\" : {
\"live_range_splitting\": false
&A e J D 0 ; q Ynbsp;}
}
关于 debuginfo 的实战我们下面马上会开始,在此之前,我们先讲讲 Dex 分包中的另一个优化点。
5、Dex 分包优化
Dex 分包优化原理
当我们的 APK 过大时,Dex 的方法数就会超过65536个,因此,必须采用 mutildex 进行分包,E V s但是此时每一个 Dex 可能会调用到其它 De) a g P 8 1 mx 中的方法,这s 6 1 0 F & S种 跨 Dex 调用的方式Z h - Y ? ~ U e会X B I a ^ (造成许多冗余G [ p h N + ! n D信息,具体有如下两点:
- 多余的 methoN * 7 ; Ud id:跨 Dex 调用会导致当前dex保留被调f t i A S 8 #用dex中的方法id,这种冗余会导致每一个dex中可以存放的classD g 5变少,最N t v J | C终又会导致编译出来的dex数量增多,而dex数据的增加又会进一步加重这个问题: l t z H K `。
- 其它跨dex调用_ N G v *造成的信息冗余:除了需要多记录被调用的method id之外,还需多记录其所属类和当前方法的定义信息,这会造成 string_ids、type_ids、proto_ids 这几部分信息的冗余。
为了减少跨 Dex 调 C a D t M用的情况,我们必须 尽量将有调用关系的类和方法分配到同一个 Dex 中。但是各个类相互之间的调用关系是非常复杂的,所以很难做到最优的情况。
所幸9 L e U J的是,ReDex 的 Cy ; f I xrossDexDefMinimizer 类分析了类之间的调用关系,并 使a a +用了贪心算法去计算局部的最优解(编译效果和dex优化效果之间的某一个平衡点)P o N。
https://github.com/facebooR 4 o w M ^ n 7 _k/redeO X ) Dxw 3 Y X z n X t/blob/master/opt/inte` 0 3 w Qrdex/CrossDexRefMinimizer.cpp
使用 \"InterDej & x LxPass\" 配置项可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度。
在 ReDex 中使用 Dex 分包优化 s ) U & b T A [跨 dex 调用造成的信息冗余的配置代码如下所示:
{
\"redeU % 3 $ f Ux\" : _ & f z i;{
&7 + N ; I Snbsp; &nbsn l K Xp; \"passes\" : [
&nbs% s j +p; &n. K T c T 3 D bsp;&n$ 7 _ K @ [bsp; \"InterDexPass\o N h J",
&a H P |nbsp; &@ p H B J J / & #nbsp; \"RegAllocPass\E c B @ ) - w R"
&nbsF 3 T Y s : 2 ; 4p; ]
&n] 0 1 0 ` y Jbsp; },
&nb+ 1 x 7 Y A Q Ssp;&np p ( 2 C obsp; \"InterDexPass\" :&nbs* / e Q K Bp;{
&nI 7 O _ bbsp; &M r p ? m 5 $ 6 6nbsp;\"minimize_cross_dex_refs\": true^ : p Z [,
X [ ( P ` \"minimize_cross_dexv I l D 5 0_refs_method_ref_weight\": 100,
&/ h d H a % 4nbsp; \"minime S r [ I 8 c X size_cross_dex_refs_field_ref_weight\": 90,
&P h A E R 9 M K qnbsp; \"minimizo - 9 ue_z ) Q !cross_dex_refs_typ, p Ie_ref_weight\": 100,
 & R c; &x a gnbsp; \"minimize_cross_dex_refs_string_rX X e l 0 N 8ef_weight\": 90
 X j + u;&nb7 i / + d B { ? Esp; },
n k H } i ^ b a&nb) o _ Z Osp; \"RegAllocPask % ss\" : {
&nbe q ,sp; \"live_range_splitting\": false
},
&ns $ ( p Q 7 pbsp;  v 4 2 o - c 7 $; 9 [ [ D ; N;\"string_sort_mode\" : \"class_order\",
\"! } xbytecode_sor) } f % it_mode\r g y ] ! } " :&nbK % 8 ` }sp;\"class_order\"
}
为了衡量优化效果,我们可以使用 Dex0 P ? $ 信息有效率 这个指标,公式如下所示:
git&nbsS q d ?p;clone https://github.com/facebook/redex.git
cL m $ | G Q i 6d&nz g ! c O Ebsp;redeU c c _ I Px
如果 Dex 有效率在t @ ! 80% 以上,就说明基本合格了。
使用 ReDex 进行分包优化、去除 debug 信息及行号信息
下面,我们就使用 Redex 来对上一步生成的 app-relv K [ H k + q g eease-proguardwithr8.apk 进行进一I T L步的优化。(macOS 环境下)
https://f| W q H *bn B B Credex.com/docs/installation
1、首先,我们需要输入一下命令去去安装 Xcode 命令行工具
xcode-select --install
2、然后,使用 homebrew 安装~ i b O redex 项目使用到的依赖库
ANDROID_SDK=/Users/quchao/Library/Android/s7 8 k 6dk redex --sign -s b [ * j 3 j & z&nk e # 0 ^ x Obsp;wan-android-key.jks -a wananc d f g / l * jdroid -p wanandroid -c @ h ?;~/Desktop/interdex_stripdebuginfo.c8 5 0 u a .onfig H B b [ ~ V ? 1 8;-P ap K g 0 fp/proguard-rules.pro -o ~/Desktop/app-release-proguardwithrq h r8-stripdebuginfo-interdex.apk ~/Desktop/app-release-proguardwithr8.apk
需要注意的是吗,2020年2月10号版本源码的 redex 需要的 b6 , A Eoost 版本为 V1.71 及x o = J H e i i以上,当你使用 brew install boost 安装 boost 时可能获取到的 boost 版本会低于 V1.71,此时可能是 brew 版本需要更新,使用 brew upgrade 去更新 brew 仓库的版本r i 8 I q y ) Z c 或者可以直接从 boost 官网下^ W ] O z V i x载最新的 b, [ | 4oov @ Nst 源码 至 /usr/local/Cellar/ 目4 f [ ( x _录下,我q e k P当前使用的是 boost V1.7.2源y N `码下载地址 中的 boost_1_72_0.zip。
https://dl.bintray.coU @ } { Gm/boostorg/release/1.72.0/sourT P C u Z r Xce/
从 深入探索 Android 启动优化 时就提及到了 Redex 的类重排优化,当时卡在这一步,所以一直没法真正完成类的重排优化。
3、接着,从 Github 上获取 ReDex 的源码并切换到 redex 目录下
git clone https://github.com/facebook/red^ _ q Vex.git
cd redex
4、下一步,使用 autoconf 和 make 去构建 Re+ 0 z * a pDex
# 如果你使用的是 gcc, 请使用 gcc-5
autoreconf -ivf && ./configure&7 B znbsp;&& make -j4
sudo make ? X y W O s c B 1;install
5、然后,配置 Redex 的 cn $ #onfig 代码
在 Redex 在运行的时候,它是根= E Y @ t t 5 :据 redex/config/default.config 这个配置文件中的通道 passes 中添加不同的优化项来对 APK 的 Dex 进行d h处理的,我们可以参考 redex/config/default.config 这个默认的配置,里面的 passes 中不同的配置项都有特定的优化。
为了优化w U U ? L { App 的包体积,我们再加上 interdex_stripdebugio t & k 4 = F ` nfo.config 中的配置项去删除 debugInfo 和减少跨 Dex 调用的情况,最终的 interdex_stripdS j ` 1 ( S 8 Bebuginfo! ( / , 1 ..config 配置代码 如下所示:
{
 b C n } 6; \"redex\"7 D N : {
\"passes\" : 7 X X # ^ & e[
&nA / Ibsp; &nb 2 q Ssp;\"StripDebuga z RInfoPass\",
&nbsx 7 u Q 5 up; = 9 y k Q @ &F & o K [ S cnbsp;\"InterDexPass\",
&nbsN H ^ | W i Qp; | r j c w _ | A h ! n i I R +&nb! k K _ 5 ?sp; &nb- & _ U a 5 y f isp; &nb: X b x D ^ x 1 Isp;\"RegAllocPase $ z ~ 1s\"
]
&nbR I (sp; &. [ u * / t t P `nbsp; },
\"StrG c J g U G ZipDebugInfoP7 G { W d Xass\" : {
 { j 9 & , X B `; \"drop_al? & U e . pl_dbg_info\" : false,
\"dQ v K irop_local_variables\" : true,
\"drop_line_numbers\" :&nbsd i 3 S zp;false,
&C l d 9nbsp; \"drop_src_files\" : false,
&n9 p N [ K i y Ibsp; &nbZ | z , C # T a nsp;\"use_whitelist\" :v z 8 j s ^ k false,
 a $ 2 X h + L V b; \"c 5 x y k J 6 Zcls_whitelist\" :&nC H P :bsp;[],
 v t T % | 1 o;&n1 : , p % I b |bsp;\"method_whitelist\" : [],
&1 L F ( D V I M -nbsp; B + B N x d 1 \"drop_prologue_end\"&nO ? ^bsp;:0 ~ g g x % 2 7 u true,
&n3 H o g O Cbsp; K z ) q { G V O W { ~ m }\"dropK n R B p @_epilogue_begiV t t H t : Y Bn\" : true,
&nb@ ! 1 M Q |sp; \"drop_all_dbg_info_if_emptyl b % o\" : true
},
&nb4 ? c w r Vsp; \"InterDexPass\" : {
\"minimize_cr2 Y F K i 3oss_dex_refs\": true,
\"minimq ( H 0 k 1 Kize_cross_dex_refs_method_ref_wey i { A ? . ;ight\": 100,
&nbJ ` ^sp;\"mind V x / limc 6 ( 3 z Xize_cross_dex_refs_field_ref_weight\": 90,
\"minimize_cross_dex_refs_type_ref_8 ( T Bweight\":&nbs8 _ c n F }p;100,
&nb` ; sp; &@ } ! U T s y ynbb Z G 6 H Gsp;\"minimize_cross_dex_refs_string_ref_weight\": 90
&nbsY = 3p; },
\"RegAllocPass\" : {u 5 $ Q
&= t `nbsp; \"live_range_splittinq + i ` H Mg\": false
&nb0 $ / ] nsp; },
\"string_sort_mode\" : \"class_order\",
&nbs. _ R dp; &nb~ H Y ] Dsp;&nT s r # 4bsp;\"bytecode_sort_mode\" : x d 7\"class_ordeM U - 4r\"
}
6、最后,执行相{ H ,应的 redex 优化命令
这里我们使用 Redex 命令对上一 Dex 优化中得到的 app_rZ A R / Aelease-proguardwithr8.apk 进行 Dex 分包优化和去除 debugInfo,它使用了贪心这种局部最优解的方式去减少跨 Dex 调用造成的信息冗余,命令如下所示(注意,在 rede^ . | 9 e hx 的前面可能5 A &需要加上 Android sdk 的路径,因为 redex 中使用到了sdk下的zipalign工具):
ANDROID_SDK=/Users/quchao/Library/Android/sdk redex --sign -s wan-android-key.jks -a wanandroid -p wanandroid -c ~/Desktop/interde; { - o = 4x_stripdebuginfo.config -P app/pro+ b J = P B ] $guard-rules.pro -o ~/Desktop/app-release-proguardwithr8-stripdebuginfo-interdex.apk ~* # 5 d ~ [ n e G/Desktop/app-release-proguardwithr8.apk
上述 redex 命令的 关键参数含义 如下所示:
- --sig4 e . Z #n:对生成的apk进行签名。
- -s:配置应用的签名文件。
- -a: 配置应用签名的 key_alias。
- -p:配置应用签名的 key_password。
- -c:指定 redex 进行 Dex 处理时需要依据的 CONF1 1 ^ X F . 4 1 ?IG 配置文件。
- -o:指定生成 APK 的全路径。
使用上面的 redex 命令我们就可以对优化g ? ? 3 E h Z后的 APK 进行 再签名和混淆,等待一会后(如* Y D K S y @ y果你的 APK 的 Dex 数量和体积很大,可能会比较久),就会生成 优化后的 APK:app-release-proguaj F p r S *rdwithr8-stripdebuginfo-interdex.apk,如下图所示:
可以看V Z x到,我们T J U @ c R V的 APK 大小几乎没有变化,这是因为当前的 APK 只有一个 Dex,并且 第一个 Dex 默认不会优化。为了能实际看到 redex 的优化效果,我们采用一个新项t N F n = & |目来进P G ~ ! A行实验,项目地址如下所示:
red/ ; # 6 L q G sex 优化 Apk 项目地址
https://github.com/AndroidAd! = / VvanceWithGeektime/Chapter22
首先,引入一大堆开源库,尝试把 Dex 数量变多一些。然后直接通过 assembleDebug, ~ $ q l # i ? 编译即可。此外,为了可以更加清楚流= O * ( B E m程,我们可以在 命令行输入 export TRAP ( i + d $ #CE=2N [ m A H 以便可以C S r & - o z L ]输出 redex 的日志。最后,我们输入下面的 redj ( k : i n yex 命令删除 dex 中的 debugInfo 和减少跨 dex 调用的情况,如下所示:
redex --sign -s ReDexSample/keystore/debug.keystore -a androiddebugkey -p android K 6 2 K / 5 k;-q * | g 9 R D -c redex-test/interdex_stripdebuginfo.config -P ReDexSample/progW c ] Juard-rules.pro -o redex-test/strip_output.apk ReDexSample/build/outputs/apk/debug/ReDexSample-debug.apk
最终,我们看到前后的 APK 体积对0 ^ 0 D O F S w |比图z O Z如下所示:
可以看到,APK 的大小从 14.2MB 减少到了 12.8MB,优} 9 G y化效果大概有10%,效果还是比较明显的。此外,如果你的 Ap x * 9 Z jp 的 Dex 数量越多,那么优化的效果就会越大。
6、三方- ^ T库处理
- 实际的开发过程中,我们会用到各种各样的三方库。尤其当项目变大之后,开发人员众多,因此引入的三方R A K ; l Z a p库也会非常多2 = 0 T +,比如说,有人引入了一个 Fresco? _ H H 图片库,然后这D G +个库你可能不熟悉,你会引入一个 Glide,并且另一个人它可能又会引入他熟悉的图片库 Picasso,所以项目中可能会存在多个相同功能的三方 SDK,这一点,在大型项目当中一定会存在。
- 因此,C { _ E t Z g `我们在做代码瘦身的时候,需要将三方库进行统一,比如说 将图片加载库、网络库、( @ o数据库以及其他基础库进行统一,去掉冗余的库。
- 同时,在选t T C择第三方 SDK 的时候,我们可以将包大小作为选择的指标之一,我们应该 尽可能地选择那些比较小的库来实现相同的功能。
- 例如,对于图片加载功能来说,Picasso、Glide、Fresco 它t J [ 7 | 8 Y 8 4们都可以实现,但是你引入 Fresco 之后会导致包大小! 5 | d 3增加很多,而 Picasso 却只增加了不到 100kb,所以引入不同的三方 SDK 对包大小的影响是不/ a ) i一样的。这里,我们可以使用 AS 插件 Android Methods Count,安装之后,它会自动在 build.gradle 文件中显示你引入的三方库的方法数。
- 最后,如果我们引入三方库的时候,I ! s 4可以 只引入部分需要的代码,而不是将整个包的代码都引入进来。
- 很多库的代码结构都设计的比较好,比如 Fresco,它将图片加载的各个功能,如 webp、gif 功能进行了剥离,它们都处于单个的库当中。如果我们只需要 Fresco 的 webp 功能,那我们可以将除 webp 之外的别: e ; , Y的库都给删掉,这样你引入的三方库就很小了,包大小就降下来了。如下所图5 y | g i ? : E所示,我们可以仅仅保留 Fresco 的 webp 功能,其它依m ; * j T赖都可以去掉。
如果你引入的三M L v d I : L R ,方库 没有进行过结构剥离,就需要 修改源码,只提取出来你需要的功能即可。
7、移除无用代码
移除无用代码时我们经常会碰到下面两个问题:
- 业务代码只增不减。
- 代码太多不敢删M a 3 %除。
这里,c o 8 W (有一个很好的方法可以 准确地判断哪些类在线上环境下用户肯定不会用到了D $ 5。我们可以通过 AOP 的方式来做,对于 Activity 来说,其实非常简单,我们只需要 在每个 Activity 的 onCre& Q ( W ( W 7ate 当中加上统计 即可,然后到了线上之后,如果这个 Activity 被统计了! f @ | i,就说明它还在被使用。
而对于那些 不是 Activity 的类,我们可以 利用 AOP 来切它们的构造函数,一个类如果它被使用,那它的构造函数肯定会被调用到。例如,下面就是 使用 AspectJ 对某个包下的类进行构造函数切面 的代码:
@After(\"execution(org.jay.launchstarter.Ta: 4 Bsk.new(..)\")
public void newObject(JoinPoint point) {
LogHelper.i(\" O # Q R v Y y 8;new \" + point.getTarget().getClass().getSimpleName());
}
其中,new 表示是 切的构造函数,括号中的 .. 表示的是 匹配所有构造参数。此外,我们也可以直接使用 coverage 插件 来做 线上无用代码分析,需要注意的是,在注册上报数据的时候记得把服务器名改为自己的。
https://github.com/bytedal s &nce/ByteX/blob/master/coverage/README-zh.md
最后,我们也可以在线下使用 Simian工具 来 扫描出重复的代码。
https://blog.csdn.net/Love66J S $ % P X k7767I e C # j 8 p 3 ~/article/details/53558m t A382
8、避免产生 Java access 方法
access 方法是什么?
为了能提供内部类和其外部类直接访问对方的私有成员的能力,又不违反封装性要求,Java 编译器A : ! g N * B在编译过程中自动生成 package 可见性的静态 access$xxx 方法,并且在需H _ S P要访问对方私有成员的地方改为] s $ M 6 调用对应的 access 方法。
主要有 两种方式 避免产生 acs 1cess 方法:
- 在开发过程中需s ] z I - 0要注意在可能产U M T生 access 方法的情况下适当调整,比如去掉 private,改为 package 可见3 K 4 o = Y F u性。
- 使用 ASM 在编译时删除生y } N 7 x { j l j成的 access 方法。
因为优化效果不E 0 q T 6 + w是很明显,这里就不多介绍了,具体的实现细节可参见 西瓜视频 apk 瘦身之 Java access 方法删除,此外,在 ReDex 中也提供了 access-ma` u _rking 这个功能去除代码中的 Access 方法,并且,在 ReDex 还有 type-erasure 的功能,它 与 access-marking 的优化效果一样,不仅能减少包大{ 9 #小,也能提升 App 的启动速度。
9/ g ~ } %、利用 ByteX Gradle 插件% p 2 | & L _平台中的代码优化插件
如果你想在项目的编译阶段去除 access 方法,这里我更加建议直接使用v b 4 + q ! r J u ByteX 的 access_inline 插件。除了 access_inlie 之外,在 ByteX 中i j @ 3 ( ` e还有 四个 很K M - ( O x H实用的代码优化 GraV ; 3 i Ndle 插件可以帮助我们有效减小 Dex 文件的大小,如下所示:
- 1、编译期间 内联常量字段:const_inline。
- 2、编译期间 移除多余赋值代码+ Z $ 4 ? z f:field_assign_opt。
- 3、编译期间 移除 Log 代码:method_call_opt。
- 4、编译期间 内联 Get / Set 方法:getter-setter-inline-plugin。
https://github.com/bytedance/W P 2 e M I 8 |ByteX
资j ; r M * H源瘦身P I W D方案探索
众所周知,Android 构建工M T b k s &具链中使用了 AAPT 工具来对资源进行处理,Manifest、Resources、s { ? )Assets 的资源经过相应的 ManifesMerger、Resoa 0 D b |urcesMerger、AssetsMerger 资源合并器将多个不同 moudule 的资源合V 8 x n m L并为了 Me! S k 1 l x v OrgedManifest、MergedResources、MergedAssets。然后,它们被 AAPT 处理后生成了 R.java、Proguard Configuration、Compiled Rek a & : osources。如下图左上方所示:
其中 Proguard Configuration、Compiled Re9 5 Q 6 5 b e E [sources 的 作用 如下所示:
Progu ] $ O F P L e Uard Configuration:这是AAPT工具为Manifest中声明的四大组件K e ~ @与布局文件中使用的各种Views所生成的混淆配置,该文件通常存放在 ${project.builL / ; 5 q 3 [ |dDir}/${AndroidPe x J % B #roject.FD_INTER[ q _MEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt。
Compiled ReY H B i tsources:它是一个ZipQ # v D 0 y 9 ^格式的文件,这个文件的路径通常为 ${project.buildDir}/${AndroidProject.FD_INTER] m DMEDIATES}/res/resouR e $ ! . = #rces-${flavorName}-${buildType}-stripped.ap_。在经过 zip 解压之后,可以发现它? 9 I c 包含了res、AndroidManifest.xml和resources.arsc 这三部分。并且,从上面的 APK 构建流程中可以得知,Compiled Resources 会被 apkbuilder 打包到 Al a Y , | -PK 包中,它其实就是 APKp d } 的资源包。
因此,我们可以 通过 Compiled Resources 文件来修改不同后缀文件资源的压缩方式来达到瘦身效果的D f 9 1 m o F J。
但是需要注意的是,resourca & V 5 i m Mes.arsc 文件最好不要压缩存储) V ` i,如果压缩会影响一定的性能,尤其是在冷启动时间方面造成的影响。并且,如果在 A8 r * ? Q y m vndroid 6.0 上开启了2 L O android:ext% h - |ractNativeLibs=”false” 的话,So 文件也不能被压缩。
文章不易d 4 Z .,如果大家喜欢这篇文章,或者对你有帮助希望大家多多点赞转发关注哦。文章会持续更新的g - S l g Q [。绝对干货!!!