APK瘦身系列
如果我问开发者他们的APP有多大,我能很确定,大部分人会去看下Android Studio生成的APK有多大,然后告诉我。这是最直接的答案。考虑以下例子:
- 当你的APP安装在用户的设备上时会占用多大的空间?
- 用户需要花费多少的网络流量来下载和安装您的APP
- 更新APP时,需要下载多少内容?
- 你的APP运行时占用了多少内存?
APK里面究竟有什么?
1 2 3 4 5 6 7
| classes.dex res resources.arsc AndriodManifest.xml libs assets META-INF
|
使用Zopfli来重新压缩APK(5.0.1可能会有crash现象)
使用zipalign -z 4 input.apk output.apk或者在gradle里加入
1 2 3 4 5 6 7 8 9 10 11 12 13
| //add zopfli to variants with release build type android.applicationVariants.all { variant -> if (variant.buildType.name == 'release') { variant.outputs.each { output -> output.assemble.doLast { println "Zopflifying... it might take a while" exec { commandLine output.zipAlign.zipAlignExe,'-f','-z', '4', output.outputFile.absolutePath , output.outputFile.absolutePath.replaceAll('\\.apk$', '-zopfli.apk') } } } } }
|
使用proguard简化dex代码
上传ProGuard mappings到play store上
三方库的Proguard配置
三方库
1 2 3 4 5
| android { defaultConfig { consumerProguardFiles "proguard-rules.txt" } }
|
跟踪你需要的依赖
过渡库的依赖
1
| ./gradlew app:dependencies
|
查看引入的依赖
使用ClassyShark来检查Dex文件
shrinkResources true
由于系统可能会出现某些错误,因此可以使用
1 2 3 4 5 6 7 8 9 10 11 12
| res/raw/keep.xml(在资源文件目录下) <?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*" />
或者 <?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="safe" tools:discard="@layout/unused2" />
|
使用ResConfigs移除无用的配置
1 2 3 4 5
| android { defaultConfig { resConfigs "en", "fr" } }
|
稀疏resources.arsc中的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| android { splits { density { enable true exclude 'ldpi', 'tvdpi', 'xxxhdpi' compatibleScreens 'small', 'normal', 'large', 'xlarge' } } }
ext.additionalDensities = ['xhdpi': ['280'], 'xxhdpi': ['420', '400', '360'], 'xxxhdpi': ['560']] import com.android.build.OutputFile
android.applicationVariants.all { variant -> // assign different version code for each output variant.outputs.each { output -> if (output.getFilter(OutputFile.DENSITY) != null && project.ext.additionalDensities.containsKey(output.getFilter(OutputFile.DENSITY))) { output.processManifest.doFirst { def manifestFile = new File(project.buildDir, "intermediates" + File.separator + "manifests" + File.separator + "density" + File.separator + output.getFilter(OutputFile.DENSITY) + File.separator + variant.buildType.name + File.separator + "AndroidManifest.xml") def manifestText = manifestFile.text for (String density : project.ext.additionalDensities.get(output.getFilter(OutputFile.DENSITY))) { manifestText = manifestText.replaceAll("</compatible-screens>", "<screen android:screenSize=\"small\" android:screenDensity=\"${density}\" />\n" + "<screen android:screenSize=\"large\" android:screenDensity=\"${density}\" />\n" + "<screen android:screenSize=\"xlarge\" android:screenDensity=\"${density}\" />\n" + "<screen android:screenSize=\"normal\" android:screenDensity=\"${density}\" />\n </compatible-screens>") } manifestFile.text = manifestText } } } }
|
ABI分割
1 2 3 4 5 6 7 8
| splits { abi { enable true reset() include 'x86', 'armeabi-v7a', 'mips' universalApk false } }
|
设置版本号
1 2 3 4 5 6 7 8 9
| // map for the version codes ext.versionCodes = ['mdpi':1, 'hdpi':2, 'xhdpi':3].withDefault {0} import com.android.build.OutputFile android.applicationVariants.all { variant -> // assign different version code for each output variant.outputs.each { output -> output.versionCodeOverride = project.ext.versionCodes.get(output.getFilter(OutputFile.DENSITY)) * 1000000 + android.defaultConfig.versionCode } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| android { ... productFlavors { xhdpi { resConfigs "xhdpi" versionCode 300001 } hdpi { resConfigs "hdpi" versionCode 200001 } mdpi { resConfigs "mdpi" versionCode 100001 } anydpi { versionCode 1 } } }
|
基于最小SDK版本的多种APK区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| android { productFlavors { prelollipop { versionCode 1 } lollipop { minSdkVersion 21 versionCode 2 } } }
android { ... flavorDimensions “density”, “version” productFlavors { xhdpi { dimension "density" resConfigs "xhdpi" versionCode 4 } //other densities here... anydpi { dimension "density" versionCode 1 } prelollipop { dimension "version" versionCode 1 } lollipop { dimension "version" minSdkVersion 21 versionCode 2 } } }
|
不能使用WebP作为启动画面的图片,因为它加载比较慢。
1 2 3 4 5 6
| android { ... aaptOptions { cruncherEnabled = false } }
|
Shape Drawable,VectorDrawables
通过下列代码控制哪些哪些图片会由vectorDrawbles生成
1 2 3 4 5 6 7 8 9 10
| android { ... defaultConfig { //if you're using Android Gradle plugin < 2.0.0 //omit the vectorDrawables block vectorDrawables { generatedDensities = ["mdpi", "hdpi", "xhdpi"] } } }
|
从6.0开始
1 2 3
| <application android:extractNativeLibs="false" >
|
总结
- 使用一套资源
- 使用
minifyEnabled混淆代码
- 使用
shrinkResources去除无用资源
- 删除无用语言资源
- 使用
tinypng有损压缩
- 使用
jpg格式
- 使用
webp
- 优化
.so文件,有些可以删除
- 缩小图片
- 使用微信资源压缩打包工具AndResGuard
- 使用
provided编译
- 使用
shape背景
- 使用
DrawableCompat
- 考虑资源在线化
- 避免重复库
- 使用更小的库
- 使用插件化
- 精简功能业务