APK瘦身

APK瘦身系列

1.解剖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')
}
}
}
}
}

2.简化代码

使用proguard简化dex代码
上传ProGuard mappings到play store上
三方库的Proguard配置

三方库

1
2
3
4
5
android {
defaultConfig {
consumerProguardFiles "proguard-rules.txt"
}
}
跟踪你需要的依赖
过渡库的依赖
1
./gradlew app:dependencies

查看引入的依赖

使用ClassyShark来检查Dex文件

3.移除没用的资源文件

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中的配置

4.通过ABI和分辨率区分多种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
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
}
}

5.通过product flavors来区分多种APK

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
}
}
}

6.图片优化Zopfli&WebP

不能使用WebP作为启动画面的图片,因为它加载比较慢。

1
2
3
4
5
6
android {
...
aaptOptions {
cruncherEnabled = false
}
}

7.图片优化Shape和VectorDrawables

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"]
}
}
}

8.从APK打开本地库

从6.0开始

1
2
3
<application
android:extractNativeLibs="false"
>

总结

  • 使用一套资源
  • 使用minifyEnabled混淆代码
  • 使用shrinkResources去除无用资源
  • 删除无用语言资源
  • 使用tinypng有损压缩
  • 使用jpg格式
  • 使用webp
  • 优化.so文件,有些可以删除
  • 缩小图片
  • 使用微信资源压缩打包工具AndResGuard
  • 使用provided编译
  • 使用shape背景
  • 使用DrawableCompat
  • 考虑资源在线化
  • 避免重复库
  • 使用更小的库
  • 使用插件化
  • 精简功能业务