1. 背景
从用户出发:为什么要做包体积压缩?
故事的起点是一场线下推广会。当大屏幕上展示出 App 的下载二维码后,我们发现不少用户只是拍了张照片保存下来,并没有当场下载安装。追问原因,回答很朴实------"流量不够,不想下 100 多 MB 的东西。"
这个场景非常直观:用户可能因为流量不足、手机存储不够,或者单纯是下载时间太长而放弃安装。而下载安装本身是用户动线的最前置环节------如果这一步流失了,后面的注册、浏览、深度使用全部归零。在相同的推广预算下,降低这一步的"摩擦力",就能让更多用户涌入。

围绕这个问题做了一些调研,得出几个关键结论:
- Google 在 2019 年的实验表明,APK 每减少 10MB,全球平均下载转化率提升约 1.75%
- 下载转化率 = 完成安装并访问应用的用户数 / 触发下载行为的用户数。降低包体积意味着相同预算下能获得更多装机收益
- 降低包体积还能提升更新率------老用户也更愿意及时升级,有利于新业务快速落地
- 团队此前已在主客户端完成过类似优化(111MB → 66MB),有成熟经验可复用

2. 现状分析
2.1 行业数据
截止 2025 年 2 月,我们对比了业内几个主流电商平台的商家端 App 安装包大小。在相同网络环境下实测,体积最小的竞品从下载到打开只需 10 秒出头,而我们的应用超过 1 分钟------对比十分鲜明。
| 商家平台 | 安装包大小(MB) | 对应主客户端大小(MB) |
|---|---|---|
| 待优化应用 | 103 | 63 |
| 竞品A | 25 | 25 |
| 竞品B | 165 | 61 |
| 竞品C | 88 | 104 |
| 竞品D | 95 | 271 |
2.2 近况分析
整个 2024 年,我们的商家版应用安装包一直维持在 103MB 左右。好消息是业务功能已相对齐全、包体增速缓慢,正适合对各业务模块进行拆分和压缩:

3. 目标制定
基于 2024 年 12 月的版本,计划在 2025 年 2 月发布新版本:
- 安装包体积从 103MB 压缩至 65MB 左右
- 预计提升 7%~10% 的下载转化率
- 由于体积降低,启动速度等相关性能指标也相应提升
- 在上述目标达成的同时,尽可能降低 crash 率
4. 方案设计
4.1 包体现状分析
首先使用 Android Studio 的 APK Analyzer 对安装包进行深度解析。体积占比最大的是 so 库,将近 56MB------地图、直播推流、Flutter、Weex 引入的 Native 库非常庞大:

其次是 dex 文件(约 28MB),由所有 Java/Kotlin class 编译而成:

最后是资源文件------assets 资源约 7MB,包含各种格式的资源文件(字体、音频等):

res 资源约 10MB,包括图片、页面布局文件、字符串资源等:

如果直接逐个处理单独的 so 库,往往会因为多个模块交叉依赖同一个 so 而难以评估风险。好在应用采用了模块化架构,可以从模块维度审视体积------借助 CI 平台的依赖树分析,对各模块体积进行排序:

各模块对应独立的业务功能,耦合度相对较低。经产品和技术协同评估,综合业务影响、包体收益、研发投入等因素,对不同模块采取不同策略:
| 模块名称 | 地图 | Flutter | Weex | 相册 | 端智能 | 消息模块 | ... |
|---|---|---|---|---|---|---|---|
| 缩减措施 | 远程化 | 前期远程化,后期下线 | 后续下线→H5替换 | 远程化 | 远程化 | so库裁剪 | ... |
4.2 方案制定
综合以上分析,确定了四条瘦身路径:
- 拔除冗余:下线无用三方库,移除无用代码和资源
- 远程化非主链路模块:地图、直播推流、端智能、相册等(核心手段)
- 资源缩减:图片资源编译时自动转换 WebP 压缩
- 长期方案:包体积监控、准入卡口、代码覆盖率监测
5. 方案实现
5.1 远程化
5.1.1 原理介绍
远程化(也称插件化),就是将 App 拆分成一个宿主(base)和多个插件(feature),每个模块都是一个独立的 APK。上传到应用商店的只有 base APK,插件 APK 在运行时按需从云端下载并加载。
这个过程分为编译期 和运行时两个阶段:
- 编译期:不需要远程化的模块打包成主 APK(base.apk);各个远程化模块(地图、推流等)分别打包成独立的 feature.apk 并上传到 CDN;上传的元信息(模块名、下载地址、MD5 等)内嵌在宿主 APK 中
- 运行时:base.apk 运行过程中完成 feature.apk 的下载、校验、安装和类加载,最终正常使用对应功能

