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%

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