Skip to content

Android 包体积优化

目录

  1. 引言
  2. [ProGuard/R8 混淆](#proguardr8 混淆)
  3. 资源压缩
  4. 图片格式优化
  5. 九宫格图优化
  6. 去除无用资源
  7. 多渠道打包
  8. 动态特性模块
  9. 依赖优化
  10. 工具使用
  11. 面试考点
  12. 最佳实践
  13. 总结

引言

包体积是 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

  1. 打开方式:

    • Build → Generate Bundle / APK
    • 完成后点击 "View" 打开
  2. 分析项:

    • 代码大小分析
    • 资源大小分析
    • 依赖分析
    • 未使用资源标记

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 混淆、资源压缩、图片格式优化、九宫格图优化、去除无用资源、多渠道打包、动态特性模块和依赖优化等手段,可以显著减小包体积。

关键要点

  1. R8 混淆:删除未使用代码
  2. 资源压缩:删除未使用资源
  3. 图片优化:使用 WebP 格式
  4. 多渠道打包:使用 ManifestPlaceholders
  5. 动态模块:按需下载
  6. 依赖优化:排除重复依赖

包体积优化效果

优化项优化前优化后提升
包体积50MB25MB50%
代码体积20MB10MB50%
资源体积30MB15MB50%

通过系统的包体积优化,可以显著提升下载成功率,改善用户体验,降低流量消耗。