远程化的主要优势:
- 非主链路模块按需加载,有效降低安装包体积
- 各模块解耦,灵活性强,支持独立热修复
- 易于维护和扩展
同时也存在一些风险:
- 有一定技术门槛,需根据应用自身情况适配
- 系统架构复杂度上升
- 运行时可能遇到类加载、资源加载相关的兼容性问题
5.1.2 方案实施
技术选型与业内方案对比
2018 年 Google I/O 发布了 Android App Bundle(AAB),基于 base APK + split APK 实现动态交付。但 AAB 强依赖 Google Play------国内应用无法直接使用。
围绕"国内如何实现动态交付"这个命题,业内诞生了多代插件化/远程化技术方案。在做技术选型时,我们对主流框架做了系统对比:
| 维度 | Google AAB | RePlugin (360) | VirtualAPK (滴滴) | Shadow (腾讯) | Qigsaw (爱奇艺) | Cigsaw (本方案) |
|---|---|---|---|---|---|---|
| 技术代际 | 官方方案 | 第一代 | 第一代 | 第二代 | 第三代(AAB系) | 第三代(AAB系) |
| Hook 数量 | 0 | 1个(ClassLoader) | 多个(AMS/PMS等) | 0 | 0 | 0 |
| 反射私有API | 无 | 少量 | 较多 | 无 | 无 | 无 |
| Android新版兼容 | 原生支持 | 需适配 | 需大量适配 | 天然兼容 | 天然兼容 | 天然兼容 |
| 四大组件 | 原生支持 | 占坑方案 | Hook方案 | 代理+字节码 | 原生支持 | 原生支持 |
| Google Play兼容 | 原生 | 不兼容 | 不兼容 | 不兼容 | 兼容 | 兼容 |
| 国内独立部署 | 不支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 维护状态(2025) | 活跃 | 停止维护 | 停止维护 | 低频维护 | 低频维护 | 活跃 |
第一代框架(RePlugin、VirtualAPK) 的核心思路是通过 Hook 系统框架层来"欺骗" Android 系统,让系统以为插件中的组件是预注册的。RePlugin 以"仅 1 个 Hook 点"著称(只 Hook ClassLoader),追求极致稳定性,但代价是不支持 Service、ContentProvider 的完整动态化。VirtualAPK 则 Hook 了 AMS、PMS 等多处系统服务,功能更全面,但每次 Android 大版本升级都需要大量适配工作------Android 9 开始限制反射调用隐藏 API 后,这类方案的维护成本急剧上升。
第二代框架(Shadow) 由腾讯开源,做出了"零反射、零 Hook "的突破性设计。它通过编译期字节码替换(将插件代码中的 Activity 替换为 ShadowActivity)和运行时代理转调来实现组件生命周期管理,完全不依赖系统私有 API。理论上天然兼容所有 Android 版本------这在当时是行业的重大进步。但 Shadow 的代价是侵入性较强 :插件开发者需要继承特定基类(ShadowActivity),现有代码迁移成本高;且它不基于 AAB 标准,无法与 Google Play 动态交付体系兼容。
第三代框架(Qigsaw/Cigsaw) 走了另一条路------拥抱 AAB 标准,替换交付通道。它的核心思想是:保留 Google AAB 的 split APK 打包格式和 Play Core API 接口,但把底层的"从 Google Play 下载"替换为"从自有 CDN 下载"。这意味着:
- 零 Hook、零反射:基于 Android Framework 原生支持的 split APK 机制,不需要任何系统 Hack
- 天然兼容新 Android 版本:只要 Android 系统支持 split APK(API 21+),就不需要额外适配
- 双轨兼容:同一份代码既能在国内走自有 CDN 下载,也能在海外走 Google Play------有出海需求时无需二次开发
- 开发体验一致:上层 API 与 Google Play Core Library 完全一致,业务开发者无感
我们最终选择的 Cigsaw 是在 Qigsaw 基础上的深度适配版本,在继承上述优势的基础上,还补充了几个关键能力:
- 支持 feature.apk 内置降级:split APK 可选内置于基础包中,网络异常时直接使用本地版本,保证功能可用
- 跨进程 AIDL 安装服务:安装过程独立进程执行,彻底隔离主进程 ANR 风险
- Native Hook 资源监控 :通过 JNI Hook
AssetManager.nativeSetApkAssets()实时感知资源变化(覆盖 API 28~36) - 远程配置热更新:支持不发版更新模块下载地址和版本信息
- 支持断点续传、MD5 完整性校验、下载优先级控制
与 Shadow 的"零 Hook 但需改代码"不同,Cigsaw 的"零 Hook"是建立在系统原生能力之上的------既不需要 Hook 系统,也不需要修改业务代码,这是选择它的决定性因素。
远程化框架核心架构
整个框架分为五层,层次清晰、职责明确:
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ 业务方通过 API 按需请求功能模块 │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Play Core 兼容层 │
│ SplitInstallManager / SplitInstallRequest │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ 核心门面层 │
│ 框架初始化 / 生命周期管理 / 配置路由 │
└───────┬────────────────┬────────────────┬───────────────┘
│ │ │
┌───────▼──────┐ ┌──────▼───────┐ ┌────▼─────────┐
│ 类加载引擎 │ │ 安装管理器 │ │ 远程下载器 │
│ ClassLoader │ │ 签名/校验 │ │ 并行下载 │
│ Hook+资源注入│ │ 跨进程AIDL │ │ 断点续传 │
└──────────────┘ └──────────────┘ └──────────────┘
远程下载:接口与实现分离
框架定义了标准的 Downloader 接口,业务方可以插入任意下载引擎实现:
java
public interface Downloader {
// 立即下载(高优先级,用户正在等待)
void startDownload(int sessionId, List<DownloadRequest> requests,
DownloadCallback callback);
// 延迟下载(低优先级,后台预加载)
void deferredDownload(int sessionId, List<DownloadRequest> requests,
DownloadCallback callback, boolean usingMobileDataPermitted);
// 取消下载
boolean cancelDownloadSync(int sessionId);
// 移动网络下载阈值(超过时需用户确认)
long getDownloadSizeThresholdWhenUsingMobileData();
// 是否仅 WiFi 下延迟下载
boolean isDeferredDownloadOnlyWhenUsingWifiData();
}
默认实现采用 GroupTaskDownloader 管理并行下载,最大并行数为 5,支持进度聚合和零拷贝缓存优化。当检测到本地已有匹配缓存时,直接跳过网络请求:
java
public void startDownload(int sessionId, List<DownloadRequest> requests,
DownloadCallback callback) {
List<DownloadParams> downloadParams = new ArrayList<>();
for (DownloadRequest request : requests) {
// 过滤本地协议(assets://、native://),只下载远程资源
if (!request.getUrl().startsWith("assets://")
|| !request.getUrl().startsWith("native://")) {
downloadParams.add(DownloadParams.newInstance(request));
}
}
// 高优先级并行下载
groupTaskDownloader.startParallelDownload(sessionId, downloadParams,
HIGH_PRIORITY, callback);
}
跨进程安装服务(AIDL)
下载完成后的安装过程通过 AIDL 运行在独立服务进程中,避免安装过程中的 IO 密集操作(解压、签名校验、dex 编译)导致主进程 ANR:
java
// ISplitInstallService.aidl
interface ISplitInstallService {
void startInstall(String packageName, in List<Bundle> moduleNames,
in Bundle versionCode, ISplitInstallServiceCallback callback);
void cancelInstall(String packageName, int sessionId,
in Bundle versionCode, ISplitInstallServiceCallback callback);
void getSessionState(String packageName, int sessionId,
ISplitInstallServiceCallback callback);
void deferredInstall(String packageName, in List<Bundle> moduleNames,
in Bundle versionCode, ISplitInstallServiceCallback callback);
void deferredUninstall(String packageName, in List<Bundle> moduleNames,
in Bundle versionCode, ISplitInstallServiceCallback callback);
}
安装流程包括:签名验证(确保 split 与主 APK 证书一致)→ MD5 完整性校验 → 提取 Native 库(文件锁保证并发安全)→ 提取 DEX → 触发 OAT 预编译 → 写入安装标记。
运行时类加载:委托 ClassLoader 架构
这是远程化方案中最精妙的部分------如何让远程下载的代码无缝被业务调用?框架通过替换系统 PathClassLoader,实现了透明的类加载委托:
SplitDelegateClassloader (替代原始 PathClassLoader)
│
findClass(name)
│
├─→ 原始 ClassLoader 查找(主应用代码)
│
└─→ ClassNotFoundInterceptor 拦截
│
▼
遍历所有 SplitDexClassLoader(远程模块代码)
│
└─→ 沿依赖链逐级查找
每个远程模块拥有独立的 SplitDexClassLoader,支持模块间的依赖链加载------当模块 A 依赖模块 B 中的类时,A 的 ClassLoader 会自动沿依赖链到 B 的 ClassLoader 中查找:
java
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name); // 先在自己的 dex 中找
} catch (ClassNotFoundException e1) {
if (dependenciesLoaders != null) {
for (SplitDexClassLoader loader : dependenciesLoaders) {
try {
return loader.loadClassItself(name);
} catch (ClassNotFoundException e2) {
// 继续下一个依赖
}
}
}
throw e1;
}
}
Android 版本兼容:符号链接绕过后台验证
Android 11 引入了后台 DEX 验证机制(RunBackgroundVerification),可能拒绝动态加载的 split APK。框架通过创建去掉 .apk 后缀的符号链接 绕过------系统只对 .apk 文件触发验证:
java
protected static String symbolicLink(String dexPath) {
if (Build.VERSION.SDK_INT >= 30 && Build.VERSION.SDK_INT <= 33
&& dexPath.endsWith(".apk")) {
// Android 11~13:创建符号链接,绕过后台验证
return createSymbolicLink(dexPath);
} else if (Build.VERSION.SDK_INT >= 34) {
// Android 14+:设为只读,满足安全要求
try { (new File(dexPath)).setReadOnly(); } catch (Throwable ignored) {}
}
return dexPath;
}
Native Hook 监控资源变化
远程模块还带来资源文件(布局、图片等)。框架通过 JNI Hook 拦截 AssetManager.nativeSetApkAssets() 方法,实时感知资源表变化并通知 Java 层刷新:
cpp
// bridge.cc
static void nativeSetApkAssetsProxy(JNIEnv* env, jclass klass,
jlong ptr, jobjectArray apkAssets, jboolean invalidateCaches) {
// 1. 执行原始逻辑
((OriginalFunc)originFuncPtr)(env, klass, ptr, apkAssets, invalidateCaches);
// 2. 通知 Java 层资源已变更
env->CallStaticVoidMethod(assetManagerMonitor, apkAssetsChanged);
}
覆盖 Android API 28~36,并针对 Android 15+ 方法签名变化做了专门适配。
远程配置热更新
除了按需下载,框架还支持不发版更新模块 ------当服务端推送新版本时,SplitUpdateService 后台验证并原子化替换本地配置:
java
protected void onHandleIntent(Intent intent) {
String newVersion = intent.getStringExtra(NEW_SPLIT_INFO_VERSION);
String newPath = intent.getStringExtra(NEW_SPLIT_INFO_PATH);
// 版本去重 → 配置文件解析 → 应用标识校验 → 原子更新
if (manager.updateSplitInfoVersion(context, newVersion, newFile)) {
onUpdateOK(oldVersion, newVersion, updateSplits);
}
}
下次用户触发对应模块时,框架自动使用新 URL 下载新版 split APK。
编译阶段

