赞
踩
为什么需要做 apk 的包体积优化?可能项目会优先做启动优化、内存优化、卡顿优化,包体积优化在实际的项目中优先级是比较低的,因为做了之后它的好处不是那么明显,特别是对还未处于稳定期的项目。
一个商业产品最主要的目的就是能为公司带来收益盈利,包体积优化让用户在更快的速度下载使用应用,聚拢收集用户群,推荐给用户需要的产品才能产生盈利。这是具体运营业务的指标,在项目精细化运营的阶段非常重要。在应用商店有很多大型的 app,它们也会推出一款 Lite 极速版本也是同样的原因。
还有一个项目做大之后,可能会和一些渠道合作商合作预装,一般合作商也是会对 app 的包体积有具体和详细的要求。
所以,包体积优化是一个项目在发展壮大后必然会经历的一个优化过程。
那么,包体积优化具体是优化什么?首先我们要了解 apk 的打包流程及整体结构,清楚 apk 的构成才能知道该怎么做优化。
上图是 Android 官方提供的 apk 打包流程图,接下来详细说明下每个节点分别代表的含义。
aapt:打包资源的工具是 aapt(The Android Asset Packaging Tool),生成 R.java、resources.arsc 和编译资源文件
aidl:将项目中的 .aidl 文件生成对应的 java 接口文件
Java Compiler:项目中所有的 java 源码、R.java 和 .aidl 文件生成的 java 接口文件编译为 .class 文件
dex:编译后的项目所有 .class 文件和三方库 .class 文件通过 dx 工具生成为可供 Dalvik 虚拟机执行的 classes.dex 文件;dx 工具主要是将 java 字节码转换成 Dalvik 字节码、压缩常量池、消除冗余信息等
apkbuilder:将 classes.dex、resources.arsc 和 res 目录(res/raw 资源会原封不动的打包进 apk,其他资源会被编译或处理)、Other Resources(assets 目录)、AndroidManifest.xml 打包成 apk 文件
Jarsigner:对 apk 进行签名,可以进行 debug 或 release 签名,android 默认为我们提供了 debug 的 keystore,该签名文件是 Users/xxx/.android/debug.keystore
zipalign:如果是 release 会对签名后的 apk 进行对齐处理。zipalign 是 android 平台上整理 apk 文件的工具,它对 apk 中未压缩的数据进行 4byte 对齐,对齐后就可以使用 mmap 函数读取文件,可以像读取内存一样对普通文件进行操作,如果没有 4byte 对齐,就必须显式的读取,这样比较缓慢并且会耗费额外的内存。它位于 sdk 目录 build-tools/zipalign.exe
简单梳理下整体打包过程:
经过 aapt 工具生成 R.java 文件和 resources.arsc 资源映射表,并编译项目中的资源
将项目中的 java 源码、R.java 和 .aidl 生成的 java 接口文件编译为 .class 文件
将项目中编译的 .class 文件和三方库的 .class 文件经过 dx 工具生成 .dex 文件
将 .dex 文件、resources.arsc、res 资源、AndroidManifest.xml 打包成 apk 文件
将 apk 文件签名、对齐,生成签名后的 apk
在 Android 中 apk 其实就是一个压缩文件,内部包含不同类型的文件。通过 Android Studio Analyze Apk 将一个打包好的 apk 文件拖进 IDE 就可以查看 apk 的结构:
lib:存放针对处理器层面的被编译的代码,其实就是存放支持对应 CPU 架构的 so 文件。这个目录针对每个平台类型都有一个子目录,例如 armeabi、armeabi-v7a、arm64-v8a、X86、x86_64 等;目前 armeabi-v7a 和 arm64-v8a 是主流平台,一般适配这两个平台类型就足够了
assets:存放能够通过 AssetManager 获取的资源
AndroidManifest.xml:存放项目所有的配置清单集合,项目中多个 module 及三方库的 AndroidManifest.xml 打包后,最终会合并为一个 AndroidManifest.xml。该文件列出了应用名称、版本、访问权限等信息
META-INF:存放 CERT.SF、CERT.RSA 签名文件 和 MANIFEST.MF 文件。它们主要用于打正式包时校验 apk 是否被人篡改,保证安全性
classes.dex:存放能被 Dalvik/ART 虚拟机理解的 dex 文件格式的类,可以理解 dex 文件就是项目代码编译为 class 文件后的集合
res:存放资源文件,例如布局文件、图片等
resources.arsc:存放被编译的资源。该文件包含了 res/values 目录所有配置的 xml 内容。经过 aapt 打包后生成的 R.java 和 resources.arsc,根据 R.java 文件的 id 就可以找到在 resources.arsc 记录的资源,例如 xml 布局文件和图片的名称和路径。所以 resources.arsc 又称为资源映射表。它的作用就是配合 R.java 完成对应数据提取的辅助信息
根据 apk 结构可以分析一个应用的资源主要可以区分为三种:项目代码(一般是 java/kotlin 代码)、资源(布局文件、图片等)和支持对应 CPU 架构的 so 文件。所以包体积的优化也是围绕着这三种资源做优化。
在实际的项目开发时,很多时候为了参考竞品应用的某些实现和使用了哪些第三方 sdk,我们都会用到反编译解析出资源文件和源码,不过线上 apk 的源码一般都已经加固和混淆过,所以源码可能会难以阅读。
要参考竞品的实现一般我们需要从设备中导出 apk,具体 adb 命令如下:
// 打开需要导出的 app,adb 查看 app 包名
adb shell dumpsys window | grep Focused
// 查看 apk 安装的路径
adb shell pm path <package name>
或
adb shell pm -lf | grep <package name>
// adb 导出 apk
adb pull [apk path] [apk name]
apk 反编译有多种方式,这里只介绍常用的两种:ApkTool 和 Android Studio Analyze Apk。
Windows ApkTool 工具下载地址【提取码: 3bf4】
ApkTool 一般指的是三个工具:apktool、dex2jar 和 jd-gui。
1、apktool:资源文件获取,可以提取出图片文件和布局文件查看
// app-debug.apk 就是需要被反编译的 apk 文件
java -jar apktool_2.3.4.jar d -f app-debug.apk
2、dex2jar:将 apk 反编译为 java 源码,把 classes.dex 转化为 jar 文件
将 apk 直接解压获取 classes.dex
将 classes.dex 拷贝到 dex2jar 目录覆盖已有的 classes.dex,如果目录已经有 classes.dex2jar.jar 最好是先删除
执行命令 d2j-dex2jar.bat classes.dex,在当前目录会生成 classes-dex2jar.jar
3、jd-gui:查看 dex2jar 反编译成 jar 文件的源码(以 Windows 系统操作为例)
在 jd-gui 所在目录 按住shift+右键 -> 在此处打开 powershell
执行命令 java -jar .\jd-gui.exe 运行程序( powershell 关闭 jd-gui 也会一起关闭)
将在 dex2jar 转换的 classes-dex2jar.jar 文件拖入 jd-gui 查看源码
Android Studio 2.2 之后提供了 Analyze Apk,它具有如下功能:
可以直观的查看 apk 的组成,比如大小、占比等
查看 dex 文件的组成
对不同的 apk 进行对比分析
从上面的 apk 打包流程我们了解到,apk 打包出来源代码的 class 都整合存放在一个个 classes.dex 文件中。那么为什么在 Android 要把字节码文件都打包为 classes.dex 而不是直接将字节码文件打包为 .jar 来使用呢?
.dex 文件是在 Android 中 Dalvik 的可执行文件,它包含应用程序的全部操作指令以及运行时数据。因为 Dalvik 是针对嵌入式设备设计的 Java 虚拟机,所以 dex 文件与标准的 class 文件在结构设计上有着本质区别。
上面也有提到,当 java 程序被编译成 class 文件后,还需要使用 dx 工具将所有的 class 文件整合到一个 dex 文件中,这样 dex 文件就将原来每个 class 文件种都有的共有信息合成了一体,这样做能 保证其中的每个类都能够共享数据,一定程序上 降低了信息冗余,同时 文件结构更加紧凑。
与传统 jar 文件相比, dex 文件的大小能够缩减 50% 左右。class 文件和 dex 文件对比如下:
在 Android 中,dex 文件一般会占很大比重,dex 数量越多,app 的安装时间也会越长。所以,在做包体积优化时,要尽可能地减少 dex 文件数量。
apk 组成部分中关于源代码文件最终是会被 dx 工具将项目所有的字节码文件和第三方库的字节码文件 .class 合并为一个 classes.dex,当然如果超过方法数 65536 时,当我们尝试用 gradle 构建生成 apk 时会抛出以下日志信息:
上面图中提示我们方法数已经超过 65536 不能将所有的 class 文件放在同一个 dex 文件中。
Android 平台的 Dalvik 执行 dex 程序时,使用的是 short 类型来索引 dex 文件的方法,这意味着单个 dex 文件可被引用的方法总数被限制为 64x1024 即 65536。方法数量统计包括 Android Framework 的方法、第三方库的方法、项目代码的方法。
为突破这个限制,我们会需要使用到 MultiDex 分成多个 classes.dex 文件。
Android 5.0 之前使用 Dalvik 运行时执行应用代码,默认 Dalvik 限制每个 apk 只能有一个 classes.dex 文件。为突破这个限制,需要添加 MultiDex 依赖支持管理额外的 dex 文件。
在主 app module 的 build.gradle 文件添加 MultiDex 依赖:
dependencies {
// implementation 'com.android.support:multidex:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
}
在项目中使用 MultiDex:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.multidex.myapplication"> <application ... // android:name="android.support.multidex.MultiDexApplication" android:name="androidx.multidex.MultiDexApplication"> ... </application> </manifest> 或 public class MyApplication extends MultidexApplication { ... } 或 public class MyApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } }
Android 5.0 及更高版本使用支持从 apk 中加载多个 dex 文件的 ART 运行时机制,在应用安装时,加载 classes(n).dex 文件并编译成一个 .oat 文件以支持在 Android 设备上运行。
Note: While using Instant Run, Android Studio automatically configures your app for multidex when your app’s minSdkVersion is set to 21 or higher. Because Instant Run only works with the debug version of your app, you still need to configure your release build for multidex to avoid the 64K limit.
上面翻译的意思是,如果使用 Instant Run,当 app 的 minSdkVersion >= 21 时,Android Studio 会自动配置支持 MultiDex,但是仅 debug 版本有效, release 版本仍然需要配置 MultiDex 突破 64k 限制。
Android SDK Build Tools 21.1 或更高版本上支持在 Gradle 开启 MultiDex,配置步骤如下:
android {
defaultConfig {
multiDexEnabled true
}
}
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.multidex.myapplication"> <application ... // android:name="android.support.multidex.MultiDexApplication" android:name="androidx.multidex.MultiDexApplication"> ... </application> </manifest> 或 public class MyApplication extends MultidexApplication { ... } 或 public class MyApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } }
重新构建生成 apk 时就会将所有 class 文件拆分成多个 dex 文件:
上面我们就有提到,在项目开发中要尽量的减少 dex 文件。dex 文件数量如何减少?
ProGuard 一般代指代码混淆,代码混淆也被称为花指令,它将计算机程序的代码转换为一种功能上等价,但是难以阅读和直接理解的形式。代码混淆的形式主要有三种:
将代码中的各个元素,比如类、函数、变量的名字改变成无意义的名字。例如将 hasValue 转换成单个的字母 a,这样,反编译阅读的人就无法通过名字来猜测用途
重写代码中的部分逻辑,将它变成功能上等价但又难以理解的形式。比如它会改变循环的指令、结构体
打乱代码的格式。比如多加一些空格或删除空格,或者将一行代码写成多行,将多行代码改成一行
在 Android sdk 里面集成了一个工具——Proguard,在 Android 中它作为压缩、优化、混淆和预先校验的工具,在实际项目打包 release 版本时都会使用。它的主要作用大概可以概括为两点:
代码压缩:它可以检测并移除未使用到的类、方法、字段以及指令、冗余代码,并能够对字节码进行深度优化。最后,它还会将类中的字段、方法、类的名称改成简短无意义的名字
安全:增加代码被反编译的难度,一定程度上保证代码的安全
所以,混淆不仅是保证 Android 程序源码安全的第一道门槛,而且在一定程度上,使用它能够减小优化字节码的大小。
优化字节码的处理流程如下所示:
整个流程分为四个步骤:
1、压缩(shrink):默认开启,以减少应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行,因为优化后可能会再次暴露一些未被使用的类和成员。在 proguard-rules.pro 可以添加规则关闭压缩
-dontshrink
2、优化(optimize):默认开启,在字节码级别执行优化,让应用运行更快。在 proguard-rules.pro 可以添加规则关闭优化
-dontoptimize
-optimizationpasses n 表示 proguard 对代码进行迭代优化的次数,Android 一般为 5
3、混淆(obfuscate):默认开启,增大反编译难度,类和类成员会被随机命名,除非用优化字节码等规则进行保护。在 proguard-rules.pro 可以添加规则关闭混淆
-dontobfuscate
4、预校验(preverify):在 proguard-rules.pro 可以添加规则关闭预校验
-dontpreverify
在 Android 中开启代码混淆很简单,只要在项目的 build.gradle 文件将 minifyEnabled 设置为 true,然后在 proguard-rules.pro 配置 混淆规则:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
需要注意的是,不要在debug环境开启混淆。因为混淆的过程相对来说是比较慢的,在 debug 环境开启混淆没什么用,它反而会拖慢编译的速度。
混淆打包之后,默认会在工程目录的 app/build/outputs/mapping/release
下生成一个 mapping.txt
文件,这就是混淆规则。
很多时候我们可能在线上捕捉到一些异常崩溃日志信息,但都是混淆后的代码不能很好的定位到具体位置代码行号,就需要借助 mapping.txt 反推回原始代码。在 Android sdk 已经为我们提供了混淆解析的工具,该工具存放在 sdk/tools/proguard/bin/proguardgui.bat
(Mac 平台是 proguardgui.sh):
在项目中使用 ProGuard 代码混淆要特别注意两种场景的处理:
使用了反射
有使用类似 gson 工具的 bean 数据类
反射使用的 Class.forName(“classname”) 获取 Class,gson 会通过名称序列化反序列化,因为 ProGuard 代码混淆后类名称和成员变量名称会被修改为无意义的字母,这将会导致失效,要记得 keep。
实际项目开发可能会是团队成员各自负责对应的功能模块,也会使用各种各样的第三方库,比如图片库,某个团队成员在自己开发的功能模块习惯使用 Fresco,另外一个使用 Glide,再来一个使用 Picasso,一个项目就可能存在了三个功能相同的三方库,增加了包体积。
所以,在三方库的代码优化时,我们要做到:
androidTestImplementation ('com.android.support.test:runner:1.0.2'){
exclude group:'com.android.support',module: 'support-annotations'
}
androidTestImplementation ('com.android.support.test.espresso:espresso-core:3.0.2'){
exclude group:'com.android.support',module: 'support-annotations'
}
但这种处理方式并不优雅,当有多个三方库都冲突时就要写很多次。比较优雅的处理方式是写一个依赖规则文件统一版本,比如名为 dependency_rule.gradle,在冲突的 module 使用 apply from: '../dependency_rule.gradle'
引用(dependency_rule.gradle 放在项目根目录下):
dependencies {
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def group = details.requested.group
def name = details.requested.name
if (group == 'androidx.annotation' && name.startsWith('annotation')) {
// 设置使用的统一版本
details.useVersion '1.1.0'
}
}
}
}
尽量选择体积较小也能实现相同功能的三方库:比如 Fresco 整个引入会导致包大小增加很多,而 Picasso 却只增加了不到 100kb。对包体积有要求,我们应该尽量选择方法数较少又能实现相同功能的三方库。
三方库只引入需要的代码:比如 Fresco 就将图片加载的各种功能拆分出来,支持 webp、gif 等,按需引入代码;如果三方库没有结构剥离,可以自己修改源码,只提取出来需要的功能
移除冗余代码时会经常碰到下面两个问题:
业务代码只增不减
代码太多不敢删除
一般并不建议自己肉眼查找冗余代码删除(除非是小项目对项目代码很熟悉)。
在 Android Studio 已经集成了 Lint,使用 Lint 可以非常方便的查找冗余的代码。Lint 放在 Android Studio 顶部 Code 的 Analyze Code -> Run Inspection by Name... -> unused class -> Whole Project
,等待分析完成后就会显示出项目中没有使用的 java 类文件。
使用 Lint 也有弊端:它不能分析反射创建使用的类。如果一个类它只是在反射时通过 Class.forName() 被创建使用,Lint 是不能分析出来的,这一点需要特别留意。
相比代码文件,apk 开发会更多的使用图片资源,比如一张 320x360 的 png 格式的背景就有 30-40KB,而同样的图片背景在 webp 格式下只有 2KB,所以对图片做压缩选择不同的图片格式,它的收益明显是更大的。图片越多,没有在对应场景选择好正确的图片格式,那么包体积将会增大很多。
目前在 android 主流使用的图片格式主要有四种:jpg、png、webp 和 svg。
要做好图片体积优化,我们需要了解各种图片格式的优缺点,针对不同业务使用不同的图片格式。
图片格式 | 描述 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
jpg/jpeg | jpeg 图片格式的设计目标,是在不影响人类可分辨的图片质量的前提下,尽可能的压缩文件大小。这意味着 jpeg 去掉了一部分图片的原始信息,即 jpeg 进行了有损压缩 | 采用了直接色,得益于更丰富的色彩, jpeg 非常适合用来存储照片,用来表达更生动的图像效果,比如颜色渐变 | 没有 alpha 通道,在保存图片时会自动保存为白色 | 高保真图像 |
png | png 支持 alpha 通道,png 是无损压缩的,显示效果比较好,但同样的图片大小也会比 jpg 大 | 支持 alpha 通道,显示效果比较好 | 中规中矩,没太大问题 | logo、图标 |
webp | webp 同时支持有损和无损压缩,从名字可以看出它是为 web 而生,相同质量的图片 webp 具有更小的文件体积。网站充斥大量图片,降低每一个文件大小将大大减少浏览器和服务器之间的数据传输量,今儿降低访问延迟,提升访问体验 | 1、无损压缩情况下,相同质量的 webp 图片,文件大小比 png 小 26% 2、有损压缩情况下,具有相同精度的 webp 图片,文件大小比 jpg 小 25%-34% 3、webp 格式支持 alpha 通道,一个无损压缩的 webp 图片要支持透明度只需要 22% 的额外文件大小 | 不能完全兼容所有 平台 | 网络传输 |
svg | svg 是无损的矢量图,是矢量图中的一种,它的特点是 使用 xml 描述图片 | svg 图片由直线和曲线以及绘制它们的方法组成,放大 svg 时还是直线和曲线不会有像素点,意味着 不会失真;只是 xml 文件描述,文件体积小 | 1、文件承载的数据不能直接给 GPU 渲染,而是要先进行一次生成;加载速度比其他图片格式快,但渲染速度比其他图片格式慢,图片越大渲染越慢 2、不支持投影、模糊、颜色矩阵以及文本,使用场景有限 | 配合 selector 做颜色变更的纯色小图标 |
svg 矢量图在 android 使用 VectorDrawable 加载。
在 Google I/O 2016 中讲到了如何选择相应的图片格式:
如果能用 VectorDrawable 来表示的话,优先使用 VectorDrawable
如果不支持 VectorDrawable,则优先用 webp
如果不支持 webp,则优先使用 png,png 图片格式主要用在展示透明或简单的图片,其他场景可以使用 jpg 格式
在项目中使用 VectorDrawable 有以下几点注意事项:
必须通过 app:srcCompat 属性来使用 svg,如果通过 android:src 则在低版本手机上会出现不兼容的问题
可能会不兼容 selector,在 Activity 中手动兼容 AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
即可
不兼容第三方库
性能问题:当 VectorDrawable 比较简单时,效率肯定比 Bitmap 高,比较复杂时则效率会不如 Bitmap
但需要注意的是,这并不是让你为了减少图片体积就将项目所有图片都换成 svg 矢量图!建议只有在显示纯色小 icon 时才考虑使用 svg 格式。
如果不是纯色小 icon 类型的图标,则建议使用 webp,只要 app 的 minSdkVersion >= 14 即 Android 4.0+ 即可。webp 不仅支持透明度,而且压缩率比 jpg 更高,在相同画质下体积更小。但是,只有 Android 4.2.1+ 才支持显示含透明度的 webp。可以在 Android Studio 右键图标-> Convert to WebP 即可直接转换图片格式。
简单总结下上面各种图片格式的使用场景:
对透明通道没有要求的高保真大图要存储到磁盘的图片,优先使用 jpg
一般 UI 给我们的都是 jpg 或 png 格式,如果是一些普通图标切图的话就使用 png
如果有用到网络传输且图片比较大的场景,比如和服务器通信获取网络图片,优先使用 webp,其次是 png
图片体积小要求不高的纯色小图标可以使用 svg
apk 的资源主要包括图片、xml 文件,与冗余代码一样,它也可能遗留了很多旧版本当中使用而新版本中不使用的资源。Android Studio 自带的 Lint 为我们提供了一键移除所有冗余资源的功能。可以在 Android Studio 任意文件 右键 -> Refractor -> Remove Unused Resources -> preview
,预览找到冗余的资源,去除冗余资源:
但是这种方式不建议使用!因为如果存在动态获取资源 id 的方式,那么这个资源也会被认为没有使用直接被删除。
Lint 还提供了手动分析冗余资源的操作,在 Android Studio 顶部 Code 的 Analyze Code -> Run Inspection by Name... -> unused resources -> Whole Project
,等待分析完成后就会显示出项目中没有使用的资源文件:
需要注意的是,这种方式只会分析出来有直接引用的资源,代码动态使用的资源也会被分析为冗余资源:
// 代码动态使用的资源,如果没有直接引用也会被认为是冗余资源
int identifier = getResources().getIdentifier("icon_1", "drawable", getPackageName());
基于这种情况,在分析出来后,建议是一个个文件对比删除冗余的资源。
版本迭代过程中,不但有废弃代码冗余,肯定会有冗余的图片存在。我们在打正式包的时候一般会添加上 shrinkResources 配置,它需要配合 minifyEnabled 同时使用:
build.gradle
android {
buildTypes {
release {
minifyEnabled true
// 开启资源缩减
shrinkResources true
}
}
}
将 shrinkResources 配置为 true,用这种方式也能扫描到代码动态使用的资源,在打包的时候会自动清除掉冗余的资源;但需要注意的是,这里的清除并不是删除资源文件,而是将资源内的数据剔除。
当 app 项目涉及到国际化时,一般我们都会配套在 res 目录根据不同的语言创建不同的目录和 strings.xml 文件;如果我们只想打包一个只在中国上线的 apk 包,因为国际化语言资源目录都写在 res 目录,所以默认情况下会将所有的语言资源都打包到 apk 中,这就导致了白白增加了 apk 体积。
我们可以通过 resConfigs 配置使用哪些语言,从而让构建工具移除指定语言之外的所有资源:
android {
defaultConfig {
resConfigs "zh-rCN" // 支持多个可以用逗号分割
}
// 多渠道的配置方式
flavorDimensions "default"
productFlavors {
huawei {
resConfigs "zh-rCN"
}
}
}
同样的方式可以配置应用需要的图片资源文件,比如 xhdpi:
android {
defaultConfig {
resConfigs "zh-rCN", "xhdpi" // 只需要中文字符串、xhdpi 目录下的资源
}
flavorDimensions "default"
productFlavors {
huawei {
resConfigs "zh-rCN", "mdpi" // 只需要中文字符串、mdpi 目录下的资源
}
}
}
需要注意的时,如果在较高版本的 gradle 使用 resConfigs 尝试配置多个图片资源目录,会有如下构建失败提示:
Cannot filter assets for multiple densities using SDK build tools 21 or later. Consider using apk splits instead.
上面提示要使用 splits 替代。比如需要配置 mdpi 和 xhdpi,可以在 build.gradle 如下配置:
android {
...
splits {
// Configures multiple APKs based on screen density.
density {
// Configures multiple APKs based on screen density.
enable true
reset()
include "mdpi", "xhdpi"
}
}
}
配置多个图片资源目录并不是生成的一个包含 mdpi 和 xhdpi 的 apk,而是根据配置的资源图片目录生成对应的 apk。比如上面配置了 mdpi 和 xhdpi,那么就会生成一个只有 mdpi 图片资源目录的 apk 和一个只有 xhdpi 图片资源目录的 apk,是两个不同资源目录的 apk。
一些更具体的用法可以查看官方文档:
- 构建多个apk
https://developer.android.google.cn/studio/build/configure-apk-splits?hl=zh_cn- resConfigs
https://developer.android.google.cn/reference/tools/gradle-api/4.2/com/android/build/api/dsl/BaseFlavor#resourceconfigurations
AndResGuard 是一个资源压缩的工具,和 ProGuard 混淆代码类似,但只作用于资源文件,它会将资源路径变短例如 res/drawable/wechat 变成 r/d/a,以及重命名资源文件例如 wechat.png 变成 a.png。资源重新打包是使用的 7zip 压缩方式能一定程度缩减包体积。
当在项目中集成 AndResGuard 后,我们在需要打包 apk 时,可以在 Android Studio 的 Gradle 窗口使用 task 的方式打包出资源混淆过的 apk。
AndResGuard 的具体配置详细可以查看文档 AndResGuard 资源混淆工具使用说明。
需要注意的是,如果项目有以下的功能实现,就需要考虑 AndResGuard 是否使用:
项目有用到动态换肤方案:动态换肤是通过代码查找资源路径动态替换的,因为资源路径会被 AndResGuard 混淆会导致换肤失败
项目中有大量用到 ConstraintLayout 的 Group:Group 使用 constraint_referenced_ids 属性将多个控件作为一个整体引用,使用了 AndResGuard 要引用控件 id 生效,引用的控件 id 都要加到白名单,否则操作 Group 控制控件会失效
so 是 Android 上的动态链接库,在 Android 开发过程中,有时候 java 代码不能满足需求,比如一些加解密算法或音视频编解码功能,这个时候就必须要通过 C/C++ 实现。之后生成 so 文件提供给 java 层调用。
Android 支持多种 CPU 架构平台,理论上来说,对应架构的 CPU 使用对应的 so,它的执行效率是最高的,也就是每种 CPU 架构平台提供一套 so;so 在包体积优化中是占大头的,因为一般 so 都比较大,每种 CPU 架构分别提供一套 so 包体积将会迅速增加。
幸运的是,现在市面上基本只需要适配 armeabi-v7a 和 arm64-v8a 两种 CPU 架构就能满足绝大部分的手机使用。
因此我们就需要对 lib 目录进行缩减,在 build.gradle 配置 abiFilters 设置 app 支持的 CPU 架构:
android {
defaultConfig {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
}
如果想做到不同的渠道包支持不同的 CPU 架构,可以使用多渠道配置:
android { flavorDimensions "default" productFlavors { arm32 { dimension "default" ndk { abiFilters "armeabi-v7a" } } arm64 { dimension "default" ndk { abiFilters "arm64-v8a" } } } }
如果需要每种 CPU 架构的 so 分别打到不同的 apk,也可以使用分包方案:
android {
...
splits {
abi {
enable true
reset()
include "armeabi-v7a", "arm64-v8a"
// exclude "armeabi"
// universalApk true // 是否打包一个包含所有 so 的 apk
}
}
}
so 动态加载其实就是把本应该打包进 apk 的 so 库通过网络的方式,经过 md5、加解密等安全性校验后下载下来,再通过插件化的方式将下载的 so 正常装载使用的方案。
从代码层面考虑就是我们要将下载的 so 文件路径插入到 DexPathList 的 nativeLibraryDirectories 列表,这样就能让 ClassLoader 查找到我们的 so 文件路径正确加载出来。这里参考 Tinker 的思路写个简单的 demo 了解思路:
private void install(ClassLoader classLoader, File soAbsoluteFilePathDir) throws Throwable { // 获取 DexClassLoader 的 DexPathList final Field pathListField = classLoader.getClass().getDeclaredField("pathList"); pathListField.setAccessible(true); final Object dexPathList = pathListField.get(classLoader); // 获取 DexPathList 的 nativeLibraryDirectories final Field nativeLibraryDirectories = dexPathList.getClass().getDeclaredField("nativeLibraryDirectories"); nativeLibraryDirectories.setAccessible(true); List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList); if (origLibDirs == null) { origLibDirs = new ArrayList<>(2); } // 如果 so 已经存在,移除路径重新装载 final Iterator<File> libDirIt = origLibDirs.iterator(); while (libDirIt.hasNext()) { final File libDir = libDirIt.next(); if (soAbsoluteFilePathDir.equals(libDir)) { libDirIt.remove(); break; } } // 在索引为 0 的位置加入远程 so 文件路径 origLibDirs.add(0, soAbsoluteFilePathDir); // 获取 DexPathList 的 systemNativeLibraryDirectories final Field systemNativeLibraryDirectories = dexPathList.getClass().getDeclaredField("systemNativeLibraryDirectories"); systemNativeLibraryDirectories.setAccessible(true); List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList); if (origSystemLibDirs == null) { origSystemLibDirs = new ArrayList<>(2); } // 将添加了远程 so 的 nativeLibraryDirectories 和 systemNativeLibraryDirectories 的 so 路径装载 final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1); newLibDirs.addAll(origLibDirs); newLibDirs.addAll(origSystemLibDirs); final Method makeElements = dexPathList.getClass().getDeclaredMethod("makePathElements", List.class); makeElements.setAccessible(true); final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs); final Field nativeLibraryPathElements = dexPathList.getClass().getDeclaredField("nativeLibraryPathElements"); nativeLibraryPathElements.setAccessible(true); nativeLibraryPathElements.set(dexPathList, elements); }
当然动态 so 加载还需要考虑很多问题,譬如 so 存在依赖关系、版本控制等等,具体可以参考网上写得比较详细的两篇有关动态 so 加载的文章学习:
Bugly:动态下发 so 库在 Android APK 安装包瘦身方面的应用
该篇文章比较清晰的从 apk 的结构、打包流程分析我们可以优化的方向主要有三点:代码优化、资源优化和 so 优化,并且从三个优化方向总结出一些常规的优化手段。
代码优化手段主要是 ProGuard 代码混淆,还有根据项目场景选出适合的三方库和版本,也可以使用 Lint 扫描出项目中冗余的代码。其中主要关注 ProGuard 代码混淆优化手段,选择合适的混淆范围能一定程度对包体积优化带来不错的收益。
资源优化手段主要是从图片体积、Lint 扫描冗余资源、shrinkResources、资源最少化配置和提到 AndResGuard 资源混淆。其中 shrinkResources、资源最少化配置、AndResGuard 是资源优化可以优先使用的优化手段。在项目有多套资源或国际化时使用资源最少化配置,根据场景只配置满足需求的资源,能很好的降低包体积;AndResGuard 虽然也能一定程度生效,但要注意有动态换肤等场景考虑选用。一般情况考虑时间和成本评估,图片体积优化这种手段的使用优先级不高,如果有极致的包体积优化需求就可以考虑,但要注意每种图片格式的适用场景。
so 优化手段主要关注指定 CPU 架构生成 so 的方案,优先考虑适配市面上覆盖范围广的 CPU 架构,例如 armeabi-v7a 和 arm64-v8a;我们也简单了解了 so 动态加载的方案和思路,但如若没有一套成熟完善经过大量测试的 so 动态加载框架,我的建议是非必须不使用,因为成本太高要考虑安全性等问题,使用不当也容易线上出现稳定性隐患。
三种优化方向一般可以先从 so 优化开始,因为 so 一般体积大优化效果明显,往后是资源优化和代码优化。
需要注意的是,了解了每种优化方向的这些常规化手段,更重要的是明白它们背后的原理,并且结合业务及成本评估是否适合应用在自己的业务场景,避免弄巧成拙。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。