Android App瘦身方法介绍
第一章 安装包构成深度剖析
1.1 APK文件结构解剖
APK文件本质是一个ZIP压缩包,通过unzip -l app.apk
命令可查看其内部结构:
Archive: app.apkLength Method Size Cmpr Date Time CRC-32 Name
-------- ------ ------- ---- ---------- ----- -------- ----0 Stored 0 0% 2025-09-07 10:00 00000000 META-INF/1024 Defl:N 512 50% 2025-09-07 10:00 12345678 META-INF/MANIFEST.MF5242880 Defl:N 2097152 60% 2025-09-07 10:00 87654321 lib/
10485760 Defl:N 4194304 60% 2025-09-07 10:00 11223344 res/8388608 Defl:N 3355443 60% 2025-09-07 10:00 55667788 resources.arsc
41943040 Defl:N 16777216 60% 2025-09-07 10:00 99887766 classes.dex2097152 Defl:N 838861 60% 2025-09-07 10:00 44556677 assets/
-------- ------- --- -------
67108864 28613188 57% 6 files
1.2 各模块体积占比分析
通过Android Studio的APK Analyzer工具分析典型应用:
模块 | 原始大小 | 压缩后 | 占比 | 主要包含内容 |
lib/ | 52MB | 21MB | 35% | .so动态库文件 |
res/ | 10MB | 4.2MB | 20% | 图片、布局、动画等资源 |
classes.dex | 42MB | 16MB | 28% | Java/Kotlin字节码 |
resources.arsc | 8MB | 3.3MB | 12% | 资源索引表 |
assets/ | 2MB | 0.8MB | 3% | 原始资源文件 |
META-INF/ | 1MB | 0.5MB | 2% | 签名和证书文件 |
第二章 Assets目录优化策略
2.1 资源去重与压缩
问题发现:某教育类App的assets目录包含:
重复的拼音音频文件(同名不同内容)占用了15MB
未压缩的JSON配置文件(3.2MB)
解决方案:
1.实施MD5去重策略:
import hashlib
import osdef find_duplicate_files(directory):file_hashes = {}duplicates = []for root, dirs, files in os.walk(directory):for file in files:file_path = os.path.join(root, file)with open(file_path, 'rb') as f:file_hash = hashlib.md5(f.read()).hexdigest()if file_hash in file_hashes:duplicates.append(file_path)else:file_hashes[file_hash] = file_pathreturn duplicates# 实施后可删除重复文件12MB
2.启用资源压缩配置:
android {packagingOptions {// 强制压缩特定扩展名resources.excludes += "**.json"resources.excludes += "**.txt"resources.excludes += "**.mp3"// 使用更高效的压缩算法jniLibs {useLegacyPackaging = true}}
}
2.2 动态资源下载方案
案例实施:将词典数据从assets迁移至云端
原始词典文件:18MB(包含英汉/汉英/专业词典)
优化方案:仅保留高频词汇(200KB),完整词典首次使用时下载
技术实现:
class DictionaryDownloader {private val dictionaryMap = mutableMapOf<String, File>()suspend fun ensureDictionaryAvailable(type: String): File? {val localFile = File(context.filesDir, "dictionary_$type.db")if (!localFile.exists()) {val metadata = DictionaryMetadataService.getDictionaryInfo(type)if (metadata.size > 50 * 1024 * 1024) { // 大于50MB提示WiFi下载if (!isWifiConnected()) {throw LargeFileDownloadException("需要WiFi网络")}}downloadWithProgress(metadata.url, localFile) { progress ->updateNotification(progress)}// 下载完成后进行MD5校验if (!verifyFileMD5(localFile, metadata.md5)) {localFile.delete()throw CorruptedFileException("文件校验失败")}}return localFile}
}
优化效果:安装包减小17.8MB,用户可选择性下载所需词典
第三章 Native库(lib)深度优化
3.1 ABI过滤与动态交付
问题分析:某视频编辑App包含以下so文件:
lib/arm64-v8a/libffmpeg.so (15.2MB)
lib/armeabi-v7a/libffmpeg.so (12.8MB)
lib/x86/libffmpeg.so (14.1MB)
lib/x86_64/libffmpeg.so (15.5MB)
优化方案:
1.实施ABI动态分发:
android {defaultConfig {ndk {abiFilters 'arm64-v8a' // 仅打包主流架构}}splits {abi {enable true //启用ABI拆分功能。reset() //重置先前的设置,以便应用新的ABI配置。include 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'//指定要包含的架构,这里选择了4个主流架构:arm64-v8a(64位ARM架构)、//armeabi-v7a(32位ARM架构)、x86(32位Intel架构)、x86_64(64位Intel架构)。universalApk false // 不生成通用包//通用APK会包含所有架构的文件,但是这里选择不生成通用包,以便减少APK大小并仅为特定架构生成APK。}}
}// 配置动态分发
play {dynamicDelivery {nativeLibraries {// 将x86架构设置为按需下载excludeSplitFromInstall "x86"excludeSplitFromInstall "x86_64"}}
}
dynamicDelivery
: 这个配置用于设置动态交付(Dynamic Delivery)功能,允许根据设备的配置(如设备架构)按需下载应用的不同部分。nativeLibraries
: 用来指定哪些本地库(native libraries)应该通过动态交付按需下载。excludeSplitFromInstall "x86"
和excludeSplitFromInstall "x86_64"
: 这两行配置指示在安装时排除x86
和x86_64
架构的文件。也就是说,如果设备是基于x86
或x86_64
架构的,它们不会在应用初次安装时包含这些本地库文件,只有在用户实际需要的时候(例如,设备是x86
架构并且运行了需要x86
库的应用时),这些库才会按需下载。
2.实施so文件懒加载:
class NativeLibLoader {companion object {private const val LIB_VERSION = "3.4.2"private val loadedLibs = mutableSetOf<String>()fun loadFFmpeg(context: Context) {if ("ffmpeg" in loadedLibs) return// 检查本地是否存在优化版本val optimizedLib = File(context.filesDir, "libffmpeg-optimized.so")if (optimizedLib.exists()) {System.load(optimizedLib.absolutePath)} else {// 首次使用时从云端下载优化版本downloadOptimizedLibrary(context, "ffmpeg") { file ->// 使用ReLinker确保可靠加载ReLinker.loadLibrary(context, "ffmpeg", object : ReLinker.LoadListener {override fun success() {loadedLibs.add("ffmpeg")// 异步优化so文件optimizeLibraryAsync(file)}override fun failure(t: Throwable) {throw NativeLoadException("Failed to load ffmpeg", t)}})}}}private fun optimizeLibraryAsync(originalFile: File) {CoroutineScope(Dispatchers.IO).launch {val optimized = File(context.filesDir, "libffmpeg-optimized.so")// 使用strip命令移除调试符号val process = Runtime.getRuntime().exec(arrayOf("strip", "-s", originalFile.absolutePath, "-o", optimized.absolutePath))if (process.waitFor() == 0) {originalFile.delete() // 删除原始文件logOptimizationResult(originalFile.length(), optimized.length())}}}}
}
3.2 Native库压缩与加密
高级优化方案:
1.使用UPX压缩so文件:
# 压缩前
ls -lh libffmpeg.so
-rwxr-xr-x 1 user staff 15M Sep 7 10:00 libffmpeg.so# 使用UPX压缩
upx --best --lzma libffmpeg.so
# 压缩后
ls -lh libffmpeg.so
-rwxr-xr-x 1 user staff 6.8M Sep 7 10:30 libffmpeg.so
2.实施按需解压策略:
class CompressedNativeLib {fun loadCompressedLibrary(context: Context, libName: String) {val compressedFile = context.assets.open("libs/${libName}.so.xz")val decompressedFile = File(context.filesDir, "lib${libName}.so")if (!decompressedFile.exists()) {// 使用XZ解压val input = XZInputStream(compressedFile)val output = FileOutputStream(decompressedFile)input.copyTo(output)input.close()output.close()// 设置可执行权限decompressedFile.setExecutable(true)}System.load(decompressedFile.absolutePath)}
}
优化效果:Native库体积减少58%,首次加载时间增加200ms,后续启动正常
第四章 Resources.arsc终极优化
4.1 资源索引表瘦身
问题发现:某社交App的resources.arsc文件达8.3MB,包含:
资源类型 | 数量 | 体积占比 |
string | 18,542条 | 45% |
drawable | 2,847个 | 30% |
layout | 1,234个 | 15% |
style | 892个 | 10% |
优化方案:
1.实施字符串资源去重:
class StringResourceOptimizer:def __init__(self, arsc_path):self.arsc = ArscParser.parse(arsc_path)def find_duplicate_strings(self):string_pool = {}duplicates = []for string in self.arsc.strings:key = (string.value, string.locale)if key in string_pool:duplicates.append(string)else:string_pool[key] = stringreturn duplicatesdef optimize(self):duplicates = self.find_duplicate_strings()# 创建字符串映射表mapping = {}for dup in duplicates:original = next(s for s in self.arsc.strings if s.value == dup.value and s.locale == dup.locale and s not in duplicates)mapping[dup.id] = original.id# 重写resources.arscself.arsc.remap_strings(mapping)self.arsc.write("optimized.arsc")# 实施后可减少2.1MB
这个StringResourceOptimizer
类的目的是优化Android应用中的字符串资源,特别是通过处理resources.arsc
文件,减少字符串资源的冗余。下面是对这个类和其方法的逐步解释:
1. init(self, arsc_path)
构造函数初始化时会解析给定路径的resources.arsc
文件。ArscParser.parse(arsc_path)
会解析并加载.arsc
文件,这个文件包含了所有的应用资源(包括字符串资源)。
2. find_duplicate_strings(self)
这个方法负责找出所有重复的字符串资源。具体步骤如下:
使用一个字典
string_pool
来存储已遇到的字符串,每个字符串以(value, locale)
作为键,表示字符串的值和区域(例如en
、zh
等)。遍历所有的字符串资源,检查它们是否在字典中已有。如果有,就认为它是重复的,加入到
duplicates
列表中;如果没有,就将其添加到字典中。
返回值是一个包含所有重复字符串的列表。
3. optimize(self)
这个方法负责进行实际的优化工作,优化的目标是减少冗余的字符串资源。具体步骤如下:
首先调用
find_duplicate_strings()
方法,找出所有重复的字符串资源。然后,创建一个
mapping
字典,将重复字符串的ID映射到它们的“原始”字符串ID上。原始字符串是指在所有具有相同值和区域的字符串中,第一次出现的那个字符串。最后,使用
self.arsc.remap_strings(mapping)
将所有重复字符串替换为它们的原始字符串,通过这个方式去除冗余的字符串。优化后的资源会被保存到一个新的文件
optimized.arsc
中。
优化的结果
通过这个优化过程,最终可以减少resources.arsc
文件的大小。例如,上述的优化可以减少2.1MB的空间,意味着冗余的字符串资源已经被有效移除,减少了APK包的大小,提高了应用的存储效率。
总结
StringResourceOptimizer
类的功能是优化Android应用的字符串资源文件,去除重复的字符串,通过映射和重用原始字符串来减少resources.arsc
文件的大小,从而节省存储空间并提高应用性能。
2.实施资源混淆:
android {buildTypes {release {minifyEnabled trueshrinkResources true// 启用资源混淆resourceShrinker {keepResources = file('keep-resources.txt')obfuscateResources = trueresourceShortener = true}}}
}// keep-resources.txt
keep com.example.app.R.string.app_name
keep com.example.app.R.drawable.ic_launcher
keep com.example.app.R.layout.activity_main
3.高级资源优化工具:
class ArscOptimizer {fun optimize(inputFile: File, outputFile: File) {val arsc = ArscParser.parse(inputFile)// 1. 移除未使用的资源val unusedResources = findUnusedResources(arsc)arsc.removeResources(unusedResources)// 2. 合并相似字符串val stringGroups = groupSimilarStrings(arsc.strings)stringGroups.forEach { group ->val representative = group.first()group.drop(1).forEach { duplicate ->arsc.replaceString(duplicate, representative)}}// 3. 压缩字符串池arsc.compressStringPool()// 4. 优化资源索引arsc.optimizeResourceIndices()arsc.write(outputFile)}private fun findUnusedResources(arsc: ArscFile): List<Resource> {val usedResources = mutableSetOf<Resource>()// 扫描代码中的资源引用scanCodeForResources(usedResources)// 扫描布局文件scanLayoutsForResources(usedResources)// 扫描manifestscanManifestForResources(usedResources)return arsc.resources.filter { it !in usedResources }}
}
优化效果:resources.arsc从8.3MB减小至2.1MB,减少74%
第五章 META-INF优化策略
5.1 签名文件优化
问题分析:META-INF目录通常包含:
META-INF/MANIFEST.MF (512KB)
META-INF/CERT.SF (256KB)
META-INF/CERT.RSA (32KB)
META-INF/ANDROIDD.SF (128KB)
META-INF/ANDROIDD.RSA (16KB)
优化方案:
1.移除调试签名:
android {signingConfigs {release {storeFile file("release.keystore")storePassword System.getenv("KEYSTORE_PASSWORD")keyAlias System.getenv("KEY_ALIAS")keyPassword System.getenv("KEY_PASSWORD")// 启用V2签名方案v2SigningEnabled true// 禁用V1签名(仅支持Android 7.0+)v1SigningEnabled false}}
}
2.压缩签名文件:
class SignatureOptimizer {fun optimizeSignature(inputApk: File, outputApk: File) {ZipFile(inputApk).use { inputZip ->ZipOutputStream(FileOutputStream(outputApk)).use { outputZip ->inputZip.entries().asSequence().forEach { entry ->when {entry.name.startsWith("META-INF/") -> {// 压缩签名文件val compressedEntry = ZipEntry(entry.name).apply {method = ZipEntry.DEFLATEDtime = entry.timecrc = entry.crc}outputZip.putNextEntry(compressedEntry)inputZip.getInputStream(entry).use { input ->input.copyTo(outputZip)}outputZip.closeEntry()}else -> {// 直接复制其他文件outputZip.putNextEntry(ZipEntry(entry.name))inputZip.getInputStream(entry).use { input ->input.copyTo(outputZip)}outputZip.closeEntry()}}}}}}
}
3.移除不必要的证书:
# 查看证书内容
keytool -printcert -file META-INF/CERT.RSA# 移除过期证书
zip -d app.apk "META-INF/*.RSA" "META-INF/*.SF" "META-INF/*.MF"
优化效果:META-INF目录从1.2MB减少至0.3MB
第六章 Res目录全面优化
6.1 图片资源优化
6.1.1 WebP转换策略
批量转换工具:
class ImageOptimizer {fun convertToWebP(directory: File) {directory.walk().forEach { file ->when (file.extension.toLowerCase()) {"png", "jpg", "jpeg" -> {val webpFile = File(file.parent, "${file.nameWithoutExtension}.webp")// 使用cwebp工具转换val command = arrayOf("cwebp","-q", "80", // 质量80%"-m", "6", // 压缩方法6"-mt", // 多线程file.absolutePath,"-o", webpFile.absolutePath)val process = Runtime.getRuntime().exec(command)if (process.waitFor() == 0) {// 验证转换后大小val originalSize = file.length()val webpSize = webpFile.length()if (webpSize < originalSize * 0.8) { // 减少20%以上才采用file.delete()println("Converted: ${file.name} (${originalSize/1024}KB -> ${webpSize/1024}KB)")} else {webpFile.delete()}}}}}}
}
转换效果对比:
图片名称 | 原始格式 | 原始大小 | WebP大小 | 减少比例 |
bg_splash | PNG | 2.3MB | 687KB | 70% |
ic_logo | PNG | 456KB | 129KB | 72% |
img_guide1 | JPG | 1.8MB | 412KB | 77% |
6.1.2 矢量图替换方案
优化案例:将图标从PNG转为矢量图
1.原始资源分析:
drawable-hdpi/ic_settings.png (8KB)
drawable-mdpi/ic_settings.png (5KB)
drawable-xhdpi/ic_settings.png (12KB)
drawable-xxhdpi/ic_settings.png (18KB)
drawable-xxxhdpi/ic_settings.png (25KB)
总计:68KB
2.矢量图实现:
<!-- drawable/ic_settings.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"android:width="24dp"android:height="24dp"android:viewportWidth="24"android:viewportHeight="24"><pathandroid:fillColor="#757575"android:pathData="M19.1,12.9a2.8,2.8 0 0,0 0.3,-1.2 2.8,2.8 0 0,0 -0.3,-1.2l1.5,-1.5a0.8,0.8 0 0,0 0,-1.2l-1.2,-1.2a0.8,0.8 0 0,0 -1.2,0l-1.5,1.5a2.8,2.8 0 0,0 -1.2,-0.3 2.8,2.8 0 0,0 -1.2,0.3l-1.5,-1.5a0.8,0.8 0 0,0 -1.2,0l-1.2,1.2a0.8,0.8 0 0,0 0,1.2l1.5,1.5a2.8,2.8 0 0,0 -0.3,1.2 2.8,2.8 0 0,0 0.3,1.2l-1.5,1.5a0.8,0.8 0 0,0 0,1.2l1.2,1.2a0.8,0.8 0 0,0 1.2,0l1.5,-1.5a2.8,2.8 0 0,0 1.2,0.3 2.8,2.8 0 0,0 1.2,-0.3l1.5,1.5a0.8,0.8 0 0,0 1.2,0l1.2,-1.2a0.8,0.8 0 0,0 0,-1.2z"/><pathandroid:fillColor="#757575"android:pathData="M12,15.5a3.5,3.5 0 1,1 0,-7 3.5,3.5 0 0,1 0,7z"/>
</vector>
矢量图优化效果:从68KB减少至2KB,减少97%
6.2 资源混淆与去重
6.2.1 自动资源去重
class ResourceDeduplicator {fun deduplicateResources(resourceDir: File) {val resourceMap = mutableMapOf<String, MutableList<File>>()// 收集所有资源文件resourceDir.walk().forEach { file ->if (file.isFile && file.extension in listOf("png", "jpg", "webp")) {val hash = calculateImageHash(file)resourceMap.getOrPut(hash) { mutableListOf() }.add(file)}}// 处理重复资源resourceMap.values.filter { it.size > 1 }.forEach { duplicates ->val keeper = duplicates.first()val replacements = duplicates.drop(1)// 创建重定向映射replacements.forEach { duplicate ->val relativePath = duplicate.relativeTo(resourceDir).pathval keeperPath = keeper.relativeTo(resourceDir).path// 记录重定向关系redirectMap[relativePath] = keeperPath// 删除重复文件duplicate.delete()println("Removed duplicate: $relativePath -> $keeperPath")}}// 生成重定向配置generateResourceRedirectConfig(redirectMap)}private fun calculateImageHash(file: File): String {val image = ImageIO.read(file)val scaled = image.getScaledInstance(64, 64, Image.SCALE_SMOOTH)val bufferedImage = BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB)val graphics = bufferedImage.createGraphics()graphics.drawImage(scaled, 0, 0, null)graphics.dispose()// 计算感知哈希return calculatePerceptualHash(bufferedImage)}
}
第七章 DEX文件极致优化
7.1 代码混淆与瘦身
7.1.1 R8高级配置
android {buildTypes {release {minifyEnabled trueshrinkResources trueproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'// 启用R8完全模式useProguard false// 额外优化选项kotlinOptions {freeCompilerArgs += ["-Xno-param-assertions","-Xno-call-assertions","-Xno-receiver-assertions"]}}}
}// proguard-rules.pro
# 保留必要的类
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service# 优化策略
-optimizations !code/simplification/cast,!field/*,!class/merging/*,!code/allocation/variable
-optimizationpasses 5
-allowaccessmodification# 移除日志
-assumenosideeffects class android.util.Log {public static boolean isLoggable(java.lang.String, int);public static int v(...);public static int i(...);public static int w(...);public static int d(...);public static int e(...);
}# 删除行号
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
7.1.2 动态特性模块
实施案例:将视频编辑功能移至动态模块
1.创建动态模块:
// 在video-editor模块的build.gradle
apply plugin: 'com.android.dynamic-feature'android {compileSdkVersion 34defaultConfig {minSdkVersion 21targetSdkVersion 34}
}dependencies {implementation project(':app')implementation 'androidx.core:core-ktx:1.12.0'implementation 'com.google.android.play:feature-delivery-ktx:2.1.4'
}// AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:dist="http://schemas.android.com/apk/distribution"package="com.example.videoeditor"><dist:moduledist:instant="false"dist:title="@string/video_editor"><dist:delivery><dist:on-demand /></dist:delivery><dist:fusing dist:include="false" /></dist:module>
</manifest>
2.实现按需下载:
class VideoEditorModuleManager {private val moduleName = "video_editor"suspend fun requestVideoEditorModule(context: Context): ModuleInstallResult {val manager = SplitInstallManagerFactory.create(context)return try {// 检查模块是否已安装if (manager.installedModules.contains(moduleName)) {return ModuleInstallResult.AlreadyInstalled}// 创建安装请求val request = SplitInstallRequest.newBuilder().addModule(moduleName).build()// 监听安装状态val result = manager.startInstall(request).await()// 监控下载进度manager.registerListener { state ->when (state.status()) {SplitInstallSessionStatus.DOWNLOADING -> {val progress = (state.bytesDownloaded() * 100 / state.totalBytesToDownload()).toInt()updateProgress(progress)}SplitInstallSessionStatus.INSTALLED -> {onModuleInstalled()}SplitInstallSessionStatus.FAILED -> {handleInstallError(state.errorCode())}}}ModuleInstallResult.Success} catch (e: Exception) {ModuleInstallResult.Error(e)}}
}
7.2 DEX分包优化
7.2.1 手动分包策略
android {defaultConfig {multiDexEnabled true// 配置主DEXmultiDexKeepProguard file('multidex-config.pro')// 优化分包策略multiDexKeepFile file('main-dex-list.txt')}
}// multidex-config.pro
# 保留主DEX中的类
-keep class android.support.multidex.** { *; }
-keep class androidx.multidex.** { *; }# 保留Application及直接依赖
-keep class com.example.app.MyApplication { *; }
-keep class com.example.app.** extends android.app.Application { *; }# 保留启动相关的类
-keep class com.example.app.MainActivity { *; }
-keep class com.example.app.SplashActivity { *; }# main-dex-list.txt
# 明确指定主DEX包含的类
com/example/app/MyApplication.class
com/example/app/MainActivity.class
com/example/app/SplashActivity.class
com/example/app/core/BaseActivity.class
com/example/app/di/AppModule.class
7.2.2 DEX压缩与加密
class DexCompressor {fun compressDexFiles(apkFile: File, outputFile: File) {ZipFile(apkFile).use { inputZip ->ZipOutputStream(FileOutputStream(outputFile)).use { outputZip ->inputZip.entries().asSequence().forEach { entry ->when {entry.name.endsWith(".dex") -> {// 压缩DEX文件val compressedData = compressDex(inputZip.getInputStream(entry))val newEntry = ZipEntry(entry.name).apply {method = ZipEntry.STORED // 存储而非压缩size = compressedData.size.toLong()compressedSize = sizecrc = calculateCRC32(compressedData)}outputZip.putNextEntry(newEntry)outputZip.write(compressedData)outputZip.closeEntry()}else -> {// 直接复制其他文件outputZip.putNextEntry(ZipEntry(entry.name))inputZip.getInputStream(entry).use { input ->input.copyTo(outputZip)}outputZip.closeEntry()}}}}}}private fun compressDex(inputStream: InputStream): ByteArray {val dexBytes = inputStream.readBytes()// 使用LZ4压缩val compressed = LZ4Factory.fastestInstance().fastCompressor().compress(dexBytes)// 添加文件头val header = ByteBuffer.allocate(8).putInt(0x44455843) // "DEXC" 标记.putInt(dexBytes.size) // 原始大小return header.array() + compressed}
}// 运行时解压
class CompressedDexLoader {fun loadCompressedDex(context: Context, dexFile: File) {val data = dexFile.readBytes()// 验证文件头val buffer = ByteBuffer.wrap(data)val magic = buffer.intif (magic != 0x44455843) {throw IllegalArgumentException("Invalid compressed DEX file")}val originalSize = buffer.intval compressedData = data.copyOfRange(8, data.size)// 解压val decompressed = LZ4Factory.fastestInstance().safeDecompressor().decompress(compressedData, originalSize)// 加载DEXval optimizedDex = File(context.codeCacheDir, "optimized.dex")optimizedDex.writeBytes(decompressed)val dexClassLoader = DexClassLoader(optimizedDex.absolutePath,context.codeCacheDir.absolutePath,null,context.classLoader)// 使用新的ClassLoaderThread.currentThread().contextClassLoader = dexClassLoader}
}
第八章 综合优化案例
8.1 实战优化流程
8.1.1 初始状态分析
某电商App优化前数据:
原始APK大小:168MB
下载大小:142MB(压缩后)构成分析:
- lib/: 89MB (53%)
- res/: 34MB (20%)
- classes.dex: 28MB (17%)
- resources.arsc: 12MB (7%)
- assets/: 3MB (2%)
- META-INF: 2MB (1%)
8.1.2 分阶段优化实施
第一阶段:资源优化(减少45MB)
图片WebP转换:34MB → 18MB (-16MB)
矢量图替换图标:18MB → 2MB (-16MB)
资源去重:2MB → 1MB (-1MB)
移除未使用资源:1MB → 0.5MB (-0.5MB)
音频文件压缩:3MB → 1.5MB (-1.5MB)
第二阶段:Native库优化(减少52MB)
ABI分包:89MB → 37MB (-52MB)
so文件压缩:37MB → 28MB (-9MB)
第三阶段:代码优化(减少18MB)
R8混淆:28MB → 18MB (-10MB)
动态模块:18MB → 12MB (-6MB)
日志移除:12MB → 10MB (-2MB)
第四阶段:索引表优化(减少8MB)
resources.arsc优化:12MB → 4MB (-8MB)
8.1.3 最终优化结果
优化后APK大小:45MB (-123MB, 73%减少)
下载大小:38MB (-104MB, 73%减少)新构成:
- lib/: 28MB (62%)
- res/: 8MB (18%)
- classes.dex: 5MB (11%)
- resources.arsc: 3MB (7%)
- assets/: 0.5MB (1%)
- META-INF: 0.5MB (1%)
8.2 自动化优化流水线
8.2.1 CI/CD集成
# .github/workflows/app-optimize.yml
name: App Optimization Pipelineon:push:branches: [ main, develop ]pull_request:branches: [ main ]jobs:optimize:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Set up JDK 17uses: actions/setup-java@v3with:java-version: '17'distribution: 'temurin'- name: Cache Gradle packagesuses: actions/cache@v3with:path: |~/.gradle/caches~/.gradle/wrapperkey: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}- name: Run Resource Optimizationrun: |./gradlew optimizeResources./gradlew convertImagesToWebP./gradlew removeUnusedResources- name: Run Native Library Optimizationrun: |./gradlew compressNativeLibs./gradlew splitApkByAbi- name: Run Code Optimizationrun: |./gradlew minifyReleaseWithR8./gradlew shrinkReleaseResources- name: Generate Optimization Reportrun: |./gradlew generateOptimizationReportcat optimization-report.json- name: Upload Optimized APKuses: actions/upload-artifact@v3with:name: optimized-apkpath: app/build/outputs/apk/release/app-optimized.apk- name: Comment PR with Resultsif: github.event_name == 'pull_request'uses: actions/github-script@v6with:script: |const fs = require('fs');const report = JSON.parse(fs.readFileSync('optimization-report.json', 'utf8'));const comment = `## 📱 App Optimization Results| Metric | Before | After | Difference ||--------|--------|-------|------------|| APK Size | ${report.originalSize}MB | ${report.optimizedSize}MB | -${report.savedSize}MB (${report.reductionPercentage}%) || Download Size | ${report.originalDownloadSize}MB | ${report.optimizedDownloadSize}MB | -${report.savedDownloadSize}MB || DEX Methods | ${report.originalMethods} | ${report.optimizedMethods} | -${report.savedMethods} || Resources | ${report.originalResources} | ${report.optimizedResources} | -${report.savedResources} |### 📊 Detailed Breakdown${report.breakdown.map(item => `- **${item.category}**: ${item.original} → ${item.optimized} (${item.saved})`).join('\n')}`;github.rest.issues.createComment({issue_number: context.issue.number,owner: context.repo.owner,repo: context.repo.repo,body: comment});
8.2.2 优化监控告警
class OptimizationMonitor {fun checkOptimizationMetrics(apkFile: File): OptimizationReport {val report = OptimizationReport()// 检查APK大小val apkSize = apkFile.length()if (apkSize > 50 * 1024 * 1024) { // 50MB阈值report.warnings.add("APK size (${apkSize / 1024 / 1024}MB) exceeds recommended limit (50MB)")}// 分析DEX方法数val dexFiles = extractDexFiles(apkFile)val totalMethods = dexFiles.sumOf { dex ->DexFile.loadDex(dex.absolutePath, File.createTempFile("opt", "dex").absolutePath, 0).entries().toList().size}if (totalMethods > 60000) { // 方法数警告阈值report.warnings.add("DEX method count ($totalMethods) approaches 64K limit")}// 检查大文件val largeFiles = findLargeFiles(apkFile, 5 * 1024 * 1024) // 5MBif (largeFiles.isNotEmpty()) {report.largeFiles = largeFiles.map { "${it.name}: ${it.length / 1024 / 1024}MB" }}// 检查未压缩资源val uncompressedResources = findUncompressedResources(apkFile)if (uncompressedResources.isNotEmpty()) {report.optimizationSuggestions.add("Compress the following resources: ${uncompressedResources.joinToString()}")}return report}fun generateOptimizationReport(report: OptimizationReport): String {return buildString {appendLine("# App Optimization Report")appendLine("Generated: ${Date()}")appendLine()if (report.warnings.isNotEmpty()) {appendLine("## ⚠️ Warnings")report.warnings.forEach { warning ->appendLine("- $warning")}appendLine()}if (report.largeFiles.isNotEmpty()) {appendLine("## 📁 Large Files (>5MB)")report.largeFiles.forEach { file ->appendLine("- $file")}appendLine()}if (report.optimizationSuggestions.isNotEmpty()) {appendLine("## 💡 Optimization Suggestions")report.optimizationSuggestions.forEach { suggestion ->appendLine("- $suggestion")}}}}
}
第九章 高级优化技巧
9.1 人工智能辅助优化
9.1.1 智能资源压缩
import tensorflow as tf
from PIL import Image
import numpy as npclass AIImageOptimizer:def __init__(self, model_path):self.model = tf.keras.models.load_model(model_path)def optimize_image(self, image_path, target_size_kb):"""使用AI模型智能压缩图片到目标大小"""image = Image.open(image_path)# 分析图片内容features = self.extract_features(image)# 预测最佳压缩参数compression_params = self.model.predict(features)# 应用智能压缩quality = int(compression_params[0][0] * 100)method = 'WebP' if compression_params[0][1] > 0.5 else 'JPEG'# 渐进式压缩直到达到目标大小optimized_image = self.compress_with_feedback(image, method, quality, target_size_kb * 1024)return optimized_imagedef compress_with_feedback(self, image, method, initial_quality, target_bytes):"""通过反馈调整压缩参数"""quality = initial_qualitystep = 5while True:# 压缩图片compressed = self.compress_image(image, method, quality)if len(compressed) <= target_bytes:return compressed# 调整质量quality -= stepif quality < 10:# 降低分辨率image = image.resize((int(image.width * 0.9), int(image.height * 0.9)),Image.LANCZOS)quality = initial_qualitydef extract_features(self, image):"""提取图片特征用于AI模型"""# 调整大小用于分析img_array = np.array(image.resize((224, 224))) / 255.0# 提取边缘特征edges = self.detect_edges(img_array)# 提取颜色复杂度color_complexity = self.calculate_color_complexity(img_array)# 提取纹理特征texture_features = self.extract_texture_features(img_array)return np.concatenate([img_array.flatten(),edges.flatten(),color_complexity,texture_features]).reshape(1, -1)
9.1.2 预测性资源下载
class PredictiveResourceManager {private val mlModel = ResourcePredictionModel.newInstance(context)suspend fun preloadPredictedResources(userId: String) {// 获取用户行为数据val userBehavior = getUserBehavior(userId)// 使用ML模型预测所需资源val inputFeatures = TensorBuffer.createFixedSize(intArrayOf(1, 100), DataType.FLOAT32)inputFeatures.loadArray(userBehavior.toFloatArray())val outputs = mlModel.process(inputFeatures)val predictionScores = outputs.outputFeature0AsTensorBuffer.floatArray// 根据预测分数预加载资源val resourcesToPreload = predictionScores.mapIndexed { index, score -> index to score }.filter { it.second > 0.7 } // 阈值0.7.sortedByDescending { it.second }.take(10) // 最多预加载10个资源// 智能预加载resourcesToPreload.forEach { (resourceId, confidence) ->val resourceInfo = getResourceInfo(resourceId)// 仅在WiFi且电量充足时预加载大资源if (resourceInfo.size > 10 * 1024 * 1024) { // >10MBif (isWifiConnected() && isBatteryLevelOk()) {preloadResource(resourceId)}} else {preloadResource(resourceId)}}}
}
9.2 云端优化服务
9.2.1 智能APK生成
class CloudOptimizationService {suspend fun optimizeApp(apkFile: File,optimizationProfile: OptimizationProfile): OptimizedApp {// 上传APK和分析数据val uploadResponse = uploadApkWithMetadata(apkFile, optimizationProfile)// 云端进行深度优化val optimizationJob = startCloudOptimization(uploadResponse.appId,optimizationProfile)// 监控优化进度while (true) {val status = checkOptimizationStatus(optimizationJob.jobId)when (status.state) {"completed" -> {// 下载优化后的APKval optimizedApk = downloadOptimizedApp(status.resultUrl)// 获取优化报告val report = downloadOptimizationReport(status.reportUrl)return OptimizedApp(optimizedApk, report)}"failed" -> {throw OptimizationException(status.errorMessage)}"running" -> {updateProgress(status.progress, status.message)delay(5000)}}}}
}// 云端优化能力
data class CloudOptimizationCapabilities(val advancedCodeObfuscation: Boolean = true,val aiPoweredResourceCompression: Boolean = true,val nativeLibraryStripping: Boolean = true,val unusedAssetRemoval: Boolean = true,val dynamicFeatureOptimization: Boolean = true,val predictiveResourceBundling: Boolean = true
)
第十章 性能监控与持续优化
10.1 安装包大小监控
10.1.1 实时监控系统
class AppSizeMonitor {private val metricsDatabase = MetricsDatabase.getInstance(context)fun trackApkSizeChanges() {val currentSize = getCurrentApkSize()val baselineSize = getBaselineSize()val changePercent = ((currentSize - baselineSize) / baselineSize.toFloat()) * 100when {changePercent > 10 -> {// 大小增加超过10%,发送警告sendAlert(type = SizeAlertType.MAJOR_INCREASE,message = "APK size increased by ${"%.1f".format(changePercent)}% " +"(${formatSize(currentSize - baselineSize)})",currentSize = currentSize,baselineSize = baselineSize)}changePercent > 5 -> {// 大小增加超过5%,记录警告logSizeWarning(changePercent, currentSize, baselineSize)}changePercent < -5 -> {// 大小显著减少,记录优化成果logSizeImprovement(changePercent, currentSize, baselineSize)}}// 存储历史数据metricsDatabase.insertSizeMetric(ApkSizeMetric(timestamp = System.currentTimeMillis(),size = currentSize,versionCode = getVersionCode(),versionName = getVersionName(),gitCommit = getGitCommitHash()))}fun generateSizeTrendReport(days: Int = 30): SizeTrendReport {val metrics = metricsDatabase.getSizeMetrics(days)return SizeTrendReport(averageSize = metrics.map { it.size }.average().toLong(),minSize = metrics.minOf { it.size },maxSize = metrics.maxOf { it.size },trend = calculateTrend(metrics),predictions = predictFutureSize(metrics),topContributors = analyzeSizeContributors(metrics.last()),recommendations = generateRecommendations(metrics))}
}
10.1.2 大小回归测试
class SizeRegressionTest {@Testfun `APK size should not exceed baseline by more than 5 percent`() {val baselineSize = 45 * 1024 * 1024L // 45MB baselineval currentApk = File("app/build/outputs/apk/release/app-release.apk")val currentSize = currentApk.length()val increasePercentage = (currentSize - baselineSize).toDouble() / baselineSize * 100assertTrue(increasePercentage <= 5,"APK size regression detected! " +"Current: ${currentSize / 1024 / 1024}MB, " +"Baseline: ${baselineSize / 1024 / 1024}MB, " +"Increase: ${"%.1f".format(increasePercentage)}%")}@Testfun `No single resource should exceed 5MB`() {val apkFile = File("app/build/outputs/apk/release/app-release.apk")val largeFiles = analyzeApkContents(apkFile).filter { it.size > 5 * 1024 * 1024 } // 5MB thresholdassertTrue(largeFiles.isEmpty(),"Large files detected that may impact download size:\n" +largeFiles.joinToString("\n") { "${it.path}: ${it.size / 1024 / 1024}MB" })}
}
10.2 用户行为分析
10.2.1 下载转化率分析
class DownloadAnalytics {fun trackDownloadFunnel() {// 跟踪不同大小的下载转化率val sizeBuckets = listOf("0-20MB", "20-30MB", "30-40MB", "40-50MB", "50-60MB", "60MB+")sizeBuckets.forEach { bucket ->val metrics = analytics.getDownloadMetrics(bucket)logEvent("download_funnel", mapOf("size_bucket" to bucket,"download_started" to metrics.started,"download_completed" to metrics.completed,"install_started" to metrics.installStarted,"install_completed" to metrics.installCompleted,"conversion_rate" to metrics.conversionRate,"avg_download_time" to metrics.avgDownloadTime,"failure_reasons" to metrics.failureReasons))}}fun generateSizeImpactReport(): SizeImpactReport {val data = analytics.getSizeImpactData()return SizeImpactReport(// 大小对下载转化率的影响conversionRateBySize = data.map { SizeConversionData(sizeRange = it.sizeRange,conversionRate = it.conversionRate,sampleSize = it.sampleSize)},// 大小对卸载率的影响uninstallRateBySize = analyzeUninstallCorrelation(data),// 大小对评分的影响ratingImpact = analyzeRatingImpact(data),// 优化建议recommendations = generateSizeRecommendations(data),// 预测不同大小策略的影响predictions = predictSizeStrategies(data))}
}
结语
App瘦身是一个持续优化的过程,需要:
建立监控体系:实时监控安装包大小变化
制定优化规范:代码审查中纳入大小检查
自动化流程:CI/CD中集成优化步骤
数据驱动决策:基于用户行为数据调整策略
平衡用户体验:在大小和功能间找到最佳平衡点
通过系统性的优化,通常可以实现50-80%的体积减少,显著提升用户获取和留存。记住,每减少1MB,都可能带来下载转化率的提升!