“每个字节都应该为用户体验服务,冗余即是对用户的不敬”
一、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:
作用:控制是否启用代码混淆和优化
设置为 :在构建发布版本时,会启用ProGuard/R8进行代码混淆、优化和压缩
true
移除未使用的代码重命名类、方法和字段名(混淆)优化字节码减小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工具直接执行验证:最可靠的方法,直接尝试执行改进的where命令:更好的错误处理和输出解析路径检查:检查常见的安装路径系统搜索:作为最后手段搜索整个系统工具验证:在真正使用前验证cwebp工具的功能
cwebp -version
检测方法优先级:
直接执行:(最可靠)where命令:Windows专用方法路径检查:检查常见安装位置系统搜索:全面搜索(较慢)
cwebp -version
运行结果如下:
> 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,只有当WebP文件确实更小时才保留智能处理:如果转换后文件变大,会自动删除生成的WebP文件,保留原始PNG/JPG文件详细报告:在统计报告中显示被忽略的文件数量和详情清晰的日志:用”⚠️ 忽略转换”标识这类情况,并显示具体增加的大小
webpSize < fileSize
现在当遇到类似这种转换后反而变大的情况时,系统会自动忽略转换结果,保留原始文件,避免不必要的体积增加。
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变小,更能让整个产品变得优雅。
每一个字节的删除,都是对用户体验的尊重;每一次架构的重构,都是对软件工程的致敬。
八、总结
















暂无评论内容