在 Android 性能优化的知识体系当中,包体积优化一直被排在优先级比较低的位置,从而导致很多开发同学对自身应用的大小并不重视。在项目发展的历程中,一般可划分为如下三个阶段:
初创期 => 成长期 => 成熟期
通常来说,当应用处于成长期的中后阶段时,才会考虑去做系统的包体积优化,因此,只有在这个阶段及之后,包体积优化带来的收益才是可观的。
那么,包体积优化能够给我们带来哪些 收益 呢?如何全面对应用的包体积进行 系统分析 及 针对性优化 呢?在这篇文章中,我们将一起进行深入地分析与探索。
众所周知,Android 构建工具链中使用了 AAPT/AAPT2 工具来对资源进行处理,Manifest、Resources、Assets 的资源经过相应的 ManifesMerger、ResourcesMerger、AssetsMerger 资源合并器将多个不同 moudule 的资源合并为了 MergedManifest、MergedResources、MergedAssets。然后,它们被 AAPT 处理后生成了 R.java、Proguard Configuration、Compiled Resources。如下图左上方所示:
其中 Proguard Configuration、Compiled Resources 的 作用 如下所示:
${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt
。${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/resources-${flavorName}-${buildType}-stripped.ap_
。在经过 zip 解压之后,可以发现它 包含了res、AndroidManifest.xml和resources.arsc 这三部分。并且,从上面的 APK 构建流程中可以得知,Compiled Resources 会被 apkbuilder 打包到 APK 包中,它其实就是 APK 的资源包。因此,我们可以 通过 Compiled Resources 文件来修改不同后缀文件资源的压缩方式来达到瘦身效果的。但是需要注意的是,resources.arsc 文件最好不要压缩存储,如果压缩会影响一定的性能,尤其是在冷启动时间方面造成的影响。并且,如果在 Android 6.0 上开启了 android:extractNativeLibs=”false” 的话,So 文件也不能被压缩。APK 的资源主要包括图片、XML,与冗余代码一样,它也可能遗留了很多旧版本当中使用而新版本中不使用的资源,这点在快速开发的 App 中更可能出现。我们可以通过点击右键,选中 Refactor,然后点击 Remove Unused Resource => preview 可以预览找到的无用资源,点击 Do Refactor 可以去除冗余资源。如下图所示:
需要注意的,Android Lint 不会分析 assets 文件夹下的资源,因为 assets 文件可以通过文件名直接访问,不需要通过具体的引用,Lint 无法判断资源是否被用到。
resources.arsc 中可能会存在很多 无用的资源映射,我们可以使用 android-arscblamer:github.com/google/andr… ,它是一个命令行工具,能够 解析 resources.arsc 文件并检查出可以优化的部分,比如一些空的引用。
此外,当我们通过 shrinkResources true 来 开启资源压缩,资源压缩工具只会把无用的资源替换成预定义的版本而不是移除。那么,如何高效地对无用资源自动进行去除呢?
我们可以 在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现自动去除无用资源,具体的实现原理如下:
通过查看 Zip 格式资源包中每个 ZipEntry 的 CRC-32 checksum 来寻找被替换的预定义资源,预定义资源的 CRC-32 定义在 ResourceUsageAnalyze 中,如下所示:
// A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
public static final long TINY_PNG_CRC = 0x88b2a3b0L;
// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
public static final long TINY_9PNG_CRC = 0x1148f987L;
// The XML document as binary-packed with AAPT
public static final long TINY_XML_CRC = 0xd7e65643L;
在大型 App 项目的开发中,一个 App 一般会有多个业务团队进行开发,其中每个业务团队在资源提交时的资源名称可能会有重复的,这将会 引发资源覆盖的问题,因此,每个业务团队都会为自己的 资源文件名添加前缀。这样就导致了这些资源文件虽然 内容相同,但因为 名称的不同而不能被覆盖,最终都会被集成到 APK 包中。这里,我们还是可以 在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现重复资源的去除,具体放入实现原理可细分为如下三个步骤:
具体的实现代码如下所示:
variantData.outputs.each {
def apFile = it.packageAndroidArtifactTask.getResourceFile();
it.packageAndroidArtifactTask.doFirst {
def arscFile = new File(apFile.parentFile, "resources.arsc");
JarUtil.extractZipEntry(apFile, "resources.arsc", arscFile);
def HashMap<String, ArrayList<DuplicatedEntry>> duplicatedResources = findDuplicatedResources(apFile);
removeZipEntry(apFile, "resources.arsc");
if (arscFile.exists()) {
FileInputStream arscStream = null;
ResourceFile resourceFile = null;
try {
arscStream = new FileInputStream(arscFile);
resourceFile = ResourceFile.fromInputStream(arscStream);
List<Chunk> chunks = resourceFile.getChunks();
HashMap<String, String> toBeReplacedResourceMap = new HashMap<String, String>(1024);
// 处理arsc并删除重复资源
Iterator<Map.Entry<String, ArrayList<DuplicatedEntry>>> iterator = duplicatedResources.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, ArrayList<DuplicatedEntry>> duplicatedEntry = iterator.next();
// 保留第一个资源,其他资源删除掉
for (def index = 1; index < duplicatedEntry.value.size(); ++index) {
removeZipEntry(apFile, duplicatedEntry.value.get(index).name);
toBeReplacedResourceMap.put(duplicatedEntry.value.get(index).name, duplicatedEntry.value.get(0).name);
}
}
for (def index = 0; index < chunks.size(); ++index) {
Chunk chunk = chunks.get(index);
if (chunk instanceof ResourceTableChunk) {
ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk;
StringPoolChunk stringPoolChunk = resourceTableChunk.getStringPool();
for (def i = 0; i < stringPoolChunk.stringCount; ++i) {
def key = stringPoolChunk.getString(i);
if (toBeReplacedResourceMap.containsKey(key)) {
stringPoolChunk.setString(i, toBeReplacedResourceMap.get(key));
}
}
}
}
} catch (IOException ignore) {
} catch (FileNotFoundException ignore) {
} finally {
if (arscStream != null) {
IOUtils.closeQuietly(arscStream);
}
arscFile.delete();
arscFile << resourceFile.toByteArray();
addZipEntry(apFile, arscFile);
}
}
}
}
然后,我们再看看图片压缩这一项。
一般来说,1000行代码在APK中才会占用 5kb 的空间,而图片呢,一般都有 100kb 左右,所以说,对图片做压缩,它的收益明显是更大的,而往往处于快速开发的 App 没有相关的开发规范,UI 设计师或开发同学如果忘记了添加图片时进行压缩,添加的就是原图,那么包体积肯定会增大很多。对于图片压缩,我们可以在 tinypng:tingpng.com/ 这个网站进行图片压缩,但是如果 App 的图片过多,一个个压缩也是很麻烦的。因此,我们可以 使用 McImage:github.com/smallSohoSo… 、TinyPngPlugin:github.com/Deemonser/T… 或 TinyPIC_Gradle_Plugin:github.com/meili/TinyP… 来对图片进行自动化批量压缩。但是,需要注意的是,在 Android 的构建流程中,AAPT 会使用内置的压缩算法来优化 res/drawable/ 目录下的 PNG 图片,但这可能会导致本来已经优化过的图片体积变大,因此,可以通过在 build.gradle 中 设置 cruncherEnabled 来禁止 AAPT 来优化 PNG 图片,代码如下所示:
aaptOptions {
cruncherEnabled = false
}
此外,我们还要注意对图片格式的选择,对于我们普遍使用更多的 png 或者是 jpg 格式来说,相同的图片转换为 webp 格式之后会有大幅度的压缩。对于 png 来说,它是一个无损格式,而 jpg 是有损格式。jpg 在处理颜色图片很多时候根据压缩率的不同,它有时候会去掉我们肉眼识别差距比较小的颜色,但是 png 会严格地保留所有的色彩。所以说,在图片尺寸大,或者是色彩鲜艳的时候,png 的体积会明显地大于 jpg。
下面,我们就着重讲解下如何针对性地选择图片格式。
在 Google I/O 2016 中,讲到了如何选择相应的图片格式。首先,如果能用 VectorDrawable 来表示的话,则优先使用 VectorDrawable;否则,看是否支持 WebP,支持则优先用 WebP;如果也不能使用 WebP,则优先使用 PNG,而 PNG 主要用在展示透明或者简单的图片,对于其它场景可以使用 JPG 格式。简单来说可以归结为如下套路:
VD(纯色icon)->WebP(非纯色icon)->Png(更好效果) ->jpg(若无alpha通道)
用 图形化 的形式如下所示:
使用矢量图片之后,它能够有效的减少应用中图片所占用的大小,矢量图形在 Android 中表示为 VectorDrawable 对象。它 仅仅需100字节的文件即可以生成屏幕大小的清晰图像,但是,Android 系统渲染每个 VectorDrawable 对象需要大量的时间,而较大的图像需要更长的时间。 因此,建议 只有在显示纯色小 icon 时才考虑使用矢量图形。(我们可以利用这个 在线工具:inloop.github.io/svg2android… 将矢量图转换成 VectorDrawable)。
最后,如果要在项目中使用 VD,则以下几点需要着重注意:
1)、必须通过 app:arcCompat 属性来使用 svg,如果通过 src,则在低版本手机上会出现不兼容的问题。
2)、可能会不兼容selector,在 Activity 中手动兼容即可,兼容代码如下所示:
static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) }
3)、不兼容第三方库。
4)、性能问题:当Vector比较简单时,效率肯定比Bitmap高,复杂则效率会不如Bitmap。
5)、不便于管理:建议原则为同目录多类型文件,以前缀区别,不同目录相同类型文件,以意义区分。
与 VD 类似,还有一种矢量图标 iconFont,即 字体图标,图标就在字体文件里面,它看着是个图标,其实却是个文字。它的 优势 有如下三个方面:
它的 缺点 也很明显,大致有如下三个方面:
如果你想要使用 iconfont,可以在阿里的 iconfont:www.iconfont.cn/ 上寻找资源。此外,使用 Android-Iconics:github.com/mikepenz/An… 可以在你的应用中便于使用任何的 iconfont 或 .svg 图片作为 drawable。最后,如果我们 仅仅想提取仅需要的美化文字,以压缩 assets 下的字体文件大小,可以使用 FontZip:github.com/forJrking/F… 字体提取工具。
如果不是纯色小 icon 类型的图片,则建议使用 WebP。只要你的 App 的 minSdkVersion 高于 14(Android 4.0+) 即可。WebP 不仅支持透明度,而且压缩率比 JPEG 更高,在相同画质下体积更小。但是,只有 Android 4.2.1+ 才支持显示含透明度的 WebP,此外,它的 兼容性不好,并且不便于预览,需使用浏览器打开。
对于应用之前就存在的图片,我们可以使用 PNG转换WebP:convertio.co/zh/png-webp… 的转换工具来进行转换。但是,一个一个转换开发效率太低,因此我们可以 使用 WebpConvert_Gradle_Plugin:github.com/meili/WebpC… 这个 gradle 插件去批量进行转换,它的实现原理是 在 mergeXXXResource Task 和 processXXXResource Task 之间插入了一个 WebpConvertPlugin task 去将 png、jpg 图片批量替换成了 webp 图片。
此外,在 Gradle 构建 APK 的过程中,我们可以判断当前 App 的 minSdkVersion 以及图片文件的类型来选用是否能使用 WebP,代码如下所示:
boolean isPNGWebpConvertSupported() {
if (!isWebpConvertEnable()) {
return false
}
// Android 4.0+
return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 14
// 4.0
}
boolean isTransparencyPNGWebpConvertSupported() {
if (!isWebpConvertEnable()) {
return false
}
// Lossless, Transparency, Android 4.2.1+
return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 18
// 4.3
}
def convert() {
String resPath = "${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/merged/${variant.dirName}"
def resDir = new File("${resPath}")
resDir.eachDirMatch(~/drawable[a-z0-9-]*/) { dir ->
FileTree tree = project.fileTree(dir: dir)
tree.filter { File file ->
return (isJPGWebpConvertSupported() && (file.name.endsWith(SdkConstants.DOT_JPG) || file.name.endsWith(SdkConstants.DOT_JPEG))) || (isPNGWebpConvertSupported() && file.name.endsWith(SdkConstants.DOT_PNG) && !file.name.endsWith(SdkConstants.DOT_9PNG))
}.each { File file ->
def shouldConvert = true
if (file.name.endsWith(SdkConstants.DOT_PNG)) {
if (!isTransparencyPNGWebpConvertSupported()) {
shouldConvert = !Imaging.getImageInfo(file).isTransparent()
}
}
if (shouldConvert) {
WebpUtils.encode(project, webpFactorQuality, file.absolutePath, webp)
}
}
}
}
最后,这里再补充下在平时项目开发中对 图片放置优化的大概思路,如下所示:
然后,我们来讲解下资源如何进行混淆。
同代码混淆类似,资源混淆将 资源路径混淆成单个资源的路径,这里我们可以使用 AndroidResGuard,它可以使冗余的资源路径变短,例如将 res/drawable/wechat 变为 r/d/a。
AndroidResGuard 项目地址:github.com/shwenzhang/…
下面,我们就使用 AndroidResGuard 来对资源进行混淆。
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17'
apply plugin: 'AndResGuard'
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
// 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
// for your icon
"R.drawable.icon",
// for fabric
"R.string.com.crashlytics.*",
// for google-services
"R.string.google_app_id",
"R.string.gcm_defaultSenderId",
"R.string.default_web_client_id",
"R.string.ga_trackingId",
"R.string.firebase_database_url",
"R.string.google_api_key",
"R.string.google_crash_reporting_api_key"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.17'
//path = "/usr/local/bin/7za"
}
/**
* 可选: 如果不设置则会默认覆盖assemble输出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可选: 指定v1签名时生成jar文件的摘要算法
* 默认值为“SHA-1”
**/
// digestalg = "SHA-256"
}
APK 生成目录如下:
对于 AndResGuard 工具,主要有 两个功能,一个是 资源混淆,一个是 资源的极限压缩。下面,我们就来分别了解下它们的实现原理。
资源混淆工具主要是通过 短路径的优化,以达到 减少 resources.arsc、metadata 签名文件以及 ZIP 文件大小 的效果,其效果分别如下所示:
AndResGuard 使用了 7-Zip 的大字典优化,APK 的 整体压缩率可以提升 3% 左右,并且,它还支持针对 resources.arsc、PNG、JPG 以及 GIF 等文件进行强制压缩(在编译过程中,这些文件默认不会被压缩)。那么,为什么 Android 系统不会去压缩这些文件呢?主要基于以下 两点原因:
此外,抖音 Android 团队还开源了针对于海外市场 App Bundle APK 的 AabResGuard 资源混淆工具:github.com/bytedance/A… ,对它的实现原理有兴趣的同学可以去了解下。然后,我们再看看资源瘦身的其它方案。
我们可以通过内联 R Field 来进一步对代码进行瘦身,此外,它也解决了 R Field 过多导致 MultiDex 65536 的问题。要想实现内联 R Field,我们需要 通过 Javassist 或者 ASM 字节码工具在构建流程中内联 R Field,其代码如下所示:
ctBehaviors.each { CtBehavior ctBehavior ->
if (!ctBehavior.isEmpty()) {
try {
ctBehavior.instrument(new ExprEditor() {
@Override
public void edit(FieldAccess f) {
try {
def fieldClassName = JavassistUtils.getClassNameFromCtClass(f.getCtClass())
if (shouldInlineRField(className, fieldClassName) && f.isReader()) {
def temp = fieldClassName.substring(fieldClassName.indexOf(ANDROID_RESOURCE_R_FLAG) + ANDROID_RESOURCE_R_FLAG.length())
def fieldName = f.fieldName
def key = "${temp}.${fieldName}"
if (resourceSymbols.containsKey(key)) {
Object obj = resourceSymbols.get(key)
try {
if (obj instanceof Integer) {
int value = ((Integer) obj).intValue()
f.replace("\$_=${value};")
} else if (obj instanceof Integer[]) {
def obj2 = ((Integer[]) obj)
StringBuilder stringBuilder = new StringBuilder()
for (int index = 0; index < obj2.length; ++index) {
stringBuilder.append(obj2[index].intValue())
if (index != obj2.length - 1) {
stringBuilder.append(",")
}
}
f.replace("\$_ = new int[]{${stringBuilder.toString()}};")
} else {
throw new GradleException("Unknown ResourceSymbols Type!")
}
} catch (NotFoundException e) {
throw new GradleException(e.message)
} catch (CannotCompileException e) {
throw new GradleException(e.message)
}
} else {
throw new GradleException("******** InlineRFieldTask unprocessed ${className}, ${fieldClassName}, ${f.fieldName}, ${key}")
}
}
} catch (NotFoundException e) {
}
}
})
} catch (CannotCompileException e) {
}
}
}
这里,我们可以 直接使用蘑菇街的 ThinRPlugin:github.com/meili/ThinR… 。它的实现原理为:android 中的 R 文件,除了 styleable 类型外,所有字段都是 int 型变量/常量,且在运行期间都不会改变。所以可以在编译时,记录 R 中所有字段名称及对应值,然后利用 ASM 工具遍历所有 Class,将除 R$styleable.class 以外的所有 R.class 删除掉,并且在引用的地方替换成对应的常量,从而达到缩减包大小和减少 Dex 个数的效果。此外,最近 ByteX 也增加了 shrink_r_class:github.com/bytedance/B… 的 gradle 插件,它不仅可以在编译阶段对 R 文件常量进行内联,而且还可以 针对 App 中无用 Resource 和无用 assets 的资源进行检查。
我们可以把所有的资源文件合并成一个大文件,而 一个大资源文件就相当于换肤方案中的一套皮肤。它的效果 比资源混淆的效果会更好,但是,在此之前,必须要解决 解析资源 与 管理资源 的问题。其相应的解决方案如下所示:
我们需要 根据 App 目前所支持的语言版本去选用合适的语言资源,例如使用了 AppCompat,如果不做任何配置的话,最终 APK 包中会包含 AppCompat 中所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。对此,我们可以 通过 resConfig 来配置使用哪些语言,从而让构建工具移除指定语言之外的所有资源。同理,也可以使用 resConfigs 去配置你应用需要的图片资源文件类,如 "xhdpi"、"xxhdpi" 等等,代码如下所示:
android
...
defaultConfig {
...
resConfigs "zh", "zh-rCN"
resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}
...
}
此外,我们还以 利用 Density Splits 来选择应用应兼容的屏幕尺寸大小,代码如下所示:
android {
...
splits {
density {
enable true
exclude "ldpi", "tvdpi", "xxxhdpi"
compatibleScreens 'small', 'normal', 'large', 'xlarge'
}
}
...
}
比如说,我们统一只把图片放到 xhdpi 这个目录下,那么 在不同的分辨率下它会做自动的适配,即 等比例地拉伸或者是缩小。
我们可以 将一些图片资源放在服务器,然后 结合图片预加载 的技术手段,这些 既可以满足产品的需要,同时可以减小包大小。
如设定统一的 字体、尺寸、颜色和按钮按压效果、分割线 shape、selector 背景 等等。
对于主要由 C/C++ 实现的 Native Library 而言,常规的优化方式就是 去除 Debug 信息,使用 C++_shared 等等。下面,对于 So 瘦身,我们看看还有哪些方案。
So 是 Android 上的动态链接库,在我们 Android 应用开发过程中,有时候 Java 代码不能满足需求,比如一些 加解密算法或者音视频编解码功能,这个时候就必须要通过 C 或者是 C++ 来实现,之后生成 So 文件提供给 Java 层来调用,在生成 So 文件的时候就需要考虑生成市面上不同手机 CPU 架构的文件。目前,Android 一共 支持7种不同类型的 CPU 架构,比如常见的 armeabi、armeabi-v7a、X86 等等。理论上来说,对应架构的 CPU 它的执行效率是最高的,但是这样会导致 在 lib 目录下会多存放了各个平台架构的 So 文件,所以 App 的体积自然也就更大了。
因此,我们就需要对 lib 目录进行缩减,我们 在 build.gradle 中配置这个 abiFiliters 去设置 App 支持的 So 架构,其配置代码如下所示:
defaultConfig {
ndk {
abiFilters "armeabi"
}
}
一般情况下,应用都不需要用到 neon 指令集,我们只需留下 armeabi 目录就可以了。因为 armeabi 目录下的 So 可以兼容别的平台上的 So,相当于是一个万金油,都可以使用。但是,这样 别的平台使用时性能上就会有所损耗,失去了对特定平台的优化。
上面我们说到了想要完美支持所有类型的设备代价太大,那么,我们能不能采取一个 折中的方案,就是 对于性能敏感的模块,它使用到的 So,我们都放在 armeabi 目录当中随着 Apk 发出去,然后我们在代码中来判断一下当前设备所属的 CPU 类型,根据不同设备 CPU 类型来加载对应架构的 So 文件。这里我们举一个小栗子,比如说我们 armeabi 目录下也加上了 armeabi-v7 对应的 So,然后我们就可以在代码当中做判断,如果你是 armeabi-v7 架构的手机,那我们就直接加载这个 So,以此达到最佳的性能,这样包体积其实也没有增加多少,同时也实现了高性能的目的,比如 微信和腾讯视频 App 里面就使用了这种方式,如下图所示:
看到上图中的 libimagepipeline_x86.so,下面我们就以这个 so 为例来写写加载它的伪代码,如下所示:
String abi = "";
// 获取当前手机的CPU架构类型
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
abi = Buildl.CPU_ABI;
} else {
abi = Build.SUPPORTED_ABIS[0];
}
if (TextUtils.equals(abi, "x86")) {
// 加载特定平台的So
} else {
// 正常加载
}
接下来,我们再了解下 So 优化当中别的优化方式。
Native Library 同 Dex 一样,也可以使用 XZ Utils 进行压缩,对于 Native Library 的压缩,我们 只需要去加载启动过程相关的 Library,而其它的都可以在应用首次启动时进行解压,并且,压缩效果与 Dex 压缩的效果是相似的。
此外,关于 Nativie Library 压缩之后的解压,我们也可以使用 Facebook 的 so 加载库 SoLoader:github.com/facebook/So… ,它 能够解压应用的 Native Library 并能递归地加载在 Android 平台上不支持的依赖项。由于这套方案对启动时间的影响比较大,所以先把它压箱底下吧。
在 Android 4.3(API 17) 之前,单个进程加载的 SO 数量是有限制的,在 Google 的 linker.cpp 源码中有很明显的定义,如下图所示:
为了解决这个问题,FaceBook 写了一个 合并Native Library的demo:github.com/fbsamples/a… ,我们可以 按照自身 App 的 so 情况来配置需要合并哪些对象。由于合并共享对象(即 .so 文件)在原先的构建流程中是无法实现的,因此 FaceBook 更改了链接库的方式,并把它集成到了构建系统 Buck:github.com/facebook/bu… 中。该功能允许每个应用程序指定应合并的 .so 库,从而避免意外引入不必要的依赖关系。然后,Buck 负责为每个合并的 .so 库收集所有对象(文件),并将它们与适当的依赖项链接在一起。
我们可以去 分析代码中的 JNI 方法以及不同 Library 库的方法调用,然后找出无用的 symbol 并删除,这样 Linker 在编译的时候也会把 symbol 对应的无用代码给删除。在 Buck 有 NativeRelinker:github.com/facebook/bu… 这个类,它就实现了这个功能,其 类似于 Native Library 的 ProGuard Shrinking 功能。
至此,可以看到,FaceBook 出品的 Buck 同 ReDex 一样,里面的功能都十分强大,Buck 除了实现 Library Merge 和 Relinker 功能之外,还实现了三大功能,如下所示:
如果有相应需求或对 Buck 感兴趣的同学可以去看看它们的实现源码。
我们可以 将部分 So 文件使用动态下发的形式进行加载。也就是在业务代码操作之前,我们可以先从服务器下载下来 So,接下来再使用,这样包体积肯定会减少不小。但是,如果要把这项技术 稳定落地到实际生产项目中需要解决一些问题,具体的 so 动态化关键技术点和需要避免的坑可以参见 动态下发 so 库在 Android APK 安装包瘦身方面的应用 :mp.weixin.qq.com/s/X58fK02im… ,这里就不多赘述了。
我们可以使用插件化的手段 对代码结构进行调整,如果我们 App 当中的每一个功能都是一个插件,并且都是可以从服务器下发下来的,那 App 的包体积肯定会小很多。插件化相关的知识非常多而且不属于我们的重点,并且,插件化严格来说属于 基础架构研发 这块的知识,掌握它是成为 Android 架构师的必经之路。
我们需要 回顾过去的业务,合理地去 评估并删除无用或者低价值的业务。
如果所有的功能都不能移除,那就可能需要去转变开发模式,比如可以更多地 采用 H5、小程序 这样开发模式。
对于应用包体积的监控,也应该和内存监控一样,去作为正式版本的发布流程中的一环,并且应该 尽量地去实现自动化与平台化。(这里建议 任何大于 100kb 的功能都需要审批,特别是需要引入第三方库时,更应该慎重)
包体积的监控,主要可以从如下 三个纬度 来进行:
包体积的 大小监控 和 依赖监控 都很容易实现,而要实现 规则监控 却得花不少功夫,幸运的是 Matrix 中的 ApkChecker:github.com/Tencent/mat… 就实现了包体积的规则监控,其 使用文档与实现原理 微信团队已经写得很清楚了,这里就不再一一赘述,有兴趣的同学可以去研究下。
瘦身优化是性能优化当中不那么重要的一个分支,不过对于处于稳定运营期的产品会比较有帮助。下面我们就来看看对于瘦身优化有哪些常见问题。
我们在回答的时候要注意一些 可操作的干货,同时注意结合你的 项目周期。主要可以从以下 三点 来回答:
在项目初期,我们一直在不断地加功能,加入了很多的代码、资源,同时呢,也没有相应的规范,所以说,UI 同学给我们很多 UI 图的时候,都是没有经过压缩的图片,长期累积就会导致我们的包体积越来越大。到了项目稳定期的时候,我们对各种运营数据进行考核,发现 APK 的包大小影响了用户下载的意愿,于是我们就着手做包体积的优化,我们采用的是 Android Studio 自带的 Analyze APK 来做的包体积分析,主要就是做了代码、资源、So 等三个方面的重点优化。
首先,针对于代码瘦身,第一点,我们首先 使用 Proguard 工具进行了混淆,它将程序代码转换为功能相同,但是不容易理解的形式。比如说将一个很长的类转换为字母 a,同时,这样做还有一个好处,就是让代码更加安全了。第二点呢,我们将项目中使用到的一些 第三方库进行了统一,比如说图片库、网络库、数据库等,不允许项目中出现功能相同,但是却实现不一样的库。同时也做了 规范,之后引入的三方库,需要去考量它的大小、方法数等,而且呢,如果只是需要一个很大库的一个小功能,那我们就修改源码,只引入部分代码即可。第三点,我们将项目中的 无用代码进行了删减,我们使用了 AOP 的方式统计到了哪些 Activity 以及 fragment 在真实的场景下没有用户使用,这样你就可以删除掉了。对于那些不是 Activity 或者是 Fragment 的类,我们切了很多类的构造函数,这样你就可以统计出来这些类在线上有没有真正被调用到。但是,对于代码的瘦身效果,实际上不是很明显。
接下来,我们做了资源的瘦身。首先,我们 移除了项目当中冗余的资源文件,这一点在项目当中一定会遇到。然后,我们做了 资源图片的压缩,UI 同学给我们资源图片的时候,需要确认已经是压缩过的图片,同时,我们还会做一个 兜底策略,在打包的时候,如果图片没有被压缩过,那我们就会再来压缩一遍,这个效果就非常的明显。对于资源,我们还做了 资源的混淆,也就是将冗余的资源名称换成简短的名字,资源压缩的效果要比代码瘦身的效果要好的多。
最后,我们做了 So 的瘦身。首先,我们只保留了 armeabi 这个目录,它可以 兼容别的 CPU 架构,这点的优化效果非常的明显。移除了对别的架构适配 So 之后,我们还做了另外一个处理,对于项目当中使用到的视频模块的 So,它对性能要求非常高,所以我们采用了另外一种方式,我们将所有这个模块下的 So 都放到了 armeabi 这个目录下,然后在代码中做判断,如果是别的 CPU 架构,那我们就加载对应 CPU 架构的 So 文件即可。这样即减少了包体积,同时又达到了性能最佳。最后,通过实践可以看出 So瘦身的效果一般是最好的。
主要可以从以下 两个方面 来进行回答:
在大型项目中,最好的方式就是 结合 CI,每个开发同学 在往主干合入代码的时候需要经过一次预编译,这个预编译出来的包对比主干打出来的包大小,如果超过阈值则不允许合入,需要提交代码的同学自己去优化去提交的代码。此外,针对项目的 架构,我们可以做 插件化的改造,将每一个功能模块都改造成插件,以插件的形式来支持动态下发,这样应用的包体积就可以从根本上变小了。
在本篇文章中,我们主要从以下 七个方面 讲解了 Android 包体积优化相关的知识:
至此,我们可以了解到,如果要想对包体积做更深入的优化,就必须对 APK 组成,Dex、So 动态库以及 Resource 文件格式,还有 APK 的编译流程 有深入地了解,这样我们才能有 足够的内功素养 去实现包体积的深度优化。
1、Top团队大牛带你玩转Android性能分析与优化 第10章 App瘦身优化
2、极客时间之Android开发高手课 包体积优化
3、《Android性能优化最佳实践》第七章 安装包大小优化
4、android-arscblamer
5、Android SVG to VectorDrawable
6、App瘦身最佳实践
7、使用Simian工具扫描重复代码
8、FontZip
9、Android-Iconics
10、iconfont
11、IconFont在Android中的使用
12、TinyPngPlugin
13、动态下发 so 库在 Android APK 安装包瘦身方面的应用
14、Apktool Install Instructions
15、nimbledroid
16、android-classyshark
17、Android混淆从入门到精通
18、APK Expansion Files
19、写入放大
20、D8 dexer and R8 shrinker
21、Android新Dex编译器D8与新混淆工具R8
22、Comparison of ProGuard vs. R8: October 2019 edition
23、支付宝 App 构建优化解析:Android 包大小极致压缩
24、Redex
25、Redex 初探与 Interdex:Andorid 冷启动优化
26、Dalvik 可执行文件格式
27、InterDex.cpp 贪心算法部分
28、CrossDexRefMinimizer.cpp 跨dex调用优化
29、我是如何通过 nimbledroid 做android app性能优化的
30、XZ Embedded
31、oatmeal
32、SoLoader
33、buck
34、android-native-library-merging-demo
35、Redex优化demo
36、android-chunk-utils
37、ResourceUsageAnalyzer.java
38、Android安装包相关知识汇总
39、安装包立减1M--微信Android资源混淆打包工具
40、AndResGuard
41、Android APK 签名原理
42、西瓜视频apk瘦身之 Java access 方法删除
转载:https://juejin.cn/post/6872920643797680136