根据安装包用途分为不同的编译模式:
- 本地调试 Debug 包:Android Studio 直接 Run,采用 Google 官方 AAB 模式,base + dynamic features 一起安装
- 发布到应用商店的正式包:CI 平台执行 Gradle 脚本,各 feature split APK 上传到 CDN,以便运行时动态加载
- 完整功能包/海外 Google Play 包:关闭远程模块能力,正常打包
运行时

运行时需要明确处理的几件事:
- 流量保护:用户首装时,非 WiFi 环境下有固定流量阈值,单次下载超过阈值会弹窗确认。防止远程化被滥用导致静默下载体积过大
- 入口拦截 :远程化模块的每个页面入口都需要拦截,确保模块加载完成后才放行。模块越多,需要拦截的入口越多,遗漏会导致线上
ClassNotFoundExceptioncrash - 兜底机制:断网、弱网、覆盖安装等场景,需要增加 loading 页面和错误提示
5.1.3 实现效果
对地图、相册、Flutter、直播推流、视频剪辑、端智能等模块进行了插件化,有效降低了约 38~40MB 的包体积。
首次安装效果:中端机型上,首次安装登录后,上传视频、开启直播、发送图片等功能正常使用,无需等待。地图功能仅在第一次使用时有一个短暂的 loading 页面。
二次启动时,各远程化组件基本无阻塞感。
5.2 图片压缩插件
5.2.1 背景
由于历史原因,应用的壳工程和二三方库中存在大量 PNG/JPG 图片。这些图片可以转换为 WebP 格式来压缩,但手动处理效率低且容易遗漏------依赖库数量众多,逐个处理不现实。
5.2.2 原理
解决思路分两步:拿到所有图片 → 批量压缩。
第一步 借鉴了 Android 构建流程中的 mergeResourcesTask------这个 Task 负责合并所有 AAR、module 的资源,在它执行之后,可以通过 AGP 提供的 allRawAndroidResources.files 接口拿到所有资源文件(包括二三方库中的)。然后插入一个自定义 Task,遍历并筛选图片文件进行压缩。
第二步 使用 Google 官方的 cwebp 命令行工具进行 WebP 转换,并采用多线程加速(根据 CPU 核心数开启并行),避免拖慢编译。同时做了体积比对------转换后反而变大的图片不做处理。

