Appearance
Android 包体积优化
目录
引言
包体积是 Android 应用的重要指标。过大的包体积会导致下载失败、安装失败、用户流失等问题。本文将深入探讨 Android 包体积优化的各个方面,提供实用的优化方案和代码示例。
为什么需要包体积优化?
- 下载成功率:30MB 以上应用下载成功率下降 30%
- 安装成功率:大容量存储设备可能安装失败
- 用户体验:下载时间影响用户耐心
- 流量消耗:用户可能使用移动网络
包体积目标
| 应用类型 | 目标体积 | 建议最大 |
|---|---|---|
| 工具类 | < 5MB | < 10MB |
| 社交类 | < 15MB | < 30MB |
| 游戏类 | < 50MB | < 100MB |
ProGuard/R8 混淆
R8 配置
R8 是 Google 推出的代码压缩工具,替代 ProGuard。
pro
# build.gradle
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
# proguard-rules.pro
# 保持自定义类
-keep class com.example.** { *; }
# 保持枚举
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(String);
}
# 保持 JSON 序列化
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 保持反射类
-keepclassmembers class * {
@androidx.annotation.Keep <fields>;
@androidx.annotation.Keep <methods>;
}高级 R8 规则
pro
# 使用 Keep Annotation
@Keep
class MyClass {
@Keep
var myField: String? = null
@Keep
fun myMethod() {}
}
# 保持 Native 方法
-keepclassmembers class * {
native <methods>;
}
# 保持 JNI
-dontwarn java.lang.**
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# 保持 Retrofit
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn retrofit2.**
-keepnames class * extends retrofit2.http.**
-keepnames class * implements retrofit2.**
# 保持 Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule {
<init>(...);
}混淆效果分析
kotlin
// 查看混淆报告
# build.gradle
tasks.whenTaskAdded { task ->
if (task.name.contains("merge")) {
task.doLast {
println("Merge task completed: ${task.name}")
}
}
}
# 生成混淆映射
-verbose
-printseeds seeds.txt
-printusage usage.txt
-printunnecessary unnecessary.txt资源压缩
ShrinkResources
groovy
android {
buildTypes {
release {
shrinkResources true // 删除未使用的资源
// 确保 R8 启用
minifyEnabled true
}
}
}资源压缩配置
groovy
// AAPT2 压缩
android {
aaptOptions {
// 压缩 PNG
enableCrunchOptions true
// 压缩 JPEG
enableCrunchOptions true
// 压缩 WebP
enableCrunchOptions true
}
}
// 资源压缩级别
enum class CruncherLevel {
NONE,
LOW,
MEDIUM,
HIGH,
ULTRA
}
// 配置压缩级别
aaptOptions.cruncherLevel = CruncherLevel.HIGH资源目录优化
res/
├── drawable/ # 通用 drawable
├── drawable-hdpi/ # 高清
├── drawable-xhdpi/ # 超高清
├── drawable-xxhdpi/ # 超超高清
├── drawable-xxxhdpi/ # 超超超高清
└── mipmap/ # 应用图标
优化方案:
├── drawable/ # 只放必要的
├── drawable-xxhdpi/ # 只保留 xxhdpi
└── mipmap/ # 应用图标图片格式优化
WebP 格式
groovy
// 使用 WebP 格式
android {
aaptOptions {
useNewCruncher true // 支持 WebP
}
}
// 转换图片为 WebP
fun convertToWebP(inputPath: String, outputPath: String) {
val input = Pixmap(inputPath)
val output = File(outputPath)
Pixmap.writeToStream(
FileOutputStream(output),
input,
PictureFormat.WebP,
85f // 质量 85%
)
}图片压缩配置
kotlin
// Glide 图片压缩
Glide.with(context)
.load(url)
.apply(
RequestOptions()
.format(ImageFormat.WEBP) // 使用 WebP
.override(Width, Height) // 指定尺寸
.centerCrop()
)
.into(imageView)
// 自定义图片压缩
class ImageCompressor {
fun compressImage(bitmap: Bitmap, quality: Int): Bitmap {
val options = Bitmap.CompressFormat.WEBP
val targetSize = 100 * 1024 // 100KB
var qualityValue = quality
var outputStream = ByteArrayOutputStream()
bitmap.compress(options, qualityValue, outputStream)
while (outputStream.size() > targetSize && qualityValue > 10) {
outputStream = ByteArrayOutputStream()
qualityValue -= 5
bitmap.compress(options, qualityValue, outputStream)
}
return BitmapFactory.decodeByteArray(
outputStream.toByteArray(), 0, outputStream.size()
)
}
}图片尺寸优化
kotlin
// 根据屏幕密度加载图片
class ScreenAwareImageLoader {
fun loadImage(context: Context, drawableName: String): Bitmap {
val density = context.resources.displayMetrics.density
val resourceId = context.resources.getIdentifier(
drawableName,
"drawable",
context.packageName
)
return when {
density <= 0.75f -> BitmapFactory.decodeResource(
context.resources, resourceId, BitmapFactory.Options().apply {
inSampleSize = 2
}
)
density <= 1.5f -> BitmapFactory.decodeResource(
context.resources, resourceId, BitmapFactory.Options().apply {
inSampleSize = 1
}
)
else -> BitmapFactory.decodeResource(
context.resources, resourceId
)
}
}
}九宫格图优化
九宫格图压缩
kotlin
class NinePatchOptimizer {
fun optimizeNinePatch(context: Context, ninePatch: NinePatch): NinePatch {
// 压缩补丁点
val optimizedChunk = optimizeChunk(ninePatch.chunk)
val optimizedLeftPatches = optimizePatches(ninePatch.leftPatches)
val optimizedTopPatches = optimizePatches(ninePatch.topPatches)
return NinePatch(
optimizedChunk,
optimizedLeftPatches,
optimizedTopPatches,
ninePatch.left,
ninePatch.top,
ninePatch.right,
ninePatch.bottom
)
}
private fun optimizeChunk(chunk: Bitmap): Bitmap {
// 减小 patch 尺寸
val scale = 0.5f
val newWidth = (chunk.width * scale).toInt()
val newHeight = (chunk.height * scale).toInt()
return Bitmap.createScaledBitmap(chunk, newWidth, newHeight, true)
}
}九宫格图工具
kotlin
// 使用 Android Studio 的九宫格编辑器
// 1. 右键九宫格图 → Ninepatch Editor
// 2. 编辑补丁区域
// 3. 导出为 WebP 格式
class NinePatchBuilder {
fun build(context: Context, drawableName: String): NinePatch {
val drawable = context.resources.getDrawable(R.drawable.button_bg)
return drawable as NinePatch
}
}去除无用资源
使用 lint 检查
groovy
// build.gradle
android {
lintOptions {
checkReleaseBuilds false
abortOnError false
disable 'UnusedResources' // 暂时禁用
// 检查未使用资源
lintConfig file('lint.xml')
}
}
// lint.xml
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="UnusedResources" severity="warning" />
<issue id="MissingTranslation" severity="ignore" />
</lint>删除未使用资源
kotlin
// 使用 ShrinkResources 自动删除
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}
// 手动检查资源引用
fun checkUnusedResources() {
val resources = context.resources
val usedResources = ConcurrentHashMap<String, Boolean>()
// 遍历所有资源
resources.getIdentifier("", "drawable", context.packageName).let {
// 检查是否被使用
}
}资源清理工具
kotlin
// 资源依赖分析
class ResourceAnalyzer {
fun analyze(context: Context): List<String> {
val unusedResources = mutableListOf<String>()
// 检查 drawable
context.resources.let { res ->
val drawableNames = getDrawableNames(res)
drawableNames.forEach { name ->
if (!isResourceUsed(name)) {
unusedResources.add(name)
}
}
}
return unusedResources
}
private fun isResourceUsed(name: String): Boolean {
// 检查 XML、Java/Kotlin 代码中是否引用
return false
}
}多渠道打包
多渠道打包配置
groovy
// build.gradle
android {
defaultConfig {
applicationId "com.example.app"
versionCode 1
versionName "1.0"
// 渠道配置
buildConfigField "String", "CHANNEL", "\"default\""
}
flavorDimensions "channel"
productFlavors {
xiaomi {
dimension "channel"
applicationIdSuffix ".xiaomi"
buildConfigField "String", "CHANNEL", "\"xiaomi\""
}
huawei {
dimension "channel"
applicationIdSuffix ".huawei"
buildConfigField "String", "CHANNEL", "\"huawei\""
}
oppo {
dimension "channel"
applicationIdSuffix ".oppo"
buildConfigField "String", "CHANNEL", "\"oppo\""
}
}
}多渠道打包优化
groovy
// 使用 ManifestPlaceholders 替代 ProductFlavors
android {
defaultConfig {
applicationId "com.example.app"
// 渠道信息放入 Manifest
manifestPlaceholders = [
CHANNEL: "default"
]
}
productFlavors {
xiaomi {
manifestPlaceholders = [
CHANNEL: "xiaomi"
]
}
}
}
// AndroidManifest.xml
<manifest>
<application
android:name=".App"
android:label="@string/app_name">
<meta-data
android:name="channel"
android:value="${CHANNEL}" />
</application>
</manifest>多渠道打包脚本
groovy
// 多渠道打包脚本
def channels = ['xiaomi', 'huawei', 'oppo', 'vivo', 'qikuai']
android {
flavorDimensions "default"
productFlavors {
channels.each { channel ->
channel {
dimension "default"
applicationIdSuffix ".${channel}"
manifestPlaceholders = [CHANNEL_NAME: channel]
}
}
}
}
// 使用脚本生成多渠道
task generateChannelsScript {
doLast {
def script = '''
#!/bin/bash
channels=("xiaomi" "huawei" "oppo")
for channel in \${channels[@]}; do
./gradlew assemble${channel}
done
'''
file('build_channels.sh').text = script
file('build_channels.sh').setExecutable(true)
}
}动态特性模块
动态模块配置
groovy
// 主模块 build.gradle
android {
dynamicFeatures = [
':feature_game',
':feature_video',
':feature_music'
]
}
// 动态模块 build.gradle
plugins {
id 'com.android.dynamic-feature'
}
android {
// 动态模块配置
}
dependencies {
implementation project(':shared')
}动态模块加载
kotlin
// 使用 Play Core 加载动态模块
class DynamicModuleLoader {
private val moduleInstallListener = ModuleInstallListener {
// 模块安装完成
if (it.installState == ModuleInstallState.INSTALLED) {
// 启动模块
startModule(it.id)
}
}
fun loadModule(moduleId: String) {
val request = ImmediateModuleRequest.builder(moduleId)
.addOnStateUpdateListener(moduleInstallListener)
.build()
DynamicModule.install(request)
}
fun checkModuleInstalled(moduleId: String): Boolean {
return DynamicModule.isInstalled(moduleId)
}
}
// 条件分发
class ConditionalDistribution {
fun checkConditions(): List<String> {
return listOf(
"feature_game" if hasGameFeature else null,
"feature_video" if hasVideoFeature else null
).filterNotNull()
}
}动态模块通信
kotlin
// 主模块与动态模块通信
class ModuleCommunication {
// 使用 Binder
interface IModuleService : IInterface {
fun performAction(data: Bundle): Bundle
}
// 使用 Intent
fun sendToModule(moduleId: String, action: String) {
val intent = Intent().apply {
`package` = "com.example.$moduleId"
action = action
}
startActivity(intent)
}
}依赖优化
依赖分析
groovy
// 使用 dependencyTree 分析依赖
./gradlew app:dependencies --configuration releaseRuntimeClasspath
// 使用 dependency-visualizer
plugins {
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
// 查找依赖冲突
configurations.all {
resolutionStrategy {
force 'com.google.code.gson:gson:2.10.1'
}
}依赖排除
groovy
dependencies {
// 排除重复依赖
implementation('com.example:library:1.0.0') {
exclude group: 'com.google.code.gson', module: 'gson'
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
// 使用 API 仅依赖
api('com.example:core:1.0.0') {
// 传递性依赖
}
// 使用 Implementation 隐藏依赖
implementation('com.example:utils:1.0.0') {
// 不传递
}
}依赖版本优化
groovy
// 使用 BOM 管理依赖版本
dependencies {
// AndroidX BOM
implementation(platform('androidx.core:core-bom:1.12.0'))
// Google BOM
implementation(platform('com.google.android.gms:play-services-bom:18.5.0'))
// 不再指定版本号
implementation('androidx.core:core-ktx')
implementation('com.google.android.gms:play-services-auth')
}依赖替代
groovy
// 替换大依赖
dependencies {
// 使用轻型替代
// ❌ 使用整个 Android 支持库
// implementation 'com.android.support:appcompat-v7:28.0.0'
// ✅ 使用 AndroidX
implementation 'androidx.appcompat:appcompat:1.6.1'
// ✅ 使用轻型 JSON 库
// ❌ implementation 'com.google.code.gson:gson:2.10.1'
// ✅ implementation 'com.squareup.moshi:moshi:1.13.0'
// ✅ 使用轻型图片库
// ❌ implementation 'com.github.bumptech.glide:glide:4.16.0'
// ✅ implementation 'io.coil-kt:coil:2.5.0'
}工具使用
Android Studio App Bundle Explorer
打开方式:
- Build → Generate Bundle / APK
- 完成后点击 "View" 打开
分析项:
- 代码大小分析
- 资源大小分析
- 依赖分析
- 未使用资源标记
APK Analyzer
kotlin
// 使用 APK Analyzer 分析包体积
class ApkAnalyzer {
fun analyze(bundlePath: String) {
val bundle = BundleFile(bundlePath)
// 分析代码
analyzeCode(bundle)
// 分析资源
analyzeResources(bundle)
// 分析依赖
analyzeDependencies(bundle)
}
}第三方工具
kotlin
// 使用 size-spy
plugins {
id 'com.getkeepsafe.sizespy' version '1.2.1'
}
// 使用 bundletool
bundletool build-bundle --output=app.aab --apk=app.apk面试考点
基础考点
1. R8 混淆原理
问题: R8 如何减小包体积?
回答:
- 代码压缩:删除未使用的代码
- 资源压缩:删除未使用的资源
- 字符串和常量优化
- 方法内联
2. 多渠道打包
问题: 如何实现多渠道打包?
回答:
- 使用 ProductFlavors
- 使用 ManifestPlaceholders
- 使用多渠道打包脚本
3. 图片优化
问题: 如何优化图片体积?
回答:
- 使用 WebP 格式
- 压缩图片质量
- 使用合适的尺寸
- 删除多余资源
进阶考点
1. 动态模块
问题: 动态特性模块的优势?
回答:
- 减小主包体积
- 按需下载
- 独立更新
- 降低更新成本
2. 依赖优化
问题: 如何优化依赖?
回答:
- 排除重复依赖
- 使用轻型替代
- 使用 BOM 管理版本
- 分析依赖树
高级考点
1. 资源压缩算法
问题: ShrinkResources 的工作原理?
回答:
- R8 分析资源引用
- 标记未使用资源
- 生成资源映射
- 删除未使用资源
2. 包体积分析
问题: 如何系统分析包体积?
回答:
- 使用 Bundle Explorer
- 使用 APK Analyzer
- 使用 lint 检查
- 使用依赖分析工具
最佳实践
包体积监控
kotlin
// 包体积监控
class PackageSizeMonitor {
fun monitorBuild() {
val buildOutput = BuildConfig.BUILD_TYPE
// 获取包大小
val packageSize = getPackageSize()
// 对比阈值
if (packageSize > MAX_SIZE) {
println("警告:包体积超过限制!")
}
}
fun getPackageSize(): Long {
val outputFile = File("build/outputs/apk/release/app-release.apk")
return outputFile.length()
}
}常见错误
1. 混淆规则错误
pro
# ❌ 错误:过度混淆
-keepclassmembers class * { *; }
# ✅ 正确:精确混淆
-keepclassmembers class com.example.** {
@androidx.annotation.Keep <fields>;
@androidx.annotation.Keep <methods>;
}2. 图片未压缩
kotlin
// ❌ 错误:未压缩
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image)
// ✅ 正确:压缩
val bitmap = BitmapFactory.decodeResource(
resources,
R.drawable.image,
BitmapFactory.Options().apply {
inSampleSize = 2
}
)总结
包体积优化是 Android 应用优化的重要环节。通过 ProGuard/R8 混淆、资源压缩、图片格式优化、九宫格图优化、去除无用资源、多渠道打包、动态特性模块和依赖优化等手段,可以显著减小包体积。
关键要点
- R8 混淆:删除未使用代码
- 资源压缩:删除未使用资源
- 图片优化:使用 WebP 格式
- 多渠道打包:使用 ManifestPlaceholders
- 动态模块:按需下载
- 依赖优化:排除重复依赖
包体积优化效果
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 包体积 | 50MB | 25MB | 50% |
| 代码体积 | 20MB | 10MB | 50% |
| 资源体积 | 30MB | 15MB | 50% |
通过系统的包体积优化,可以显著提升下载成功率,改善用户体验,降低流量消耗。