App瘦身优化

“每个字节都应该为用户体验服务,冗余即是对用户的不敬”

一、APK结构与瘦身本质分析

典型Android APK体积分布:

├─ assets/ 15-25% (游戏资源、配置文件)

├─ res/ 25-40% (图片、布局、字符串)

├─ lib/ 20-35% (SO库、NDK代码)

├─ classes.dex 10-20% (Java/Kotlin字节码)

├─ META-INF/ 1-3% (签名信息)

└─ AndroidManifest <1% (清单文件)

体积失控的架构问题:

┌─ 资源冗余 ───→ 缺少构建时清理机制

├─ 依赖爆炸 ───→ 传递依赖管理混乱

├─ 多ABI支持 ───→ CPU架构兼容性过度

├─ 图片未压缩 ───→ 资源流水线缺失

└─ 代码膨胀 ───→ 模块化边界模糊

二、资源优化策略深度解析

1、资源臃肿的表现

常见资源问题:

├─ 图片资源占用40%+ APK体积

├─ 多密度资源文件重复

├─ 未使用的drawable、layout残留

├─ 字符串资源国际化过度

└─ 动画文件过度精细化

2、资源优化的系统方案

2.1、图片资源压缩流水线


// 现象层修复
android {
  buildTypes {
      release {
          // 启用资源收缩
          shrinkResources true
          minifyEnabled true
      }
  }
}

// 本质层优化 - WebP转换
task convertToWebP {
  doLast {
      // PNG/JPG → WebP 自动转换
      // 无损压缩减少30-50%体积
  }
}

// 哲学层思考:格式选择的权衡艺术
// PNG适合透明图标,WebP适合照片,矢量图适合简单图形

minifyEnabled:

作用​:控制是否启用代码混淆和优化

设置为
true
:在构建发布版本时,会启用ProGuard/R8进行代码混淆、优化和压缩
移除未使用的代码重命名类、方法和字段名(混淆)优化字节码减小APK大小
设置为
false
(你当前的设置):禁用代码混淆和优化
保持代码可读性,便于调试开发阶段常用设置但会增加APK大小

shrinkResources:

作用​:控制是否移除未使用的资源文件

设置为
true
:自动移除项目中未使用的资源文件(图片、布局、字符串等)
显著减小APK大小只保留实际被代码引用的资源
设置为
false
(你当前的设置):保留所有资源文件
即使某些资源未被使用也会打包进APK开发阶段常用,便于随时使用各种资源会增加APK大小

2.1.1、convertToWebP实现V1 (未真正压缩)

在具体实现压缩的模块的build.gradle中实现


task convertToWebPSafe {
​    ​doLast {
​        ​println("开始安全的WebP转换优化...")

        def resDir = file("src/jxw/res")
        if (!resDir.exists()) {
            println("资源目录不存在: ${resDir.absolutePath}")
            return
        }

        // 创建备份目录
        def backupDir = new File(resDir.parentFile, "res_backup_${System.currentTimeMillis()}")
        if (!backupDir.exists()) {
            backupDir.mkdirs()
        }

        def convertedFiles = []
        def backedUpFiles = []
        def originalFilesDeleted = []

        // 遍历所有资源目录
        resDir.eachDir { ​dir ->
​            ​if (dir.name.startsWith('mipmap-') || dir.name.startsWith('drawable-')) {
                println("处理目录: ${dir.name}")

                // 创建对应的备份目录
                def backupSubDir = new File(backupDir, dir.name)
                if (!backupSubDir.exists()) {
                    backupSubDir.mkdirs()
                }

                dir.eachFile { ​file ->
​                    ​if (file.name.endsWith('.png') || file.name.endsWith('.jpg') || file.name.endsWith('.jpeg')) {
                        def fileName = file.name.substring(0, file.name.lastIndexOf('.'))
                        def webpFile = new File(dir, "${fileName}.webp")

                        // 先复制备份原始文件(真正的备份)
                        def backupFile = new File(backupSubDir, file.name)
                        try {
                            // 使用文件流进行真正的复制
                            file.withInputStream { ​input ->
​                                ​backupFile.withOutputStream { ​output ->
​                                    ​output << input
                                }
​                            }
​                            ​backedUpFiles.add(file.name)
                            println("✓ 已备份: ${file.name} → ${backupFile.absolutePath}")
                        } catch (Exception e) {
                            println("⚠ 备份失败: ${file.name} - ${e.message}")
                            return // 跳过这个文件
                        }

                        if (!webpFile.exists()) {
                            println("转换: ${file.name} → ${webpFile.name}")

                            // 这里可以集成cwebp工具进行实际转换
                            // 暂时先创建占位文件
                            webpFile.createNewFile()

                            // 检查WebP文件是否成功创建
                            if (webpFile.exists()) {
                                convertedFiles.add(file.name)

                                // 转换成功后删除原始文件
                                if (file.delete()) {
                                    originalFilesDeleted.add(file.name)
                                    println("✓ 转换成功并删除原始文件: ${file.name}")
                                } else {
                                    println("⚠ 转换成功但删除原始文件失败: ${file.name}")
                                }
                            } else {
                                println("⚠ WebP创建失败: ${webpFile.name}")
                                // 备份文件会保留在备份目录中
                            }
                        } else {
                            println("WebP文件已存在: ${webpFile.name}")
                            // 如果WebP已存在,删除原始文件
                            if (file.delete()) {
                                originalFilesDeleted.add(file.name)
                                println("✓ 删除原始文件: ${file.name}")
                            } else {
                                println("⚠ 删除原始文件失败: ${file.name}")
                            }
                        }
                    }
                }
​            ​}
        }

​        ​// 检查备份目录内容
        def backupFileCount = 0
        backupDir.eachDirRecurse { ​dir ->
​            ​backupFileCount += dir.listFiles().size()
        }

​        ​if (backupFileCount > 0) {
            println("✓ 备份完成!备份目录: ${backupDir.absolutePath}")
            println("✓ 备份了 ${backupFileCount} ​个文件")
        } else {
            println("⚠ 备份目录为空,正在清理...")
            backupDir.deleteDir()
        }

        println("WebP转换完成!")
        println("转换了 ${convertedFiles.size()} ​个文件")
        println("删除了 ${originalFilesDeleted.size()} ​个原始文件")
        println("备份了 ${backedUpFiles.size()} ​个文件")
        println("预计可减少30-50%图片体积")
    }
}

图片压缩功能:

保留备份文件:备份文件会一直保留在备份目录中,不会被删除错误处理:添加了异常捕获和更详细的日志备份目录检查:转换完成后会检查备份目录中的文件数量真正的文件复制:使用文件流(
withInputStream
/
withOutputStream
)进行真正的文件复制,而不是移动

转换完成后,备份目录会保持这样的结构:


src/jxw/res_backup_时间戳/
├── mipmap-hdpi/
│   └── ic_launcher.png (备份文件)
├── mipmap-mdpi/
│   └── icon.png (备份文件)
└── drawable-hdpi/
    └── background.jpg (备份文件)

这样即使转换过程中出现问题,你也能从备份目录中恢复所有原始文件!

2.1.2、convertToWebP实现V2 (未真正压缩)

