
1. 包体积优化到底在优化什么1.1 从一个真实场景说起去年我接手了一个项目安装包体积从最初的 28MB 一路膨胀到了 97MB。这个数字在应用市场上不算离谱但用户下载转化率的数据很难看——每增大 10MB下载完成率大概掉 1.5 到 2 个百分点。更别提那些存储空间紧张的中低端机型用户安装时直接提示空间不足。包体积优化这件事表面上看是“把 APK 改小”实际上它牵扯到资源管理、代码组织、构建流程、甚至团队协作规范。我见过太多团队把这件事当成一次性的“瘦身运动”做完一轮就不管了结果半年后又胖回去。真正有效的做法是把它变成一套可持续的工程实践。这篇文章面向的是有一定 Android 开发经验、正在被包体积问题困扰的开发者。不管你的包是 50MB 还是 200MB下面的思路和方法都能直接拿去用。我会从整体设计思路讲起然后逐层拆解代码、资源、Native 库、动态交付这几个维度最后给出一套可复现的实操流程和排查经验。1.2 先搞清楚体积构成再动手很多人一上来就开始删资源、开混淆这是典型的“还没诊断就开药”。我的习惯是先用工具把 APK 拆开看搞清楚每一部分占了多少。Android Studio 自带的APK Analyzer是最顺手的入口。把 APK 拖进去你能看到几个关键数据DEX 文件大小、res 目录、assets 目录、lib 目录、resources.arsc、META-INF 等。一个典型的膨胀型 APK问题往往集中在三四个地方。我经手过的一个项目拆开之后发现组成部分大小占比DEX 文件32MB33%libNative 库28MB29%res assets24MB25%resources.arsc6MB6%其他7MB7%这个分布很典型。DEX 大通常意味着代码量大或者依赖库太多lib 大往往是引入了多个 ABI 的 so 文件res 和 assets 大则是图片、音频、字体这些没管好。注意APK Analyzer 看到的是压缩后的大小而用户下载时看到的是应用市场重新压缩后的体积两者会有差异。真正要关注的是“下载体积”和“安装后占用”两个指标前者影响转化率后者影响用户体验。1.3 优化的优先级怎么排搞清楚构成之后就要排优先级。我的原则是先做收益大、风险低的再做收益大、风险高的最后做收益小但能长期维持的。具体来说资源优化图片压缩、冗余资源清理通常收益大且风险低应该最先做。代码混淆和 R8 优化收益中等但需要仔细配置规则风险可控。Native 库裁剪收益大但涉及 ABI 取舍需要评估用户设备分布。动态交付App Bundle、动态特性模块收益最大但改动也最大适合作为中长期方案。这个排序不是绝对的。如果你的项目 Native 库占比特别高那就应该优先处理 so 文件。关键是先看数据再决定动哪里。2. 代码层面的瘦身从 DEX 到依赖治理2.1 R8 到底帮你做了多少事从 AGP 3.4 开始R8 就取代了 ProGuard 成为默认的代码压缩和混淆工具。很多人对 R8 的理解还停留在“混淆类名方法名”其实它做的事情远不止这些。R8 的核心能力包括代码压缩Tree Shaking把没有被引用到的类、方法、字段删掉代码混淆Obfuscation把有意义的类名方法名替换成短名称代码优化Optimization比如内联短方法、删除无用参数、合并类等。我实测过一个项目开启 R8 完整优化后DEX 体积从 32MB 降到了 21MB降幅超过 30%。这个收益非常可观而且几乎是“免费”的——只要配置好 keep 规则就行。但这里有个坑很多团队为了省事在 proguard-rules.pro 里写了一堆-keep规则把整个包都保留下来。这等于把 R8 的手脚捆住了。我的建议是keep 规则要精确到具体的类和方法不要用通配符大面积保留。# 不好的做法整个包都保留 -keep class com.example.** { *; } # 好的做法只保留必要的类和方法 -keep class com.example.model.UserBean { *; } -keepclassmembers class com.example.network.ApiResponse { public init(...); }2.2 依赖库是隐藏的增肥元凶代码膨胀的另一个大头是第三方依赖。我见过一个项目引入了某个“全能工具库”结果这个库又传递依赖了七八个其他库最后光是这一个依赖就贡献了 4MB 的 DEX 体积。排查依赖树的方法很简单在终端里跑./gradlew :app:dependencies --configuration releaseRuntimeClasspath输出会是一棵很大的树。你要重点关注两类一是重复功能的库比如同时引入了 OkHttp 和另一个网络库二是传递依赖很多库会悄悄带进来你根本用不到的东西。我的处理策略是能用官方库就用官方库体积通常更可控引入大库之前先看它的依赖树评估传递依赖的体积对于只需要一两个功能的库考虑自己实现或者找更轻量的替代用exclude排除掉不需要的传递依赖implementation(com.example.heavylib:core:1.0.0) { exclude group: com.example.unnecessary, module: unnecessary-module }实操心得每次引入新依赖后都跑一次 APK Analyzer 看看 DEX 涨了多少。养成这个习惯能避免“温水煮青蛙”式的体积膨胀。2.3 代码层面的其他优化点除了 R8 和依赖治理还有一些细节能抠出体积。枚举的替代Java 枚举在 DEX 里占用的空间比静态常量多不少。对于简单的状态标记用IntDef或StringDef注解配合静态常量能省下一些空间。不过这个收益通常不大除非你的项目里有大量枚举。内联函数Kotlin 的 inline 函数能减少方法调用开销但过度使用会增加代码体积。这个需要权衡不能一概而论。移除调试代码确保 release 构建里没有 Log、Timber 等日志库的调用。可以用 R8 规则直接删掉-assumenosideeffects class android.util.Log { public static *** d(...); public static *** v(...); public static *** i(...); }避免重复的匿名内部类这个比较细节但在大项目里累积起来也不少。能用 lambda 的地方就用 lambdaR8 会更好地处理。3. 资源优化图片、冗余资源和 resources.arsc3.1 图片资源是最好啃的骨头资源目录里图片通常占大头。我见过一个项目res/drawable 下有 2000 多张 PNG其中很多是历史遗留的、根本没人用的。第一步是清理无用资源。Android Studio 的 “Remove Unused Resources” 功能可以扫出没有被引用的资源。但要注意它只能识别代码和 XML 里的直接引用通过反射或字符串拼接引用的资源它识别不到。所以清理之前一定要仔细核对最好配合 Lint 检查。第二步是压缩图片。PNG 可以用工具转成 WebP体积通常能减少 30% 到 70%。Android 从 4.0 开始支持 WebP现在几乎没有兼容性问题。有损 WebP 的压缩率更高但要注意画质。# 用 cwebp 批量转换 for file in *.png; do cwebp -q 80 $file -o ${file%.png}.webp done第三步是处理不同分辨率的图片。很多项目在 drawable-hdpi、xhdpi、xxhdpi 里放了同一张图的不同尺寸版本。如果这张图本身不大或者可以用矢量图替代就没必要放多个版本。矢量图VectorDrawable是另一个利器。简单的图标用矢量图体积可能只有 PNG 的十分之一。但要注意复杂的矢量图在低端设备上渲染性能会差一些而且矢量图不支持所有 SVG 特性。3.2 assets 目录里藏着什么assets 目录经常被忽视因为它的内容不参与资源索引Lint 也检查不到。但这里往往藏着大文件离线地图数据、预置的数据库、字体文件、音频视频等。我的做法是定期审计 assets 目录问三个问题这个文件是必须的吗能不能改成按需下载能不能压缩字体文件是个典型例子。一个中文字体动辄 5MB 到 10MB如果只是用了几十个字完全可以用字体子集化工具裁剪。英文和数字字体通常只有几百 KB问题不大。预置数据库如果很大可以考虑拆分成多个小文件或者改成首次启动时下载。离线地图数据同理。3.3 resources.arsc 的优化resources.arsc 是资源索引表它的大小和资源数量、配置维度有关。优化它的方法主要有两个。减少资源配置维度。如果你的项目支持了几十种语言但实际用户只集中在几种语言上可以考虑裁剪。同样屏幕密度、API 版本等维度也可以精简。开启资源混淆。用 AndResGuard 这类工具可以把资源路径缩短比如把res/drawable/ic_launcher.png变成r/d/a.png。这能显著减小 resources.arsc 和 APK 整体体积。// AndResGuard 配置示例 andResGuard { mappingFile null use7zip true useSign true keepRoot false whiteList [ R.drawable.icon, R.string.app_name ] compressFilePattern [ *.png, *.jpg, *.jpeg, *.gif ] }注意资源混淆后如果代码里有通过字符串拼接访问资源的地方可能会出问题。上线前一定要充分测试。4. Native 库与动态交付进阶优化手段4.1 so 文件的 ABI 取舍Native 库的体积问题核心在于 ABI应用二进制接口。一个 so 文件通常需要为 armeabi-v7a、arm64-v8a、x86、x86_64 四个架构各编译一份。如果每个架构的 so 加起来是 7MB四个架构就是 28MB。现在的设备绝大多数是 arm64-v8aarmeabi-v7a 还有少量存量x86 系列基本只在模拟器上用。所以常见的做法是只保留 arm64-v8a 和 armeabi-v7a能省掉将近一半的体积。android { defaultConfig { ndk { abiFilters arm64-v8a, armeabi-v7a } } }如果对体积要求极致可以只保留 arm64-v8a。但这样会丢失一部分老设备用户需要根据你的用户设备分布数据来决定。App Bundle是更好的方案。上传 AAB 到应用市场后市场会根据用户设备架构只下发对应的 so 文件用户下载的体积自然就小了。这是 Google 主推的方案国内主流市场也在逐步支持。4.2 动态特性模块的拆分逻辑动态特性模块Dynamic Feature Module允许你把部分功能拆出来用户用到的时候再下载。这适合那些使用频率低、体积大的功能比如某个特定的编辑工具、离线包、或者某个垂直场景的完整功能。拆分的逻辑不是“哪个大拆哪个”而是要看功能的内聚性和使用频率。一个功能如果 90% 的用户从来不用那它就很适合拆出去。如果一个功能虽然大但 80% 的用户每天都要用拆出去反而增加加载等待体验会变差。我经手的一个项目把“视频编辑”功能拆成了动态模块主包体积直接降了 18MB。这个功能的使用率只有 12%拆出去之后主包下载转化率明显提升而真正需要这个功能的用户多等几秒下载模块也能接受。4.3 按需加载与资源下发除了动态特性模块还有一些更轻量的按需加载方案。资源下发把一些不常用的图片、音频、配置放在服务器上首次使用时下载并缓存。这适合那些体积大但使用频率低的内容。插件化把整个功能模块做成插件运行时加载。这个方案技术复杂度高维护成本也大适合大型项目。小项目用动态特性模块就够了。分阶段加载把启动时必须的资源放在主包其他资源按页面或功能分批加载。这个需要配合资源管理框架来做。实操心得动态交付虽然能大幅减小主包体积但会增加代码复杂度。拆分之前一定要评估维护成本不要为了优化而优化。我见过一个项目拆了十几个动态模块结果构建时间翻倍团队怨声载道。5. 完整实操流程与参数计算5.1 从诊断到优化的完整步骤下面是我在实际项目中总结的一套流程可以直接照着做。第一步建立基线。在优化之前先记录当前 APK 的各项数据总大小、DEX 大小、lib 大小、res 大小、assets 大小、resources.arsc 大小。同时记录构建时间、启动时间等性能指标方便对比。第二步APK Analyzer 拆解。把 APK 拖进 Android Studio 的 APK Analyzer逐层展开找出体积最大的几个文件和目录。重点关注最大的 DEX 文件、最大的 so 文件、最大的图片和 assets 文件。第三步依赖树分析。跑./gradlew :app:dependencies找出体积大或者传递依赖多的库。对于每个大依赖评估是否真的需要有没有更轻量的替代。第四步资源审计。用 Lint 和 “Remove Unused Resources” 扫出无用资源。用脚本统计各目录下图片的数量和总大小找出可以压缩或转 WebP 的。第五步配置 R8。检查 proguard-rules.pro精简 keep 规则。开启完整的 R8 优化选项。第六步ABI 裁剪。根据用户设备分布决定保留哪些 ABI。如果支持 App Bundle优先用 App Bundle。第七步动态交付评估。评估哪些功能可以拆成动态模块计算拆分后的收益和维护成本。第八步验证与回归。优化后跑完整的测试用例确保功能没有受影响。对比优化前后的体积和性能数据。5.2 参数计算ABI 裁剪能省多少假设你的项目有四个 ABI 的 so 文件每个架构的 so 总大小如下ABIso 总大小arm64-v8a7.2MBarmeabi-v7a6.8MBx867.5MBx86_647.8MB四个架构合计 29.3MB。如果只保留 arm64-v8a 和 armeabi-v7a体积降到 14MB节省 15.3MB。如果只保留 arm64-v8a体积降到 7.2MB节省 22.1MB。但要注意APK 里的 so 文件是压缩存储的实际节省的体积会略小于这个数字。而且如果用了 App Bundle市场下发时只会包含用户设备对应的 ABI用户下载体积本来就只有一份 so 的大小。5.3 图片压缩的收益估算假设 res/drawable-xxhdpi 下有 500 张 PNG平均每张 80KB总计 40MB。转成有损 WebP质量 80后平均每张降到 25KB总计 12.5MB节省 27.5MB。如果其中有 100 张是简单的图标可以转成矢量图每张从 80KB 降到 2KB又能节省 7.8MB。当然这是理想情况实际项目中图片的压缩率会有差异。但即使打个对折收益也很可观。6. 常见问题与排查技巧实录6.1 优化后应用崩溃了怎么办这是最常见的问题。优化后崩溃通常有几个原因。R8 混淆导致反射失败。如果代码里有反射调用的类或方法而它们被混淆了就会找不到。解决方法是给这些类加上 keep 规则。排查方法是看崩溃堆栈找到混淆后的类名用 mapping.txt 反解出原始类名。资源混淆导致资源找不到。如果用了 AndResGuard而代码里有通过字符串拼接访问资源的地方就会出问题。解决方法是在 whiteList 里保留这些资源或者改成用 R 类访问。so 文件缺失。如果裁剪了 ABI而某些设备正好用的是被裁掉的架构就会崩溃。排查方法是看崩溃设备的 ABI确认是否在保留列表里。避坑技巧每次优化后一定要在多种设备上测试包括低端机、不同 Android 版本、不同 ABI。有条件的话用云测平台跑一轮。6.2 优化效果不明显是什么原因有时候做了一堆优化体积只降了一点点。这通常是因为没有找到真正的“大头”。我的排查思路是回到 APK Analyzer看优化后各部分的占比变化。如果 DEX 还是很大说明 R8 没配好或者依赖没治理干净。如果 lib 还是很大说明 ABI 裁剪没生效或者 so 本身就没法再小。如果 res 还是很大说明图片压缩不彻底或者还有大量无用资源。另一个常见原因是优化被其他增长抵消了。比如你删了 5MB 资源但同期又引入了 3MB 的新依赖净效果就只有 2MB。所以优化期间要控制新代码和新资源的引入。6.3 常见问题速查表问题现象可能原因排查方法解决方案优化后崩溃R8 混淆反射类看堆栈 mapping 反解加 keep 规则资源找不到资源混淆看崩溃日志的资源名加 whiteListso 加载失败ABI 裁剪看设备 ABI调整 abiFilters体积降幅小没找到大头APK Analyzer 对比重新分析构成构建变慢动态模块太多看构建日志合并模块启动变慢资源加载变复杂看启动耗时优化加载策略6.4 我踩过的几个坑坑一过度依赖 Remove Unused Resources。这个工具会误删通过反射引用的资源。我有一次直接用它清理结果上线后某个功能崩溃排查了半天才发现是资源被删了。后来我养成了习惯清理前先跑一遍 Lint把反射引用的资源加到 keep 列表里。坑二WebP 转换后画质下降。有损 WebP 的质量参数设得太低图片会出现明显噪点。我的经验是照片类图片质量设 80 到 85图标类设 90 以上或者直接用无损 WebP。坑三动态模块拆分太细。有个项目拆了 15 个动态模块结果构建时间从 3 分钟涨到 12 分钟而且模块间的依赖管理变得很复杂。后来合并成了 5 个情况才好一些。坑四忘了更新基线。优化做完后没有及时更新体积基线结果几个月后体积又涨回去了而且没人发现。后来我们在 CI 里加了体积检查超过基线就报警。7. 把优化变成可持续的工程实践7.1 建立体积监控机制一次性的优化只能管一时要长期维持必须建立监控机制。我的做法是在 CI 流程里加一个体积检查步骤。每次构建 release 包后自动记录 APK 大小和各组成部分的大小和基线对比。如果涨幅超过阈值比如 2%就发通知给相关负责人。# 简单的体积检查脚本示例 APK_SIZE$(stat -c%s app-release.apk) BASELINE50000000 THRESHOLD1.02 if [ $APK_SIZE -gt $(echo $BASELINE * $THRESHOLD | bc) ]; then echo 警告APK 体积超过基线 2% exit 1 fi这个脚本很简单但效果很好。它能让团队对体积变化保持敏感避免不知不觉中膨胀。7.2 制定团队规范除了技术手段团队规范也很重要。我们团队定了几个规矩引入新依赖前必须评估体积影响超过 500KB 的需要说明理由图片资源必须用 WebP 或矢量图PNG 需要说明原因新增 assets 文件需要评估是否可以用按需下载每次发版前跑一次体积检查记录变化这些规矩看起来琐碎但执行下来效果很明显。半年时间里我们的包体积不仅没涨还降了 8MB。7.3 持续优化的方向包体积优化没有终点。随着业务发展新的功能、新的资源会不断加入体积压力会一直存在。我的建议是把优化当成一个持续的过程而不是一次性的项目。每个版本都花一点时间看看体积变化及时处理异常。同时关注新技术和新工具比如 App Bundle 的进一步优化、新的图片格式、更高效的资源管理方案。最后分享一个小技巧我习惯在每次发版后把 APK 拖进 APK Analyzer 看一眼。这个动作只需要两分钟但能让我对体积变化保持敏感。很多问题都是在早期发现的等到体积涨了几十 MB 再处理成本就高多了。