研发在使用流程(CI/CD)平台进行提测和集成时,都首先需要触发apk构建,franky-plugin作为包大小分析工具在构建期的一款gradle插件,会收集数据并将结果输出到构建产物。接下来,流程平台中的卡口插件负责收集apk和franky-plugin生成的文件,并上传到卡口平台备用。
之后,流程平台会执行准入检测,其中包大小卡口检测会触发能力平台中的包大小分析任务,通过调用franky对应的命令行工具,生成json格式的包大小分析报告。通过解析分析报告,并与预先设置的团队阈值和Buffer值进行对比,以此判定提测/集成单中包含模块所在的团队(1个或多个)是否超限。流程平台中的准入检测在获取检测结果后,通过或者阻断提测/集成流程。如果卡口未通过,可以通过进行瘦身改造来使卡口通过,但这一般无法在当前版本完成,这时可以申请带时限的Buffer来临时通过卡口,从而完成提测/集成。
瘦身技术前面讲了很多治理模式相关内容,看起来可能有些抽象,接下来会回到具体的瘦身技术,侧重点不在于深入技术原理和实现细节(大多会给出参考链接),而是尝试将每一项瘦身技术的优缺点进行一次概括性讲解和巡展,便于形成一个整体认知,在对具体app进行瘦身时,能够根据实际情况进行选择和优先级安排。
瘦身技术,顾名思义,就是指可以用于包瘦身的任何技术(方案),按照所需技术、生效阶段、影响范围综合评判,将其划分为以下三种类型:
4.1 远程化
远程化是指将原本在apk中的功能,剥离出来放到服务器,app运行时进行下载、加载等一系列动作后,才能够正常使用功能的一种技术方案,其核心特点可以归纳为“本地剥离,远程下载”。远程化瘦身效果显著,也因此容易为了追求瘦身结果而被过度使用,但这并不是远程化本身的原罪,实际上一些边缘、非核心、实验性业务,都比较适合进行远程化改造。远程化框架涉及的关键技术,在业内已经有很成熟的解决方案,但是具体到代码实现,还是有不少需要仔细思考和反复打磨的地方,例如:apk构建体系的兼容性是否足够广泛,远程化改造的代码限制和改造难度是否足够低,app唤端、多进程、用户磁盘占用、下载线程占用、apk升级复用、下载带宽成本等。
按照远程化元素类型,以及业界普遍使用情况,将其分为远程so、远程bundle、远程资源三种。
首先来看远程so。动态链接库so与其它代码的耦合度低,在apk中具有较强的独立性,同时占用apk体积相对较大,因此单独将so进行远程化往往具有很高的瘦身投入产出比。
第二种远程bundle,一般是指可以完整的将一块功能进行远程化,远程部分相当于一个迷你apk(dex、resource、so)。远程bundle相关技术“历史悠久”,动态化、插件化、组件化等虽然有语境、功能以及设计思想上的区别,但在技术上有着很多相似的地方,随着新版本os加强了对系统API调用、拦截和替换等方面限制,这个领域的主流技术方案逐渐演变为对系统侵入越来越轻量的方向。相对于远程so,远程bundle在实现上要复杂很多:在运行时阶段,虽然对系统API的侵入性比较小,但是对唤端、组件路由跳转、后台Activity销毁重建等情况依然需要小心处理;在构建阶段,由于要“分离”出一个迷你apk,因此对构建体系的兼容非常困难,目前业界有一些同类框架,很多时候并不是运行时无法满足需求,而是在构建侧无法做到很好的兼容性和易用性。
最后一种远程资源,是指针对资源文件的远程化。可以通过将资源上传到文件托管平台,获取文件url后直接下载并使用(优酷采用的就是这种方式)。远程资源的实际应用场景较少,投入产出比也不高,所以目前优酷没有专门研发一个这样的框架。当然,如果有大量资源需要进行这种远程化改造,那可能有必要开发一个专用框架。
4.2 整包瘦身
整包瘦身,是指在apk构建阶段整体进行处理的一类瘦身技术,对全部apk元素均可生效(包括无源码的二、三方sdk),新增代码也可以立刻得到同样的处理,其核心特点可以归纳为“中间拦截,整体生效”。也正是由于上述特点,这类瘦身的影响范围较广,因此在首次应用到app时,如何控制好验证成本和线上风险变得非常关键,当然在瘦身效果上,一般可以立竿见影的获得较大收益。自定义的一些整包瘦身方案,往往容易出现处理逻辑考虑不周全而导致的稳定性问题,注意这不属于技术方案本身的特点,而是具体实现代码的问题。在工程效能方面,对代码质量无影响,构建耗时则一般会有增加,有些整包瘦身技术会改变apk中目标元素形态,因此对各类相关问题分析会带来一定程度的效率降低。
这里划分了14项整包瘦身技术,其中Android官方没有提供的能力,都已经沉淀到了优酷自研gradle plugin中,目前正在开源筹备中。
- 【代码】Proguard/R8。利用Proguard工具,对java代码进行裁剪、混淆、优化处理,从而实现无用代码删除、符号(类、变量、方法)混淆、代码逻辑优化,包体积降低效果非常显著。值得注意的是,google官方已经在近几年的Android Gradle Plugin中,使用自研的R8替代了java领域传统的Proguard工具,裁剪和优化效果更为强大,进一步压缩包大小的同时处理耗时更低,优酷从proguard切换到R8后,包大小降低约4.8%(3.2MB),构建耗时降低约25%(2min),当然这也和原progaurd的全局配置有关,尤其是优化次数-optimizationpasses。
- 【代码】D8。在apk构建过程中,java代码需要经历由jvm字节码到dalvik字节码的转换处理,DX/D8就是承担这个责任的工具。在优酷的具体实践中,由DX升级到D8后,包大小降低约9.5%(9.7MB),由于额外对dex合并进行了优化,dex数量降低,导致包大小收益出现一次跃升,因此比官方给出的Benchmark收益5%要更高。
- 【代码】R类合并。将所有<模块package>.R类移除,并将java代码中对前者的引用统一替换为.R类,以此来降低包大小的一种技术手段。在优酷当前情况下(模块800多个,dex24MB),R类裁剪可以减少80万个java类Field,带来近5MB包大小收益。由于每个dex中Field数量也受到65536限制,因此Field数量大幅减少所带来的dex数量减少,是瘦身收益的主要来源。进一步,可以把所有java代码中R..的引用,也全部替换为对应id值,这样.R类也可以删除,但是在已经完成R类合并的情况下,这个处理的收益比较有限,因此优酷并没有实际投入研发和使用,但是如果追求极致瘦身确实可以这么做!
- 【代码】Dex排布优化。Dex排布优化是指通过合理安排dex中包含的类,从而尽可能减少常量池冗余度以及dex数量,进而降低dex整体大小的一种瘦身技术。由于历史原因,Dalvik字节码中调用method和field指令的操作数是16位,因此一个dex中method和field数量上限均为65536,而现代app一般都会包含多个dex,dex数量过多会导致各类常量池冗余度变高,从而导致包大小增加。事实上,Dex排布优化不仅可以用于降低包大小,还可以通过选择不同的优化策略,来提升app运行时的性能,Facebook的Redex即是这一领域的著名开源框架。优酷并没有使用复杂的排布优化策略,而是自定义了简单的Dex合并能力,获得了约2MB左右的包瘦身收益。
- 【代码】字节码指令精简。准确的说这并不是一项瘦身技术,而是一类瘦身技术的集合。通过更精细的字节码上下文分析,以删除、合并、转换等方式精简指令序列,从而达到瘦身目的,例如删除冗余赋值指令(值与类型默认值一致)、access$xxx方法消除(修改private方法为public,避免access方法生成)、常量/短方法内联等等。不知道大家是不是会有个疑问,proguard/R8没有进行这些处理吗?这些“民间”自定义的字节码指令精简方案,可以看作是对前者的一种“极致性”扩展,因为前者在进行字节码优化时对正确性的要求极高,如果有些优化策略存在风险,或者违背原代码设计意图(比如修改private方法为public),那么就不会应用。当需要使用自定义的字节码指令精简之前,建议先把proguard/R8各种优化配置选项研究透彻并充分应用,可能你会发现通过配置就可以实现同样效果,并且处理过程更稳定、高效、可信,如果不得不走到需要进行自定义处理的境地,也一定要谨慎使用。
- 【资源】无用资源裁剪。ShrinkResources[3]是Android官方提供的无用资源裁剪功能,在apk构建时直接对无用资源进行删除。在app中除了通过http://R.xxx.xxx/0xffxxxxxx显式引用资源,还可以通过Resources.getIdentifier在参数中指定资源名称来引用,由于后者可以拼接甚至是动态下发字符串,因此会导致此类资源的引用关系无法被准确获取,对此ShrinkResources提供了两种模式:严格模式、正常(默认)模式。严格模式仅考虑显式引用关系,正常模式则会采用“安全优先原则”,如果资源名称以任何java代码中的常量字符串为前缀,那么会被标记为疑似引用而无法得到删除,还有其它几种“疑似性标记逻辑”不再列举。
- 【资源】多维度(备用)资源裁剪。Android资源可以配置不同维度,从而在运行时灵活适配各种不同情况(语言、屏幕尺寸、屏幕横竖状态、os版本等),这里的多维度(备用)资源裁剪,就是在apk构建期将不需要的维度裁剪掉,AndroidGradlePlugin提供了对应功能,通过android DSL配置(resConfigs)[4]即可直接完成。自研代码一般只会包含需要用到的资源,而二、三方sdk为了提高兼容性会包含尽可能多的维度,在优酷实践中对语言维度进行了裁剪(仅保留中文),瘦身收益约3%(2MB)。
- 【资源】图片压缩。图片压缩,是指在apk构建期批量对图片进行压缩,或者格式转换的一种瘦身技术。优酷自研turbo-plugin中包含了这项功能,在处理上的考量包括:提供配置项对压缩质量(quality)进行设置,这决定了压缩率(图片有损程度);有些图片压缩后,尺寸反而会变大,通过检查压缩结果,当发现这种情况时对这些图片不使用压缩。在优酷实践中,图片压缩带来的瘦身收益约0.8MB,收益较低的原因,主要是有很多模块中的图片,提前已经进行了压缩处理。
- 【资源】resources.arsc压缩。resources.arsc文件在Android原生构建流程中不会进行压缩,而运行时os识别到resources.arsc被压缩后,存在兼容逻辑对其进行解压处理,所以可以通过压缩resources.arsc来进一步降低包大小。需要注意的是,os在运行时解压缩resources.arsc会导致资源查找耗时增加,官方也不建议这么做,并且当apk的targetSdkVersion大于等于30时,无法在Android11[5]及以上设备中安装。
- 【资源】去重。对于值相同的不同名资源仅保留一份,删除重复资源并将所有引用到的地方替换为保留下来的资源。和ShrinkResources一样,在应用这项技术时依然需要注意,通过Resources.getIdentifier以资源名称作为参数方式使用到的资源,不能进行去重处理。优酷曾经使用到了这项技术,瘦身收益在MB级别,现在已经通过对应的「单点瘦身」方案,在源码层面直接处理。
- 【资源】混淆。和java代码Proguard混淆类似,是通过缩短资源名称以及文件类型资源存放路径,实现包瘦身的一种技术方案。AndResGuard[6]是实现这项瘦身技术的一套开源框架,功能相对成熟且完备,后来也有些一些同类框架在基础的资源名称缩短之上,又进一步对resources.arsc、xml资源等进行了更精细化的瘦身处理,具体可以参考相关公开文章。在应用这项技术时,依然需要注意处理Resources.getIdentifier带来的问题。
- 【so】debug信息裁剪。debug信息裁剪,是指将so中携带的debug信息删除掉,这并不会影响so正常功能,只是会导致无法源码调试so。在Android官方apk构建过程中,默认有一项StripDebugSymbol的处理逻辑,正是用于对debug信息进行裁剪,之所以作为一项整包瘦身技术放在这里,是因为如果构建环境中未包含NDK(或者NDK未在可识别路径),那么这项处理就不会执行,但是apk还可以正常生成,这一点需要特别注意。
- 【so】abi分包。abi(application binary interface)是指应用二进制接口,不同Android设备使用不同的CPU,而不同CPU支持不同的指令集,CPU与指令集的每种组合都有专属的应用二进制接口 (ABI)。在Android生态中arm CPU是绝对主流,指令集按支持的CPU(指令寻址)位数可分为32位(armeabi、armeabi-v7a)和64位(arm64-v8a)两大类,32位设备只能运行32位的so,64位设备既可以运行32位so也可以运行64位so。虽然目前市场中64位设备已经成为绝对主流,但32位设备也还没到可以舍弃的量级,因此apk如何同时支持64和32位设备就成了一道选择题:合包,即apk中同时包含32和64位两套so;分包,即分为32位和64位两个apk,各自仅包含一套对应的so。显然,后者可以极大减小包体积,但也会带来app分发的一些问题,需要辅以额外处理逻辑。对于分包方案,在apk构建时应该保障一次构建直接生成两个分包的方式,这样可以避免多次构建不一致带来的32位和64位包代码差异问题。
- 【apk】7z压缩。7z是一种压缩格式,同时也是一个压缩工具。这里的7z压缩是使用7z工具替代Android工具链,使用可以被Android系统所兼容的压缩算法,对apk中(本质就是一个zip文件)原本就会压缩的文件进行效果更好的压缩处理,从而实现瘦身的一种技术方案。由于并未改变apk元素内容值本身,因此基本无需验证即可稳定上线使用。在优酷的实践中,包大小降低约4%(3.5MB)。
4.3 单点瘦身
单点瘦身,是指在源代码层面,通过去除无用、合并冗余、修正不合理等方式实现瘦身,其核心特点可以归纳为“源头处理,轻爽健康”。由于需要在源码级别操作,因此只能针对有源码工程的自研代码,对于无源码的二、三方SDK则无法实施(其实也可以在字节码层面改造sdk,非常规方案)。另外,之所以称为“单点”瘦身,是因为需要对每一个具体的可瘦身点进行改造、验证并上线,因此最好是对代码最熟悉并负责的同学直接上手改造,这类瘦身的应用难度整体较低,但是涉及研发同学范围很广,改造周期通常也非常之久,同时在瘦身效果上一般会比较缓慢。
这里划分了9个单点瘦身技术,在优酷自研的包大小分析工具中,均实现了对应的检测分析能力,具体可以参考前文「分析技术」章节,这里简单列出:
- 【代码】线上无用类
- 【资源】超大
- 【资源】无用
- 【资源】多维度
- 【资源】无透明度png图片
- 【资源】相似
- 【so】静态链接C STL
- 【so】链接非标准C STL
- 【so】无用导出符号
另外,无用和冗余的去除,本身就是一种代码质量的提升,也可以明显降低工程腐化程度,同时对构建耗时也会有正向收益。
还能做些什么包瘦身是移动app领域长期存在的一个工程问题,无论是否关注和治理,其影响始终客观存在。接下来聊聊一些相关的思考,希望能够给感兴趣的同学带来一些有价值的参考和启发。
5.1 决心
任何新需求迭代几乎不可能做到0代码增加,因此包大小天然是一个与代码增量“对抗”的事情,但又不像稳定性、性能一样可以产生立即、直接的影响,所以在写代码时很容易被忽视。如果包瘦身的重要性并没有在app全开发团队上下,获得一致性的认可以及足够的决心,即使相关技术、卡口能力、治理策略再怎么完善,也无法在这场“包瘦身持久战”中始终利于不败之地。
前文所属的常态化治理模式下,各种技术支撑以及治理策略,究其本质都是为了将“对包大小的考量”融入到每一名研发同学的代码思维中,这样才能够在coding阶段就尽可能减少包大小不友好代码的产生。“不产生”比“产生了再治理”,在对研发同学技术能力的要求上,恐怕要高出不止一个段位。在追求卓越工程师的路上,不妨把代码对包大小的影响也纳入进来吧。
5.2 以包大小为支点
“穷则独善其身,达则兼济天下”,当包瘦身治理已经处于良好的常态化治理局面时,由于包瘦身本质还是对app工程中不合理代码的改进,因此不妨以包大小为支点,撬动用户体验、工程(代码)质量等其它方面的提升。各种以瘦身作为“导火索”的代码清理、优化、改造,实际上是对app整体工程和代码健康度的有效提升,也是促使业务间功能复用的重要推动力量。而这些代码和业务功能设计层面的提高,长期来看也会对app稳定性、性能、研发效率等的全面提升,具有很好的促进作用。
5.3 探索实践永不止步
虽然优酷的包大小治理,已经处于可持续的常态化治理模式,但瘦身相关的技术探索,以及现有技术的完整落地实践,还没有到结束的时候。很多存量技术问题仍有待挖掘,例如:对于动态链接库so的检测分析技术,还有不少可以探索的方向;对于混淆规则精简,如何能够提供更有效的辅助工具,进一步降低分析、改造、验证的成本和风险,也是一件很有挑战的事情;对于各种中间件,如何能够作出对包大小更友好的设计和迭代,这也已经超出个人、单个组织所能够完成的范围,但是如何能够对此带来更好的影响和改变也值得思考。新技术的趋势和影响也需要及时关注:AndroidX包含的新组件、新开发模式,各种手机厂商的特色能力sdk不断引入等等,都会带来新的机遇和挑战。
参考文档:
[1]https://baijiahao.baidu.com/s?id=1661744084264876433
[2]https://baijiahao.baidu.com/s?id=1661744084264876433
[3]https://developer.android.com/studio/build/shrink-code#shrink-resources
[4]https://developer.android.com/studio/build/shrink-code#unused-alt-resources
[5]https://developer.android.com/about/versions/11/behavior-changes-11#compressed-resource-file
[6]https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md
作者 | 谦风
原文链接:http://click.aliyun.com/m/1000346560/
本文为阿里云原创内容,未经允许不得转载。