task convertToWebPSmart {
​    ​doLast {
​        ​println("开始智能WebP转换优化...")

        def resDir = file("src/jxw/res")
        if (!resDir.exists()) {
            println("资源目录不存在: ${resDir.absolutePath}")
            return
        }

        // 创建备份目录
        def backupDir = new File(resDir.parentFile, "res_backup_${System.currentTimeMillis()}")
        if (!backupDir.exists()) {
            backupDir.mkdirs()
        }

        def convertedFiles = []
        def backedUpFiles = []
        def originalFilesDeleted = []
        def skippedFiles = []
        def totalSizeSaved = 0L
        def totalOriginalSize = 0L

        // 智能判断条件配置
        def config = [
                minFileSize: 1024, // 1KB以下文件不压缩(太小无意义)
                maxFileSize: 10 * 1024 * 1024, // 10MB以上文件不压缩(可能有问题)
                minCompressionRatio: 0.1, // 至少10%的压缩率才值得转换
                skipAlreadyWebP: true, // 跳过已经是WebP格式的文件
                skipTransparentPNG: false, // 是否跳过透明PNG(WebP对透明支持不如PNG)
                skipAnimatedImages: true // 跳过可能包含动画的图片
        ]

        // 遍历所有资源目录
        resDir.eachDir { ​dir ->
​            ​if (dir.name.startsWith('mipmap-') || dir.name.startsWith('drawable-')) {
                println("处理目录: ${dir.name}")

                // 创建对应的备份目录
                def backupSubDir = new File(backupDir, dir.name)
                if (!backupSubDir.exists()) {
                    backupSubDir.mkdirs()
                }

                dir.eachFile { ​file ->
​                    ​if (file.name.endsWith('.png') || file.name.endsWith('.jpg') || file.name.endsWith('.jpeg')) {
                        def fileName = file.name.substring(0, file.name.lastIndexOf('.'))
                        def webpFile = new File(dir, "${fileName}.webp")
                        def fileSize = file.length()
                        totalOriginalSize += fileSize

                        // 智能判断:是否需要转换
                        def shouldConvert = true
                        def skipReason = ""

                        // 判断条件1:文件大小是否在合理范围内
                        if (fileSize < config.minFileSize) {
                            shouldConvert = false
                            skipReason = "文件太小(${fileSize} bytes < ${config.minFileSize} bytes)"
                        } else if (fileSize > config.maxFileSize) {
                            shouldConvert = false
                            skipReason = "文件太大(${fileSize} bytes > ${config.maxFileSize} bytes)"
                        }

                        // 判断条件2:WebP文件是否已存在且较新
                        if (shouldConvert && webpFile.exists()) {
                            def webpSize = webpFile.length()
                            def webpTime = webpFile.lastModified()
                            def originalTime = file.lastModified()

                            // 如果WebP文件比原始文件新,且大小合理,则跳过
                            if (webpTime > originalTime && webpSize > 0) {
                                def compressionRatio = (fileSize - webpSize) * 100.0 / fileSize
                                if (compressionRatio >= config.minCompressionRatio * 100) {
                                    shouldConvert = false
                                    skipReason = "WebP文件已存在且压缩率达标(${String.format("%.1f", compressionRatio)}%)"
                                }
                            }
                        }

                        // 判断条件3:文件名特征判断
                        if (shouldConvert) {
                            def lowerName = file.name.toLowerCase()
                            // 跳过可能包含动画的文件
                            if (config.skipAnimatedImages &&
                                    (lowerName.contains("anim") || lowerName.contains("gif") ||
                                            lowerName.contains("frame") || lowerName.contains("sequence"))) {
                                shouldConvert = false
                                skipReason = "可能是动画图片"
                            }

                            // 跳过图标类小文件(通常已经优化过)
                            if (lowerName.contains("icon") && fileSize < 5 * 1024) {
                                shouldConvert = false
                                skipReason = "小图标文件已优化"
                            }
                        }

                        // 先备份原始文件(无论是否转换)
                        def backupFile = new File(backupSubDir, file.name)
                        try {
                            file.withInputStream { ​input ->
​                                ​backupFile.withOutputStream { ​output ->
​                                    ​output << input
                                }
​                            }
​                            ​backedUpFiles.add(file.name)
                            println("✓ 已备份: ${file.name}")
                        } catch (Exception e) {
                            println("⚠ 备份失败: ${file.name} - ${e.message}")
                            return // 跳过这个文件
                        }

                        if (!shouldConvert) {
                            skippedFiles.add([file: file.name, reason: skipReason])
                            println("⏭ 跳过转换: ${file.name} (${skipReason})")
                            return
                        }

                        // 执行转换
                        println("🔄 转换: ${file.name} → ${webpFile.name} (${fileSize} bytes)")

                        // 这里可以集成cwebp工具进行实际转换
                        // 模拟转换效果:假设压缩率为40%
                        def estimatedWebpSize = fileSize * 0.6
                        def estimatedSavings = fileSize - estimatedWebpSize

                        if (estimatedSavings / fileSize >= config.minCompressionRatio) {
                            // 创建WebP文件(实际应该使用cwebp工具)
                            webpFile.createNewFile()

                            if (webpFile.exists()) {
                                convertedFiles.add(file.name)
                                totalSizeSaved += estimatedSavings

                                // 转换成功后删除原始文件
                                if (file.delete()) {
                                    originalFilesDeleted.add(file.name)
                                    println("✅ 转换成功: ${file.name} → 预计节省 ${String.format("%.1f", estimatedSavings/1024)}KB")
                                } else {
                                    println("⚠ 转换成功但删除原始文件失败: ${file.name}")
                                }
                            } else {
                                println("❌ WebP创建失败: ${webpFile.name}")
                            }
                        } else {
                            println("⏭ 压缩率不足,跳过: ${file.name} (预计节省 ${String.format("%.1f", estimatedSavings/1024)}KB)")
                            skippedFiles.add([file: file.name, reason: "压缩率不足"])
                        }
                    }
                }
​            ​}
        }

​        ​// 统计结果
        def backupFileCount = 0
        backupDir.eachDirRecurse { ​dir ->
​            ​backupFileCount += dir.listFiles().size()
        }

​        ​// 输出详细报告
        println("
" + "="*50)
        println("📊 WebP转换智能报告")
        println("="*50)
        println("📁 处理目录: ${resDir.absolutePath}")
        println("💾 原始总大小: ${String.format("%.2f", totalOriginalSize/1024/1024)} MB")
        println("💰 预计节省: ${String.format("%.2f", totalSizeSaved/1024/1024)} MB")
        println("📈 压缩率: ${String.format("%.1f", totalSizeSaved*100/totalOriginalSize)}%")
        println("🔄 转换文件: ${convertedFiles.size()} ​个")
        println("⏭ 跳过文件: ${skippedFiles.size()} ​个")
        println("💾 备份文件: ${backupFileCount} ​个")

        if (skippedFiles.size() > 0) {
            println("
📋 跳过文件详情:")
            skippedFiles.take(10).each { ​skip ->
​                ​println("   - ${skip.file}: ${skip.reason}")
            }
​            ​if (skippedFiles.size() > 10) {
                println("   ... 还有 ${skippedFiles.size() - 10} ​个文件被跳过")
            }
        }

        if (backupFileCount > 0) {
            println("✅ 备份完成!目录: ${backupDir.absolutePath}")
        } else {
            backupDir.deleteDir()
            println("ℹ️ 备份目录已清理")
        }

        if (convertedFiles.size() == 0 && skippedFiles.size() > 0) {
            println("
💡 建议: 所有文件都被智能跳过,可能已经是最优状态")
        }
    }
}

图片压缩功能:在V1的基础上增加了智能判断条件

文件大小判断:
小于1KB的文件不压缩(太小无意义)大于10MB的文件不压缩(可能有问题)
压缩率判断:
至少10%的压缩率才值得转换如果WebP文件已存在且压缩率达标,则跳过
文件类型判断:
跳过可能是动画的图片文件跳过已经优化过的小图标文件
时间戳判断:
如果WebP文件比原始文件新,且压缩率合理,则跳过

这个智能版本会:

✅ 自动识别不值得压缩的小文件✅ 避免重复转换已经优化过的文件✅ 跳过可能包含动画的特殊文件✅ 提供详细的转换报告和统计信息✅ 只在真正能节省空间时才进行转换

这样既能保证压缩效果,又能避免不必要的处理时间!

运行结果如下:


> Task :Study:convertToWebPSmart
开始智能WebP转换优化...
处理目录: drawable-hdpi
✓ 已备份: account_avatar_default.png
🔄 转换: account_avatar_default.png → account_avatar_default.webp (22009 bytes)
✅ 转换成功: account_avatar_default.png → 预计节省 8.6KB
......
✓ 已备份: zwzx.png
🔄 转换: zwzx.png → zwzx.webp (19853 bytes)
✅ 转换成功: zwzx.png → 预计节省 7.8KB

==================================================
📊 WebP转换智能报告
==================================================
📁 处理目录: E:projectstudyV3.0-Study-AitutorappStudysrcjxw
es
💾 原始总大小: 7.11 MB
💰 预计节省: 2.83 MB
📈 压缩率: 39.8%
🔄 转换文件: 336 个
⏭ 跳过文件: 61 个
💾 备份文件: 397 个

📋 跳过文件详情:
   - icon_exp.png: 文件太小(576 bytes < 1024 bytes)
   - icon_room_native_top_app.png: 文件太小(254 bytes < 1024 bytes)
   - icon_study_plan.png: 小图标文件已优化
   - ic_delete_new.png: 文件太小(456 bytes < 1024 bytes)
   - ic_gm_home_popu.png: 文件太小(129 bytes < 1024 bytes)
   - ic_no_delete.png: 文件太小(492 bytes < 1024 bytes)
   - ic_room_back.png: 文件太小(316 bytes < 1024 bytes)
   - ic_room_card_2_tip.png: 文件太小(385 bytes < 1024 bytes)
   - ic_room_edit.png: 文件太小(329 bytes < 1024 bytes)
   - ic_room_home_top_notice.png: 文件太小(546 bytes < 1024 bytes)
   ... 还有 51 个文件被跳过
✅ 备份完成!目录: E:projectstudyV3.0-Study-AitutorappStudysrcjxw
es_backup_1761207071141
2.1.3、convertToWebP实现V3 (借助cwebp工具实现真正压缩)

task convertToWebPReal {
​    ​doLast {
​        ​println("开始真实的WebP转换优化...")

        def resDir = file("src/jxw/res")
        if (!resDir.exists()) {
            println("资源目录不存在: ${resDir.absolutePath}")
            return
        }

        // 创建备份目录
        def backupDir = new File(resDir.parentFile, "res_backup_${System.currentTimeMillis()}")
        if (!backupDir.exists()) {
            backupDir.mkdirs()
        }

        def convertedFiles = []
        def backedUpFiles = []
        def originalFilesDeleted = []
        def skippedFiles = []
        def failedFiles = []
        def totalSizeSaved = 0L
        def totalOriginalSize = 0L

        // 改进的cwebp工具检测
        def cwebpAvailable = false
        def cwebpPath = ""

        println("🔍 正在检测cwebp工具...")

        // 方法1: 直接尝试执行cwebp命令(最可靠的方法)
        try {
            def testProcess = ["cwebp", "-version"].execute()
            testProcess.waitFor()
            if (testProcess.exitValue() == 0) {
                cwebpPath = "cwebp"
                cwebpAvailable = true
                println("✅ 方法1: 通过直接执行找到cwebp工具")
            }
        } catch (Exception e) {
            println("❌ 方法1失败: ${e.message}")
        }

        // 方法2: 如果方法1失败,尝试使用where命令(Windows专用)
        if (!cwebpAvailable) {
            try {
                def whereProcess = ["cmd", "/c", "where", "cwebp"].execute()
                whereProcess.waitFor()
                if (whereProcess.exitValue() == 0) {
                    def output = whereProcess.text.trim()
                    if (output && !output.contains("INFO:") && output.contains("cwebp")) {
                        cwebpPath = output.split("
")[0].trim()
                        cwebpAvailable = true
                        println("✅ 方法2: 通过where命令找到cwebp工具: $cwebpPath")
                    }
                }
            } catch (Exception e) {
                println("❌ 方法2失败: ${e.message}")
            }
        }

        // 方法3: 检查常见安装路径
        if (!cwebpAvailable) {
            def commonPaths = [
                    "C:libwebpbincwebp.exe",
                    "C:Program Fileslibwebpbincwebp.exe",
                    "C:Program Files (x86)libwebpbincwebp.exe",
                    "D:libwebp-1.6.0-windows-x64bincwebp.exe",
                    "/usr/local/bin/cwebp",
                    "/usr/bin/cwebp",
                    "/opt/homebrew/bin/cwebp" // M1 Mac
            ]

            for (path in commonPaths) {
                def file = new File(path)
                if (file.exists()) {
                    cwebpPath = path
                    cwebpAvailable = true
                    println("✅ 方法3: 通过路径检查找到cwebp工具: $path")
                    break
                }
            }
        }

        // 方法4: 搜索整个系统(最后的手段)
        if (!cwebpAvailable) {
            println("🔍 正在搜索系统cwebp工具...")
            try {
                // 在Windows上搜索
                def searchProcess
                if (System.getProperty("os.name").toLowerCase().contains("windows")) {
                    searchProcess = ["cmd", "/c", "dir", "/s", "/b", "cwebp.exe"].execute()
                } else {
                    searchProcess = ["find", "/", "-name", "cwebp", "-type", "f", "2>/dev/null"].execute()
                }

                searchProcess.waitFor(5000) // 最多等待5秒
                if (searchProcess.exitValue() == 0) {
                    def output = searchProcess.text.trim()
                    if (output) {
                        def paths = output.split("
")
                        if (paths.size() > 0) {
                            cwebpPath = paths[0].trim()
                            cwebpAvailable = true
                            println("✅ 方法4: 通过系统搜索找到cwebp工具: $cwebpPath")
                        }
                    }
                }
            } catch (Exception e) {
                println("❌ 方法4失败: ${e.message}")
            }
        }

        if (!cwebpAvailable) {
            println("❌ 未找到cwebp工具,无法进行真实的WebP转换")
            println("")
            println("💡 请按以下步骤安装WebP工具:")
            println("1. 下载 libwebp: https://developers.google.com/speed/webp/download")
            println("2. 解压到 C:libwebp")
            println("3. 将 C:libwebpbin 添加到系统PATH环境变量")
            println("4. 重启命令行或IDE")
            println("5. 验证安装: 在命令行输入 'cwebp -version'")
            println("")
            println("或者运行: ./gradlew installWebPTools 查看详细安装指南")
            return
        }

        // 验证cwebp工具是否真正可用
        println("🔍 验证cwebp工具功能...")
        try {
            def verifyProcess
            if (cwebpPath.endsWith(".exe")) {
                verifyProcess = ["cmd", "/c", cwebpPath, "-version"].execute()
            } else {
                verifyProcess = [cwebpPath, "-version"].execute()
            }
            verifyProcess.waitFor()
            if (verifyProcess.exitValue() == 0) {
                println("✅ cwebp工具验证成功: ${verifyProcess.text.trim()}")
            } else {
                println("❌ cwebp工具验证失败")
                cwebpAvailable = false
            }
        } catch (Exception e) {
            println("❌ cwebp工具验证异常: ${e.message}")
            cwebpAvailable = false
        }

        if (!cwebpAvailable) {
            println("❌ cwebp工具不可用,请检查安装")
            return
        }

        // 遍历所有资源目录
        resDir.eachDir { ​dir ->
​            ​if (dir.name.startsWith('mipmap-') || dir.name.startsWith('drawable-')) {
                println("处理目录: ${dir.name}")

                // 创建对应的备份目录
                def backupSubDir = new File(backupDir, dir.name)
                if (!backupSubDir.exists()) {
                    backupSubDir.mkdirs()
                }

                dir.eachFile { ​file ->
​                    ​if (file.name.endsWith('.png') || file.name.endsWith('.jpg') || file.name.endsWith('.jpeg')) {
                        def fileName = file.name.substring(0, file.name.lastIndexOf('.'))
                        def webpFile = new File(dir, "${fileName}.webp")
                        def fileSize = file.length()
                        totalOriginalSize += fileSize

                        // 先备份原始文件
                        def backupFile = new File(backupSubDir, file.name)
                        try {
                            file.withInputStream { ​input ->
​                                ​backupFile.withOutputStream { ​output ->
​                                    ​output << input
                                }
​                            }
​                            ​backedUpFiles.add(file.name)
                            println("✓ 已备份: ${file.name}")
                        } catch (Exception e) {
                            println("⚠ 备份失败: ${file.name} - ${e.message}")
                            return // 跳过这个文件
                        }

                        // 如果WebP文件已存在且有效,跳过转换
                        if (webpFile.exists() && webpFile.length() > 100) {
                            def webpSize = webpFile.length()
                            skippedFiles.add([file: file.name, reason: "有效的WebP文件已存在(${webpSize} bytes)"])
                            println("⏭ 跳过: ${file.name} (有效的WebP文件已存在)")
                            return
                        }

                        println("🔄 开始转换: ${file.name} → ${webpFile.name}")

                        try {
                            // 使用cwebp进行真实的WebP转换
                            def cmd = []
                            if (cwebpPath.endsWith(".exe")) {
                                cmd = ["cmd", "/c", cwebpPath, "-q", "80", file.absolutePath, "-o", webpFile.absolutePath]
                            } else {
                                cmd = [cwebpPath, "-q", "80", file.absolutePath, "-o", webpFile.absolutePath]
                            }

                            println("  执行命令: ${cmd.join(' ')}")
                            def process = cmd.execute()
                            process.waitFor()

                            if (process.exitValue() == 0) {
                                // 检查生成的WebP文件是否有效
                                if (webpFile.exists() && webpFile.length() > 100) {
                                    def webpSize = webpFile.length()
                                    def savings = fileSize - webpSize
                                    totalSizeSaved += savings

                                    convertedFiles.add(file.name)
                                    println("✅ 转换成功: ${file.name} (${fileSize} → ${webpSize} bytes, 节省 ${String.format("%.1f", savings/1024.0)} KB)")

                                    // 转换成功后删除原始文件
                                    if (file.delete()) {
                                        originalFilesDeleted.add(file.name)
                                        println("   🗑️ 已删除原始文件")
                                    } else {
                                        println("   ⚠️ 删除原始文件失败")
                                    }
                                } else {
                                    failedFiles.add([file: file.name, reason: "生成的WebP文件无效或太小"])
                                    println("❌ 转换失败: 生成的WebP文件无效")
                                    if (webpFile.exists()) {
                                        webpFile.delete() // 清理无效文件
                                    }
                                }
                            } else {
                                def errorOutput = process.err.text ?: process.text ?: "未知错误"
                                failedFiles.add([file: file.name, reason: "cwebp转换失败: ${errorOutput.trim()}"])
                                println("❌ 转换失败: ${errorOutput.trim()}")
                                if (webpFile.exists()) {
                                    webpFile.delete() // 清理失败的文件
                                }
                            }
                        } catch (Exception e) {
                            failedFiles.add([file: file.name, reason: "执行错误: ${e.message}"])
                            println("❌ 转换异常: ${e.message}")
                        }
                    }
                }
​            ​}
        }

​        ​// 统计结果
        def backupFileCount = 0
        backupDir.eachDirRecurse { ​dir ->
​            ​backupFileCount += dir.listFiles().size()
        }

​        ​// 输出详细报告
        println("
" + "="*60)
        println("📊 真实WebP转换报告")
        println("="*60)
        println("🛠️  使用的工具: $cwebpPath")
        println("📁 处理目录: ${resDir.absolutePath}")
        println("💾 原始总大小: ${String.format("%.2f", totalOriginalSize/1024/1024)} MB")
        println("💰 实际节省: ${String.format("%.2f", totalSizeSaved/1024/1024)} MB")
        if (totalOriginalSize > 0) {
            println("📈 压缩率: ${String.format("%.1f", totalSizeSaved*100/totalOriginalSize)}%")
        }
        println("✅ 成功转换: ${convertedFiles.size()} ​个文件")
        println("❌ 转换失败: ${failedFiles.size()} ​个文件")
        println("⏭ 跳过文件: ${skippedFiles.size()} ​个")
        println("💾 备份文件: ${backupFileCount} ​个")

        if (failedFiles.size() > 0) {
            println("
🔴 失败文件详情:")
            failedFiles.take(5).each { ​fail ->
​                ​println("   - ${fail.file}: ${fail.reason}")
            }
​            ​if (failedFiles.size() > 5) {
                println("   ... 还有 ${failedFiles.size() - 5} ​个文件转换失败")
            }
        }

        if (backupFileCount > 0) {
            println("
✅ 备份完成!目录: ${backupDir.absolutePath}")
            println("💡 如需恢复原始文件,可从备份目录复制")
        } else {
            backupDir.deleteDir()
            println("ℹ️ 备份目录已清理")
        }
    }
}

图片压缩功能:引入cwebp工具实现正真压缩

检测cwebp工具:

多重检测方法:使用4种不同的方法来检测cwebp工具直接执行验证:最可靠的方法,直接尝试执行
cwebp -version
改进的where命令:更好的错误处理和输出解析路径检查:检查常见的安装路径系统搜索:作为最后手段搜索整个系统工具验证:在真正使用前验证cwebp工具的功能

检测方法优先级:

直接执行:
cwebp -version
(最可靠)where命令:Windows专用方法路径检查:检查常见安装位置系统搜索:全面搜索(较慢)

运行结果如下:


> Task :Study:convertToWebPReal
开始真实的WebP转换优化...
🔍 正在检测cwebp工具...
❌ 方法1失败: Cannot run program "cwebp": CreateProcess error=2, 系统找不到指定的文件。
✅ 方法3: 通过路径检查找到cwebp工具: D:libwebp-1.6.0-windows-x64incwebp.exe
🔍 验证cwebp工具功能...
✅ cwebp工具验证成功: 1.6.0
libsharpyuv: 0.4.2
处理目录: drawable-hdpi
✓ 已备份: account_avatar_default.png
🔄 开始转换: account_avatar_default.png → account_avatar_default.webp
  执行命令: cmd /c D:libwebp-1.6.0-windows-x64incwebp.exe -q 80 E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-hdpiaccount_avatar_default.png -o E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-hdpiaccount_avatar_default.webp
✅ 转换成功: account_avatar_default.png (22009 → 8414 bytes, 节省 13.3 KB)
   🗑️ 已删除原始文件
......
✓ 已备份: zwzx.png
🔄 开始转换: zwzx.png → zwzx.webp
  执行命令: cmd /c D:libwebp-1.6.0-windows-x64incwebp.exe -q 80 E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-xhdpizwzx.png -o E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-xhdpizwzx.webp
✅ 转换成功: zwzx.png (19853 → 2978 bytes, 节省 16.5 KB)
   🗑️ 已删除原始文件

============================================================
📊 真实WebP转换报告
============================================================
🛠️  使用的工具: D:libwebp-1.6.0-windows-x64incwebp.exe
📁 处理目录: E:projectstudyV3.0-Study-AitutorappStudysrcjxw
es
💾 原始总大小: 7.11 MB
💰 实际节省: 4.26 MB
📈 压缩率: 59.8%
✅ 成功转换: 397 个文件
❌ 转换失败: 0 个文件
⏭ 跳过文件: 0 个
💾 备份文件: 397 个

✅ 备份完成!目录: E:projectstudyV3.0-Study-AitutorappStudysrcjxw
es_backup_1761210674696
💡 如需恢复原始文件,可从备份目录复制
2.1.4、convertToWebP实现V4 (忽略转换后反而变大的情况)

task convertToWebPRealAuto {
​    ​doLast {
​        ​println("开始真实的WebP转换优化...")

        def resDir = file("src/jxw/res")
        if (!resDir.exists()) {
            println("资源目录不存在: ${resDir.absolutePath}")
            return
        }

        // 创建备份目录
        def backupDir = new File(resDir.parentFile, "res_backup_${System.currentTimeMillis()}")
        if (!backupDir.exists()) {
            backupDir.mkdirs()
        }

        def convertedFiles = []
        def backedUpFiles = []
        def originalFilesDeleted = []
        def skippedFiles = []
        def failedFiles = []
        def ignoredFiles = [] // 新增:记录被忽略的文件(转换后变大的文件)
        def totalSizeSaved = 0L
        def totalOriginalSize = 0L

        // 改进的cwebp工具检测
        def cwebpAvailable = false
        def cwebpPath = ""

        println("🔍 正在检测cwebp工具...")

        // 方法1: 直接尝试执行cwebp命令(最可靠的方法)
        try {
            def testProcess = ["cwebp", "-version"].execute()
            testProcess.waitFor()
            if (testProcess.exitValue() == 0) {
                cwebpPath = "cwebp"
                cwebpAvailable = true
                println("✅ 方法1: 通过直接执行找到cwebp工具")
            }
        } catch (Exception e) {
            println("❌ 方法1失败: ${e.message}")
        }

        // 方法2: 如果方法1失败,尝试使用where命令(Windows专用)
        if (!cwebpAvailable) {
            try {
                def whereProcess = ["cmd", "/c", "where", "cwebp"].execute()
                whereProcess.waitFor()
                if (whereProcess.exitValue() == 0) {
                    def output = whereProcess.text.trim()
                    if (output && !output.contains("INFO:") && output.contains("cwebp")) {
                        cwebpPath = output.split("
")[0].trim()
                        cwebpAvailable = true
                        println("✅ 方法2: 通过where命令找到cwebp工具: $cwebpPath")
                    }
                }
            } catch (Exception e) {
                println("❌ 方法2失败: ${e.message}")
            }
        }

        // 方法3: 检查常见安装路径
        if (!cwebpAvailable) {
            def commonPaths = [
                    "C:libwebpbincwebp.exe",
                    "C:Program Fileslibwebpbincwebp.exe",
                    "C:Program Files (x86)libwebpbincwebp.exe",
                    "D:libwebp-1.6.0-windows-x64bincwebp.exe",
                    "/usr/local/bin/cwebp",
                    "/usr/bin/cwebp",
                    "/opt/homebrew/bin/cwebp" // M1 Mac
            ]

            for (path in commonPaths) {
                def file = new File(path)
                if (file.exists()) {
                    cwebpPath = path
                    cwebpAvailable = true
                    println("✅ 方法3: 通过路径检查找到cwebp工具: $path")
                    break
                }
            }
        }

        // 方法4: 搜索整个系统(最后的手段)
        if (!cwebpAvailable) {
            println("🔍 正在搜索系统cwebp工具...")
            try {
                // 在Windows上搜索
                def searchProcess
                if (System.getProperty("os.name").toLowerCase().contains("windows")) {
                    searchProcess = ["cmd", "/c", "dir", "/s", "/b", "cwebp.exe"].execute()
                } else {
                    searchProcess = ["find", "/", "-name", "cwebp", "-type", "f", "2>/dev/null"].execute()
                }

                searchProcess.waitFor(5000) // 最多等待5秒
                if (searchProcess.exitValue() == 0) {
                    def output = searchProcess.text.trim()
                    if (output) {
                        def paths = output.split("
")
                        if (paths.size() > 0) {
                            cwebpPath = paths[0].trim()
                            cwebpAvailable = true
                            println("✅ 方法4: 通过系统搜索找到cwebp工具: $cwebpPath")
                        }
                    }
                }
            } catch (Exception e) {
                println("❌ 方法4失败: ${e.message}")
            }
        }

        if (!cwebpAvailable) {
            println("❌ 未找到cwebp工具,无法进行真实的WebP转换")
            println("")
            println("💡 请按以下步骤安装WebP工具:")
            println("1. 下载 libwebp: https://developers.google.com/speed/webp/download")
            println("2. 解压到 C:libwebp")
            println("3. 将 C:libwebpbin 添加到系统PATH环境变量")
            println("4. 重启命令行或IDE")
            println("5. 验证安装: 在命令行输入 'cwebp -version'")
            println("")
            println("或者运行: ./gradlew installWebPTools 查看详细安装指南")
            return
        }

        // 验证cwebp工具是否真正可用
        println("🔍 验证cwebp工具功能...")
        try {
            def verifyProcess
            if (cwebpPath.endsWith(".exe")) {
                verifyProcess = ["cmd", "/c", cwebpPath, "-version"].execute()
            } else {
                verifyProcess = [cwebpPath, "-version"].execute()
            }
            verifyProcess.waitFor()
            if (verifyProcess.exitValue() == 0) {
                println("✅ cwebp工具验证成功: ${verifyProcess.text.trim()}")
            } else {
                println("❌ cwebp工具验证失败")
                cwebpAvailable = false
            }
        } catch (Exception e) {
            println("❌ cwebp工具验证异常: ${e.message}")
            cwebpAvailable = false
        }

        if (!cwebpAvailable) {
            println("❌ cwebp工具不可用,请检查安装")
            return
        }

        // 遍历所有资源目录
        resDir.eachDir { ​dir ->
​            ​if (dir.name.startsWith('mipmap-') || dir.name.startsWith('drawable-')) {
                println("处理目录: ${dir.name}")

                // 创建对应的备份目录
                def backupSubDir = new File(backupDir, dir.name)
                if (!backupSubDir.exists()) {
                    backupSubDir.mkdirs()
                }

                dir.eachFile { ​file ->
​                    ​if (file.name.endsWith('.png') || file.name.endsWith('.jpg') || file.name.endsWith('.jpeg')) {
                        def fileName = file.name.substring(0, file.name.lastIndexOf('.'))
                        def webpFile = new File(dir, "${fileName}.webp")
                        def fileSize = file.length()
                        totalOriginalSize += fileSize

                        // 先备份原始文件
                        def backupFile = new File(backupSubDir, file.name)
                        try {
                            file.withInputStream { ​input ->
​                                ​backupFile.withOutputStream { ​output ->
​                                    ​output << input
                                }
​                            }
​                            ​backedUpFiles.add(file.name)
                            println("✓ 已备份: ${file.name}")
                        } catch (Exception e) {
                            println("⚠ 备份失败: ${file.name} - ${e.message}")
                            return // 跳过这个文件
                        }

                        // 如果WebP文件已存在且有效,跳过转换
                        if (webpFile.exists() && webpFile.length() > 100) {
                            def webpSize = webpFile.length()
                            skippedFiles.add([file: file.name, reason: "有效的WebP文件已存在(${webpSize} bytes)"])
                            println("⏭ 跳过: ${file.name} (有效的WebP文件已存在)")
                            return
                        }

                        println("🔄 开始转换: ${file.name} → ${webpFile.name}")

                        try {
                            // 使用cwebp进行真实的WebP转换
                            def cmd = []
                            if (cwebpPath.endsWith(".exe")) {
                                cmd = ["cmd", "/c", cwebpPath, "-q", "80", file.absolutePath, "-o", webpFile.absolutePath]
                            } else {
                                cmd = [cwebpPath, "-q", "80", file.absolutePath, "-o", webpFile.absolutePath]
                            }

                            println("  执行命令: ${cmd.join(' ')}")
                            def process = cmd.execute()
                            process.waitFor()

                            if (process.exitValue() == 0) {
                                // 检查生成的WebP文件是否有效
                                if (webpFile.exists() && webpFile.length() > 100) {
                                    def webpSize = webpFile.length()
                                    def savings = fileSize - webpSize

                                    // 关键修复:检查转换后文件是否变大
                                    if (webpSize < fileSize) {
                                        // 转换后文件变小,保留WebP文件
                                        totalSizeSaved += savings
                                        convertedFiles.add(file.name)
                                        println("✅ 转换成功: ${file.name} (${fileSize} → ${webpSize} bytes, 节省 ${String.format("%.1f", savings/1024.0)} KB)")

                                        // 转换成功后删除原始文件
                                        if (file.delete()) {
                                            originalFilesDeleted.add(file.name)
                                            println("   🗑️ 已删除原始文件")
                                        } else {
                                            println("   ⚠️ 删除原始文件失败")
                                        }
                                    } else {
                                        // 转换后文件变大,忽略转换结果
                                        def increase = webpSize - fileSize
                                        ignoredFiles.add([file: file.name, originalSize: fileSize, webpSize: webpSize, increase: increase])
                                        println("⚠️ 忽略转换: ${file.name} (转换后变大: ${fileSize} → ${webpSize} bytes, 增加 ${String.format("%.1f", increase/1024.0)} KB)")

                                        // 删除生成的WebP文件,保留原始文件
                                        if (webpFile.delete()) {
                                            println("   🗑️ 已删除无效的WebP文件")
                                        }
                                    }
                                } else {
                                    failedFiles.add([file: file.name, reason: "生成的WebP文件无效或太小"])
                                    println("❌ 转换失败: 生成的WebP文件无效")
                                    if (webpFile.exists()) {
                                        webpFile.delete() // 清理无效文件
                                    }
                                }
                            } else {
                                def errorOutput = process.err.text ?: process.text ?: "未知错误"
                                failedFiles.add([file: file.name, reason: "cwebp转换失败: ${errorOutput.trim()}"])
                                println("❌ 转换失败: ${errorOutput.trim()}")
                                if (webpFile.exists()) {
                                    webpFile.delete() // 清理失败的文件
                                }
                            }
                        } catch (Exception e) {
                            failedFiles.add([file: file.name, reason: "执行错误: ${e.message}"])
                            println("❌ 转换异常: ${e.message}")
                        }
                    }
                }
​            ​}
        }

​        ​// 统计结果
        def backupFileCount = 0
        backupDir.eachDirRecurse { ​dir ->
​            ​backupFileCount += dir.listFiles().size()
        }

​        ​// 输出详细报告
        println("
" + "="*60)
        println("📊 真实WebP转换报告")
        println("="*60)
        println("🛠️  使用的工具: $cwebpPath")
        println("📁 处理目录: ${resDir.absolutePath}")
        println("💾 原始总大小: ${String.format("%.2f", totalOriginalSize/1024/1024)} MB")
        println("💰 实际节省: ${String.format("%.2f", totalSizeSaved/1024/1024)} MB")
        if (totalOriginalSize > 0) {
            println("📈 压缩率: ${String.format("%.1f", totalSizeSaved*100/totalOriginalSize)}%")
        }
        println("✅ 成功转换: ${convertedFiles.size()} ​个文件")
        println("⚠️ 忽略转换: ${ignoredFiles.size()} ​个文件(转换后变大)")
        println("❌ 转换失败: ${failedFiles.size()} ​个文件")
        println("⏭ 跳过文件: ${skippedFiles.size()} ​个")
        println("💾 备份文件: ${backupFileCount} ​个")

        if (ignoredFiles.size() > 0) {
            println("
🟡 忽略文件详情(转换后变大):")
            ignoredFiles.take(5).each { ​ignore ->
​                ​println("   - ${ignore.file}: ${ignore.originalSize} → ${ignore.webpSize} bytes (增加 ${String.format("%.1f", ignore.increase/1024.0)} KB)")
            }
​            ​if (ignoredFiles.size() > 5) {
                println("   ... 还有 ${ignoredFiles.size() - 5} ​个文件被忽略")
            }
        }

        if (failedFiles.size() > 0) {
            println("
🔴 失败文件详情:")
            failedFiles.take(5).each { ​fail ->
​                ​println("   - ${fail.file}: ${fail.reason}")
            }
​            ​if (failedFiles.size() > 5) {
                println("   ... 还有 ${failedFiles.size() - 5} ​个文件转换失败")
            }
        }

        if (backupFileCount > 0) {
            println("
✅ 备份完成!目录: ${backupDir.absolutePath}")
            println("💡 如需恢复原始文件,可从备份目录复制")
        } else {
            backupDir.deleteDir()
            println("ℹ️ 备份目录已清理")
        }
    }
}

图片压缩功能:忽略掉转换后反而变大的情况

新增忽略文件统计:添加了
ignoredFiles
列表来记录转换后变大的文件文件大小比较逻辑:在转换成功后检查
webpSize < fileSize
,只有当WebP文件确实更小时才保留智能处理:如果转换后文件变大,会自动删除生成的WebP文件,保留原始PNG/JPG文件详细报告:在统计报告中显示被忽略的文件数量和详情清晰的日志:用”⚠️ 忽略转换”标识这类情况,并显示具体增加的大小

现在当遇到类似
mask_voice.png
这种转换后反而变大的情况时,系统会自动忽略转换结果,保留原始文件,避免不必要的体积增加。

运行结果如下:


> Task :Study:convertToWebPRealAuto
开始真实的WebP转换优化...
🔍 正在检测cwebp工具...
❌ 方法1失败: Cannot run program "cwebp": CreateProcess error=2, 系统找不到指定的文件。
✅ 方法3: 通过路径检查找到cwebp工具: D:libwebp-1.6.0-windows-x64incwebp.exe
🔍 验证cwebp工具功能...
✅ cwebp工具验证成功: 1.6.0
libsharpyuv: 0.4.2
处理目录: drawable-hdpi
✓ 已备份: account_avatar_default.png
🔄 开始转换: account_avatar_default.png → account_avatar_default.webp
  执行命令: cmd /c D:libwebp-1.6.0-windows-x64incwebp.exe -q 80 E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-hdpiaccount_avatar_default.png -o E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-hdpiaccount_avatar_default.webp
✅ 转换成功: account_avatar_default.png (22009 → 8414 bytes, 节省 13.3 KB)
   🗑️ 已删除原始文件
......
✓ 已备份: ic_gm_home_popu.png
🔄 开始转换: ic_gm_home_popu.png → ic_gm_home_popu.webp
  执行命令: cmd /c D:libwebp-1.6.0-windows-x64incwebp.exe -q 80 E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-hdpiic_gm_home_popu.png -o E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-hdpiic_gm_home_popu.webp
⚠️ 忽略转换: ic_gm_home_popu.png (转换后变大: 129 → 154 bytes, 增加 0.0 KB)
   🗑️ 已删除无效的WebP文件
......
✓ 已备份: zwzx.png
🔄 开始转换: zwzx.png → zwzx.webp
  执行命令: cmd /c D:libwebp-1.6.0-windows-x64incwebp.exe -q 80 E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-xhdpizwzx.png -o E:projectstudyV3.0-Study-AitutorappStudysrcjxw
esdrawable-xhdpizwzx.webp
✅ 转换成功: zwzx.png (19853 → 2978 bytes, 节省 16.5 KB)
   🗑️ 已删除原始文件

============================================================
📊 真实WebP转换报告
============================================================
🛠️  使用的工具: D:libwebp-1.6.0-windows-x64incwebp.exe
📁 处理目录: E:projectstudyV3.0-Study-AitutorappStudysrcjxw
es
💾 原始总大小: 7.11 MB
💰 实际节省: 4.47 MB
📈 压缩率: 62.8%
✅ 成功转换: 379 个文件
⚠️ 忽略转换: 18 个文件(转换后变大)
❌ 转换失败: 0 个文件
⏭ 跳过文件: 0 个
💾 备份文件: 397 个

2.2、多密度资源策略

传统多密度:

res/drawable-mdpi/

res/drawable-hdpi/

res/drawable-xhdpi/

res/drawable-xxhdpi/

智能密度选择:

resConfigs “xxhdpi” // 只保留最高密度,让系统缩放

一图胜千图: 矢量图是密度无关的终极解决方案


<vector android:width="24dp" android:height="24dp">
      <path android:fillColor="@color/primary"
            android:pathData="M12,2L2,7V10C2,16.5 6.5,22.1 12,24C17.5,22.1 22,16.5 22,10V7L12,2Z"/>
  </vector>

2.3、资源去重与清理

资源去重与清理


class ResourceAnalyzer {
  fun detectUnusedResources(): List<Resource> {
      // 静态分析 + 动态依赖图
      // 识别真正的孤儿资源
  }

  fun optimizeStringResources() {
      // 合并相同字符串
      // 移除过度翻译的语言
  }
}

// 哲学层思考:资源就像代码,需要重构
// "DRY原则同样适用于资源文件"

下面提供几种方案:

2.3.1、方案一:通过解析apk来分析未使用资源

命令行工具

aapt dump resources app.apk | grep -E “^ resource”

使用aapt2解析arsc文件

aapt2 dump resources app.apk > resources_dump.txt

解析资源表结构

aapt dump –values resources app.apk | head -100

资源引用链路:


源码 → R.java → resources.arsc → 实际资源文件
↓
编译时:所有资源都会进入arsc
运行时:只有被引用的资源才会被加载

arsc文件结构:


resources.arsc
  ├─ Resource Table Header
  ├─ String Pool (资源名称)
  ├─ Package Info
  │  ├─ Type String Pool
  │  ├─ Key String Pool
  │  └─ Resource Entries
  │     ├─ drawable entries
  │     ├─ string entries
  │     └─ layout entries

最终可以实现脚本的方式进行自动资源使用分析,脚本实现思路如下:

自动化检测工具:


#!/usr/bin/env python3
import re
import subprocess

def analyze_unused_resources(apk_path):
  # 1. 提取resources.arsc中的所有资源
  result = subprocess.run(['aapt', 'dump', 'resources', apk_path],
                        capture_output=True, text=True)

  resources = set()
  for line in result.stdout.split('
'):
      if 'resource 0x' in line:
          resource_id = re.search(r'resource (0xw+)', line)
          if resource_id:
              resources.add(resource_id.group(1))

  # 2. 分析代码中的引用
  dex_result = subprocess.run(['dexdump', '-d', apk_path],
                             capture_output=True, text=True)

  referenced = set()
  for line in dex_result.stdout.split('
'):
      resource_ref = re.search(r'const.*?(0x7fw+)', line)
      if resource_ref:
          referenced.add(resource_ref.group(1))

  # 3. 计算未使用资源
  unused = resources - referenced
  return unused

使用示例:

unused_resources = analyze_unused_resources(‘app.apk’)

print(f”未使用资源数量: {len(unused_resources)}”)

Gradle集成方案:


android {
  buildTypes {
      release {
          // 启用资源收缩
          shrinkResources true
          minifyEnabled true
          proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                       'proguard-rules.pro'
      }
  }

  // 配置Lint检查
  lintOptions {
      check 'UnusedResources'
      xmlReport true
      htmlReport true
  }
}

// 自定义任务分析未使用资源
task analyzeUnusedResources {
  doLast {
      def apkFile = file("build/outputs/apk/release/app-release.apk")
      exec {
          commandLine 'python3', 'scripts/analyze_resources.py', apkFile.absolutePath
      }
  }
}
2.3.2、方案二:通过AS自带的工具分析未使用资源

资源文件夹上右键—>Refactor—>Remove Unused Resource,之后选择Preview

切记先选Preview进行预览,否则AS会直接进行删除,有可能会误删除资源。

最终会分析出未使用资源,建议亲自检查下,再剔除。

三、代码层面瘦身的艺术

Dex文件膨胀表现:

├─ 第三方库版本不统一导致重复类

├─ 反射和注解处理器生成冗余代码

├─ Kotlin协程生成的状态机代码

├─ 多个HTTP客户端库并存

└─ 工具类和Utils文件泛滥

1、ProGuard/R8 深度混淆

现象层:基础混淆

-keep class * extends android.app.Activity

-keep class * extends androidx.fragment.app.Fragment

本质层:激进优化

-allowaccessmodification

-overloadaggressively

-repackageclasses

-optimizations !code/simplification/arithmetic

哲学层:代码即是数据,混淆即是压缩

“移除一切不被观察到的状态”

2、依赖管理的哲学


// 现象层问题:依赖地狱
  implementation 'com.squareup.okhttp3:okhttp:4.9.3'
  implementation 'com.squareup.retrofit2:retrofit:2.9.0'
  implementation 'com.android.volley:volley:1.2.1'  // 冗余!

  // 本质层解决:统一网络栈
  implementation 'com.squareup.retrofit2:retrofit:2.9.0'
  implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

  // 哲学层思考:约束生产力
  configurations.all {
      resolutionStrategy {
          force 'com.squareup.okhttp3:okhttp:4.9.3'
      }
  }

3、字节码级别优化


// 现象层:臃肿的数据类
  data class User(
      val id: Long,
      val name: String,
      val email: String,
      val avatar: String,
      val settings: UserSettings,
      val permissions: List<Permission>
  ) {
      // 编译器生成大量样板代码
      fun copy(...) = User(...)
      fun equals(other: Any?) = ...
      fun hashCode() = ...
      fun toString() = ...
  }

  // 本质层:inline类优化
  @JvmInline
  value class UserId(val value: Long)  // 零成本抽象

  // 哲学层:让编译器为你工作
  // "最好的代码是不存在的代码"

四、构建配置的深度优化


现象层:构建配置膨胀

// 典型的低效构建配置
android {
  compileSdkVersion 33
  defaultConfig {
      applicationId "com.example.app"
      minSdkVersion 21
      targetSdkVersion 33
      // 问题:所有ABI都包含
      ndk {
          abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
      }
  }
}

本质层:构建系统的智能化

1、ABI分包策略


// 现象层问题:所有架构打包在一起
  // 本质层解决:按需分发
  android {
      splits {
          abi {
              enable true
              reset()
              include 'arm64-v8a', 'armeabi-v7a'
              universalApk false  // 关键:不生成universal APK
          }
      }
  }

  // 哲学层:让用户只下载需要的代码
  // "每个CPU架构都是一个独立的世界"

2、Bundle配置优化


// 现象层:传统APK发布
// 本质层:AAB动态分发
android {
  bundle {
      density {
          enableSplit true  // 密度分包
      }
      abi {
          enableSplit true  // ABI分包
      }
      language {
          enableSplit true  // 语言分包
      }
  }
}

// 哲学层:让Google Play为你做分发优化
// "云端智能胜过本地全能"

3、构建变体的艺术


// 现象层:单一构建配置
// 本质层:多维度构建矩阵
android {
  flavorDimensions "version", "environment"

  productFlavors {
      lite {
          dimension "version"
          // 精简版:移除非核心功能
          buildConfigField "boolean", "ENABLE_ANALYTICS", "false"
      }
      full {
          dimension "version"
          buildConfigField "boolean", "ENABLE_ANALYTICS", "true"
      }

      dev { dimension "environment" }
      prod { dimension "environment" }
  }
}

// 哲学层思考:一个源码,多种产品
// "变体是需求多样性的技术表达"

五、动态加载与模块化的终极方案


现象层:模块化需求

传统单体应用问题:
├─ 启动时加载所有功能模块
├─ 低频功能占用大量空间
├─ 无法按需更新功能
└─ 团队协作边界模糊

本质层:模块化架构设计

1、Dynamic Feature Modules


// 主应用模块 (app/build.gradle)
android {
  dynamicFeatures = [':feature-camera', ':feature-payment']
}

// 动态功能模块 (feature-camera/build.gradle)
apply plugin: 'com.android.dynamic-feature'
android {
  compileSdkVersion 33
}
dependencies {
  implementation project(':app')
}

2、运行时模块加载


// 现象层:传统启动时加载
class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
      // 所有功能都已加载,占用内存
  }
}

// 本质层:按需动态加载
class ModuleManager {
  private val splitInstallManager = SplitInstallManagerFactory.create(context)

  suspend fun loadCameraModule(): Boolean {
      val request = SplitInstallRequest.newBuilder()
          .addModule("feature_camera")
          .build()

      return try {
          splitInstallManager.startInstall(request).await()
          true
      } catch (e: Exception) {
          false
      }
  }
}

// 哲学层思考:延迟加载的智慧
// "需要时才存在,不需要时就不存在"

3、模块间通信的解耦


// 现象层问题:直接依赖
class PaymentActivity {
  fun pay() {
      CameraManager.startCamera()  // 紧耦合!
  }
}

// 本质层:接口抽象
interface CameraService {
  fun startCamera()
}

class ServiceLocator {
  inline fun <reified T> get(): T? {
      return when (T::class) {
          CameraService::class -> {
              if (isModuleInstalled("feature_camera")) {
                  loadService<T>("com.app.camera.CameraServiceImpl")
              } else null
          }
          else -> null
      } as? T
  }
}

// 哲学层:服务即边界
// "模块之间只通过契约对话,不通过实现"

六、瘦身效果量化与监控体系


现象层:度量指标体系

关键瘦身指标:
├─ 下载大小 (Download Size)    目标:<50MB
├─ 安装大小 (Install Size)     目标:<100MB
├─ 内存占用 (Runtime Memory)   目标:<200MB
├─ 启动时间 (Cold Start)       目标:<2s
└─ 方法数量 (Method Count)     目标:<65K

本质层:自动化监控体系

1、构建时体积监控


// Gradle插件:体积回归检测
apply plugin: 'size-analyzer'

sizeAnalyzer {
  maxApkSizeMB = 50
  maxMethodCount = 65000

  onSizeIncrease { sizeDiff ->
      if (sizeDiff > 1024 * 1024) {  // 1MB增长阈值
          throw new GradleException("APK size increased by ${sizeDiff} bytes")
      }
  }
}

// 本质:把瘦身变成CI/CD的一部分

2、运行时性能监控


// 现象层:手动性能测试
// 本质层:自动化监控
class AppSizeMonitor {
  fun trackMemoryUsage() {
      val runtime = Runtime.getRuntime()
      val maxMemory = runtime.maxMemory() / 1024 / 1024
      val usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024

      Firebase.analytics.logEvent("memory_usage") {
          param("max_memory_mb", maxMemory)
          param("used_memory_mb", usedMemory)
          param("memory_ratio", usedMemory.toDouble() / maxMemory)
      }
  }

  fun trackStartupTime() {
      val startTime = System.currentTimeMillis()
      // ... 应用启动逻辑
      val startupTime = System.currentTimeMillis() - startTime

      if (startupTime > 2000) {
          Crashlytics.log("Slow startup: ${startupTime}ms")
      }
  }
}

// 哲学层:数据驱动的瘦身决策
// "测量是改进的前提"

3、APK分析工具集成


// 持续集成中的体积分析
class ApkAnalyzer {
  fun generateSizeReport(): SizeReport {
      return SizeReport(
          totalSize = getApkSize(),
          breakdown = mapOf(
              "dex" to getDexSize(),
              "resources" to getResourceSize(),
              "assets" to getAssetSize(),
              "native" to getNativeLibSize()
          ),
          suggestions = generateOptimizationSuggestions()
      )
  }

  fun compareWithBaseline(baseline: SizeReport): ComparisonReport {
      // 与基线版本对比,识别回归
  }
}

// 哲学层思考:透明化是优化的开始
// "看见问题才能解决问题"

七、瘦身哲学的终极升华

1、瘦身的本质规律

┌─────────────────────────────────────────────────────────────┐

│ Android瘦身的三重境界 │

├─────────────────────────────────────────────────────────────┤

│ │

│ 第一重:删除冗余 ←── “减法的艺术” │

│ ├─ 移除未使用资源 │

│ ├─ 清理重复依赖 │

│ └─ 压缩现有文件 │

│ │

│ 第二重:架构重构 ←── “设计的智慧” │

│ ├─ 模块化拆分 │

│ ├─ 动态加载 │

│ └─ 按需分发 │

│ │

│ 第三重:哲学思考 ←── “本质的洞察” │

│ ├─ 每个字节都有存在价值 │

│ ├─ 复杂度守恒定律 │

│ └─ 用户体验第一原则 │

│ │

└─────────────────────────────────────────────────────────────┘

2、瘦身的设计哲学

“App瘦身不是技术问题,是价值观问题”

极简主义:每个功能都要证明自己存在的必要性延迟加载:不要给用户不需要的东西智能分发:让平台为你做优化持续优化:瘦身是一个过程,不是一个结果

3、终极实施路线图

Phase 1: 立竿见影 (1-2周)

├─ 启用资源收缩和代码混淆

├─ WebP图片格式转换

├─ 移除未使用的依赖库

└─ 预期收益:20-30%体积减少

Phase 2: 架构重构 (4-6周)

├─ 实施动态功能模块

├─ AAB发布流程改造

├─ 多ABI分包策略

└─ 预期收益:40-50%体积减少

Phase 3: 持续优化 (长期)

├─ 自动化监控体系

├─ 体积回归检测

├─ 性能基线建立

└─ 预期收益:持续的瘦身文化

4、瘦身的禅意

代码如诗,简洁为美

资源如水,按需而流

模块如山,各司其职

用户如神,体验至上

这就是Android瘦身的全貌:从表层的文件压缩,到深层的架构重构,再到最高层的设计哲学。真正的瘦身大师,不仅能让APK变小,更能让整个产品变得优雅。

每一个字节的删除,都是对用户体验的尊重;每一次架构的重构,都是对软件工程的致敬。

八、总结

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容