这套 Gradle 插件的优势:
- 接入简单:引入插件即可自动执行,无需手动操作
- 面向切面:壳工程、二方库、三方库的图片资源全覆盖
- 多线程加速:CI 平台上对编译时长增加不超过 20s,本地调试时不执行
- 白名单机制:关键图片(安全组件生成的图片、.9.png 等)可配置跳过
5.2.3 实现效果
CI 平台打包时的日志显示,在手动处理壳工程后,插件仍然处理了约 1400 张图片 ,节省了大量手动操作。图片压缩总收益约 5MB(经 APK 二次压缩后,最终包体积降低约 0.7MB)。

6. 数据指标
截至上线一个月后的数据统计:
| 指标 | 数值 |
|---|---|
| 包体积 | 63.8MB(较优化前降低 38%) |
| 稳定性 | crash 率 0.011%,远程化引起的不足总 crash 数 5% |
| 模块下载成功率 | > 98% |
| 模块加载成功率 | 99.98% |
| 版本覆盖率 | 上线一个月覆盖设备 > 87% |
上架后应用商店下载量出现明显激增。

7. 总结和展望
7.1 总结
这次历时两个月的瘦身,收获体现在三个层面:
对用户而言:无论新安装还是更新,下载等待时间和流量消耗都大幅降低,有利于新业务在用户群体中的快速触达。
对应用自身而言:第一次系统性地为应用做了"减肥"------移除了大量冗余架构、代码和资源,把一些历史包袱远程化以备后续下线或改造。模块间耦合度降低,架构层次更加清晰。
技术沉淀而言:远程化方案从原理验证到大规模落地,积累了完整的问题排查经验------从 ClassNotFoundException 的入口拦截遗漏,到 Android 11+ 的 dex2oat 后台验证问题,到多进程并发安装的文件锁设计,每一个坑都变成了可复用的工程能力。
7.2 展望
包体积优化是一场长效治理的持久战。这次瘦身成功了,但没有防腐化措施,减肥成效很快就会"反弹"。需要在交付流程上建立规约:
"非必要不依赖,依赖原因要透明,引新需要去旧,变更留记录"
------这既是包体防腐,更是架构防腐。
包体积压缩只是应用性能优化的一部分。在这个过程中也发现了内存、启动速度等方面的潜在优化空间,未来会持续推进,把应用的性能和用户体验做到更好。