Administrator
发布于 2026-08-25 / 31 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 61 篇:APK 体积优化:包体瘦身完整指南

上一篇讲了屏幕适配,切到包体积问题。APK 每涨 1MB,用户下载转化率掉 1%。这个数字看着小,乘以亿级 DAU 就是钱。这一篇把所有能瘦身的手段系统过一遍。

APK 的组成结构

分析 APK 第一步是看里面有什么:

./gradlew app:assembleRelease
ls -lh app/build/outputs/apk/release/

# 用 Android Studio 的 APK Analyzer
# 或者反编译
java -jar apktool_2.9.3.jar d app-release.apk -o apk_extracted

典型 APK 的体积分布:

部分

占比

优化潜力

classes.dex

30-50%

中(混淆、删除无引用)

资源文件(drawable/、raw/)

20-30%

大(webp、删除无引用)

第三方库

15-25%

中(裁剪、按需引入)

so 库

10-20%

大(按 abi 分包、删除 v7)

assets/

5-15%

中(按需下载)

其他(kotlin、Multidex 配置)

< 5%

小

第一步:APK Analyzer 看家底

Android Studio 的 APK Analyzer 是必学工具:

APK Analyzer 能看出:
1. 每个文件的体积、占比
2. dex 里每个 package、class、method 的体积
3. 资源引用的次数(多少代码引用、是否重复资源)
4. R8 删除类的依据

常见分析结论:

  • "呦,我有个帮助类写了 5KB,没人用" → 删除

  • "哎,这图片 1024x1024,我用在了 100dp 区域" → 缩小

  • "这 5 个第三方库,每个 200KB" → 真需要这么多吗?

第二步:R8/ProGuard 优化

// app/build.gradle.kts
android {
    buildTypes {
        release {
            isMinifyEnabled = true  // R8 删代码
            isShrinkResources = true // R8 删无用资源
        }
    }
}

proguardFiles(
    getDefaultProguardFile("proguard-android-optimize.txt"),
    "proguard-rules.pro"
)

写好 -keep 规则,避免反射类被删:

# proguard-rules.pro
-keep class com.example.model.** { *; }
-keep class com.example.api.** { *; }
-keepattributes Signature
-keepattributes *Annotation*

R8 全模式优化比 ProGuard 强 30%+,但默认配置可能误删:

  • 注解(用 @Keep 显式标记)

  • JNI 调用的方法(-keepclasseswithmembernames)

  • 反射调用(-keep class X.Y)

开启 R8 的更多优化:

buildTypes {
    release {
        // R8 fullMode
        // 实际 Android Gradle 8.0 默认就是 fullMode
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}

测试方法不能只看构建成功,要跑全量回归:

./gradlew :app:assembleRelease
./gradlew :app:connectedAndroidTest

第三步:图片资源优化

1. WebP 替代 PNG

PNG 改 WebP 平均省 25-35%:

# 单文件
cwebp -q 80 input.png -o output.webp

# 批量
for f in *.png; do cwebp -q 80 "$f" -o "${f%.png}.webp"; done

Android Studio 一键转:

  1. 选中一堆 PNG

  2. 右键 → Convert to WebP

  3. 选择质量,自动生成

2. AAB + Resource Sharding

Android App Bundle(AAB)能让 Google Play 自动按设备分发资源:

android {
    bundle {
        language {
            enableSplit = true  // 按语言分包
        }
        density {
            enableSplit = true // 按 dpi 分包
        }
        abi {
            enableSplit = true // 按 ABI 分包
        }
    }
}

效果:用户只下载匹配自己设备的资源。一个 1080P 设备只下 xxxhdpi 资源,不下载 xxhdpi。

3. 图片压缩工具

工具

用途

Guetzli

JPEG 高压缩比

pngcrush

PNG 优化

ImageOptim

macOS 工具,多格式

Android 资源编译时也会优化,但二次压缩能进一步压。

第四步:Native 库按需打包

1. App Bundle 按 ABI 分包

android {
    defaultConfig {
        ndk {
            abiFilters("arm64-v8a", "armeabi-v7a", "x86_64")
        }
    }

    splits {
        abi {
            isEnable = true
            reset()
            include("arm64-v8a", "armeabi-v7a")
            isUniversalApk = false
        }
    }
}

效果:老设备可能只用 armeabi,新设备用 arm64,单 ABI 包大约 5-8MB(基线包)。

2. v7 和 v8 分开打

历史上 armeabi-v7a 设备市场占有率已经 < 10%,可以只保留 arm64:

ndk {
    abiFilters("arm64-v8a")  // 仅 64 位
}

包体积直接砍掉 30-40%。

3. 移除不必要的 so

很多三方库带了多套 so(每个 ABI 一份),但实际只用了其中一个:

# 看 APK 里都有什么 so
unzip -l app-release.apk | grep '\.so$'

常见操作:

  • 找三方库,看看能不能配置只保留需要的 ABI

  • 用 ./gradlew app:bundleRelease 让 Play 自动分发

  • 国产 ROM 商店可能不支持 ABI split,要测试

第五步:删代码,砍三方库

无用的代码被 R8 自动处理,但实际打包到 dex 的代码不会自动删——R8 是 javacode 级别的优化。要在依赖层面解决:

./gradlew :app:dependencies --configuration releaseRuntimeClasspath

# 看哪些三方库占空间
# 常见结果:event_bus、glide、retrofit、moshi、okhttp...

问自己:

  • "这个库真的用了 5% 还是 80% 的 API?"

  • "能不能用现有库替代?"

  • "能不能 Android SDK 自带的替代?"

我的经验:

库

替代方案

EventBus

用 Flow/Kotlin Flow

Volley

Retrofit + OkHttp

RxAndroid

Kotlin Flow + Coroutines

Glide

Coil(Kotlin、协程友好)

Gson

Moshi 或 Kotlinx Serialization

三方 EventBus

hilt + SharedFlow

三方崩溃 SDK

集成 Firebase Crashlytics

第六步:资源按需下载

大体积资源(H5 包、地图离线数据、字体包)不放进 APK,按需下载:

// 把资源放到 OSS,启动时检测版本下载
class ResourceDownloader {
    suspend fun downloadResourcesIfNeeded(version: String) {
        val localVersion = preferenceStorage.getLocalVersion()
        if (version != localVersion) {
            val res = apiClient.fetchResourceList(version)
            res.forEach { download(it) }
            preferenceStorage.setLocalVersion(version)
        }
    }
}

第七步:动态化

更激进的做法是把某些业务模块做成独立包,按需加载:

  • Tinker + 自研动态化框架

  • 微信 tinker / 各厂自研

对常规小项目成本太高,对亿级 App ROI 高。

真实案例

一个 30MB 的电商 App 优化过程:

步骤

优化内容

减小

体积

起始

30MB

-

30.0 MB

1. PNG → WebP

高质量图片

3.5 MB

26.5 MB

2. 移除 v7 ABI

仅 arm64

5.0 MB

21.5 MB

3. R8 fullMode

优化 + 混淆

2.0 MB

19.5 MB

4. 删除没用的 activity-indicator 库

5KB

0.2 MB

19.3 MB

5. 字体文件改用下载

4MB → 0.5 MB

3.5 MB

15.8 MB

6. 使用 AAB 提交

自动分包

用户侧下载 ≤ 12MB

-

优化误区

1. 过度依赖 R8

R8 强,但不是银弹。它处理的是字节码,不会管你用没用 String、Drawable、ImageView。

2. 删图只删一张

设计师上传的几 MB 大图,三方库的 demo 图,启动引导图……全过一遍。

3. 资源 sharding 不开

AAB + resource sharding 是 Google 推荐姿势,但很多团队没用——理由是"对国内渠道不支持"。

4. 把优化放最后

体积应该在每个 PR 监控,而不是打包发版前突击。

持续监控

把体积加到 CI:

- name: APK Size Check
  run: |
    SIZE=$(stat -c%s app-release.apk)
    PREV=$(cat apk-size.json | jq -r '.size')
    if [ $SIZE -gt $((PREV + 500000)) ]; then
      echo "APK 增大超过 500KB,请 review!"
      exit 1
    fi
    jq -n --arg size $SIZE --arg ap prev . '{size: $size}' > apk-size.json

小结

APK 瘦身是系统性工程,方法很多:R8、图片格式、ABI 分包、资源 sharding、动态下发。关键是建立监控,每个 PR 都报告 APK 变化。绝大多数 App 都能从 30MB 减到 15MB 左右,不需要架构改动,只需要按步骤执行。

下一篇进入 R8 的更多高级优化,包括混淆、字符串加密、签名校验。


评论