前言
由于业务发展快,前期视觉提供的照片都是PNG无损格式的,项目里也存在很多无用资源,在初期,团队没有注意这些,现在处于稳定期,就需要考虑着手包体积优化,为用户节省点流量,优化体验。而这个任务分配给了笔者,(后续还有内存优化),接下来将结合极客时间的Android开发高手课 + 项目实际经验来介绍一下体积包的优化。本文将分常规瘦身方案和进阶瘦身方案来记录,常规瘦身方案基本应该是大家都会做的,进阶瘦身有很多方案,可以看看支付宝mPass公众号和抖音的一些相关技术文章。
参考资料:
极客时间-Andorid开发高手课
深入探索Android包体积优化(匠心之作) jsonchao 写的很细,在学习之前可以看一下他写的插桩编译原理基础部分
慕课网-Top团队大牛带你玩转Android性能分析与优化
思维导图目录
一、瘦身优势和APK分析方案
1、 瘦身优势
转化率
在项目初期瘦身基本上都不会做,初期功能模块显得更重要,并且现在网速也很快,下载个百来兆的应用也不慢,更何况5G发展起来了,网速蹭蹭蹭往上飙。但是没有对比就没有伤害,等同条件下的APP,体积包小的APP,用户应该是更倾向于下载它的,那么该APP是不是下载转换率也就会高一点,所以包体积大小也成为了衡量APP业务的重要指标之一。
应用市场
Google Play市场对APK的包是有要求的,超过100MB的应用只可以用APK扩展方式上传,如果要避免使用扩展文件,那么就应该使用Android App Bundles,Android Studio里由个选项Build Bundle。
安装速度
我们的APP体积32MB,在OPPO应用市场检测报告中显示安装时间得60分,16.52s是最佳安装时间,测出来是47.11s,抖音是17.81s是最佳安装时间,实际90.26s,也得60分。也不知道Testin是怎么测出来的,个人感觉安装很快,都要不了16s。安装过程中涉及文件拷贝、Library解压、签名校验(文末有学习链接),安装包体积解压后肯定比原来APK所占内存大,解压后的体积不需要过多考虑,毕竟现在手机内存都很大,当然如果实在不行,可以安装在SD卡上,但是安装在SD卡上有两个问题,安装在SD卡上所需要的内存会比安装在手机上的大很多,还有一个就是例如开机广播不会触发这类问题。
2、APK组成
dex文件 class转成dex文件
lib 存放native需要的so文件
assets 存放打包静态资源文件,目录层级不限制,不会在R文件中生成ID,通过AssetManager获取
res 存放资源文件,会在R文件中生成对应的ID,运行时通过ID找到对应的资源路径
resources.arsc ID和资源文件的映射关系
AndroidManifest.xml 清单文件
META-INF 存放签名相关信息,解压安装时会用到(文末有学习链接),用于验证APK的完成性和保证APK的安全
3、APK分析
a、ApkTool
如果比较懒的话,可以用这个软件直接打开apk文件,看smali文件 Smali2Java
反编译后可以看类文件,如果混淆的话,可能就不好看了
b、AS 自带的Analyze APK
这个相信大家都知道在哪,我平时都用它来分析APK,和别的APK做对比,它还能很直观的查看资源对应ID
c、nimbledroid性能分析指标
使用方式简单,直接打开 nimbledroid官网,上传APK就可以了。But 网站注册必须使用公司邮箱注册,个人邮箱注册Error Hint
它的相关性能指标可以从两方面分析
静态分析 可以看APK安装包大文件排行,Dex方法数及比例,包括第三方SDK
动态分析 可以看冷启动时间,Block UI具体方法,Hot Method等
d、android-classyshark 二进制检查工具
使用方式很简单,下载jar包,然后拖动APK到打开的窗口内即可,可以看到类、成员变量等信息
二、常规瘦身方案
为什么说是常规瘦身方案呢,其实也就是很容易在代码里实践的方法,类似于网上的APK 九道瘦身程序,下面我们按分类来看看
1、代码瘦身
a、ProGuard
混淆的重要性就不过多阐述了,其实每个上线的APP都会做混淆,可能大部分初级开发只了解混淆能将字段、方法、类的名称改成简短的名字,增加了反编译的难度,起到一定的安全作用当然这个也算瘦身。还有一个作用,它会检测移除未使用的类、方法、字段、冗余代码,对字节码进行深度优化。想要具体了解可以学习底部链接知识,包括混淆的规则。
优点
在将枚举类型简化为原始整数方面会更加强大
在删除所有跟踪(包括组成日志消息的字符串操作)方面更有效
应用的模式匹配算法可以识别和替换短指令序列,从而提高代码效率并为更多优化打开了机会。在优化遍历的顺序中,尤其是数学运算和字符串运算可从中受益
具有独特的能力来优化使用 GSON 库将对象序列化或反序列化为 JSON 的代码,访问更加高效
buildTypes{
release{
//开启混淆
minifyEnabled true
//sdk默认的混淆配置,默认打开了优化开关 /-dontoptimize 关闭优化 /-dontobfuscate 关闭混淆
proguardFiles getDefaultProguardFile('..'),'我们自定义的混淆配置.pro'
}
}b、Dex分包
与传统jar文件比较,dex文件的大小能够缩小50%左右,而且dex是分区结构,内部各个区块通过offset来进行索引,class是流式接构,一个Dex中包含了很多class。个人理解,为什么android不用class,而重新设计一个dex,从优化角度理解,完全是因为class只能针对某个类优化,而dex包含了很多类,文件结构紧凑,类之间还可以优化,优化空间更大。
当我们APK方法数超过65536时,(原因是首次加载dex时,DexOpt是把所有方法id存起来,放到链表里,链表长度定义的是short类型,所以超过2的16次方-1就报错了)会报错,必须采用 mutildex 进行分包
defaultConfig{
multiDexEnabled true
}
dependencies{
//仅对当前包可见
implementation 'com.android.support.multidex:1.0.0'
}但是,分包后会有什么问题?想过没,说实话,作为渣渣的我确实没想过。读者可以想一下,分包后有什么问题,答案在本文下面的进阶瘦身方案Dex模块会讲解。
c、D8 R8
官网介绍 只要gradle版本大于3.4.0,R8都是默认开启的,但是R8是3.3.0引入的。下面两张图可以看出使用R8的区别
下面了解一下D8 R8的优化效果以及开启方式
D8
Dex的编译实践更多
.dex文件更小并且拥有更好的运行时性能
包含java 8语言支持的处理
使用方式(AS3.1默认)
gradle.properties
android.enableD8=trueR8
和混淆类似,使用随机无意义的名称
inline内联,避免对象分配
在删除未使用的类、字段、防范上更好
与proguard相比,R8能使apk减小约10%,体积更小
使用方式
android.enableR8=true
android.enableR8.libraries=trued、移除无用代码
随着业务增加,许多之前使用的类可能并没有用到,然后又想着以后万一又能用到呢?反正就一个文件,删不删除无所谓,占内存又不大,长此以往,apk就越来越大了。
我平时都用Android Studio自带的 Lint 检测工具无效代码
慕课网的性能优化视频里讲到过一种方案,通过ASpectJ(简单入门)对某个包下的类就行构造函数切面
@After("execution(org.jay.launchstarter.Task.new(..)")
public void newObject(JoinPoint point) {
LogHelper.i(" new " + point.getTarget().getClass().getSimpleName());
}感觉这个确实能打印出来使用的类,可以搜集起来,那还得写一个脚本找出没使用得类,删除,有点风险。
我觉得还是jsonchao提出的方案简单有效,通过 coverage插件 基于线上用户上报无用代码分析
2、资源瘦身
首先我们了解一下APK解压后,资源相关的目录
a、资源混淆
混淆优点
resources.arsc中需要记录资源文件名与路径,混淆后短路径会减少整个文件大小,例如res/drawable/icon -> res/s/a
metadata签名文件,使用短路径也会减小文件大小。这块想详细了解可以看 深入探索编译插桩技术之编译基础 和 Android APK 签名原理
ZIP文件索引。zip文件格式里面也需要记录每个文件的Entry路径、压缩算法、CRC校验、文件大小等信息,使用短路径也可以减小文件路径的字符串大小。
采用方案 微信的AndResGuard
b、无用资源删除
android{
....
buildTypes{
release{
shrinkResources true
minifyEnable true
}
}
}一般我们都会在build.gradle里面配置上述代码,并通过 Android Studio 自带的Lint检测工具 进行 删除无用资源,这里注意,笔者操作过程中,发现用Lint检测出来的无用资源,很多其实都引用了的。
注意,这里配置 shrinkResources true 是真的将无用资源去除了吗?答案:不是的
理由有两点
没有处理resources.arsc文件,这样导致大量无用的String 、ID、Attr、Dimen等资源并没有被删除
没有真正删除资源文件,对于Drawable、Layout这些无用资源,shrinkResources 也没有真正把它们删除,仅仅是替换为了一个空文件,为什么不删除呢?因为resources.arsc里面还有这些文件的路径
综上,所以说系统目前的这种做法并没有真正减少文件数量,resources.arsc、签名信息、以及ZIP文件信息依然没有改善。
为什么系统不做删除呢?因为resources.arsc和R文件的资源ID默认是连续的,删除无用资源文件,资源ID会改变,这个时候代码中已经替换过的ID就会出现资源找不到的情况,所以只是替换成了空文件。
思路方案:删除文件,同时keep住R文件和resources.arsc中的ID
还有一种,利用 android-arscblamer 分析,具体可以查看 在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现自动去除无用资源
c、图片压缩
这个比较好实现,笔者将项目中大于10k的图片都压缩了一下,体积减小了3%,笔记使用了cwebp 工具,转换成webp
# coding=utf-8
import os
from pathlib import Path
'''
file_directory : 目录
modules : 变长参数 指定模块才解析
'''
def findAndConvert(file_directory, *modules):
if len(modules) == 0:
print("modules must Designated")
return
if file_directory.is_file():
print("this is a file , need a directory")
return
if os.path.exists(file_directory):
print("has this directory")
for homes, dirs, files in os.walk(file_directory):
# print(homes)
# print(dirs)
# print(files)
for module in list(modules):
check_path = os.path.join(file_directory.absolute(), module)
if check_path in homes:
# print("============== convert this path " + check_path + " ================== ")
# 寻找 src/main/res/darwable开头的文件
# 或者直接找png jpg图片
if "src\\main\\res" in homes and "drawable" in homes:
# print(homes)
for file in files:
if file.endswith(".png") or file.endswith(".jpg"):
file_path = Path(os.path.join(homes, file))
if file_path.is_file() and os.path.getsize(file_path.absolute()) > 10 * 1024:
# print(file)
# print(file[:-4])
current_file = os.path.join(homes, file)
target_file = os.path.join(homes, file[:-4] + ".webp")
cmd = "cwebp " + current_file + " -o " + target_file
result = os.system(cmd)
if result == 0:
# os.remove(file_path.absolute())
os.system("svn add " + target_file)
os.system("svn del " + current_file)
# else:
# print("please check this path " + check_path)
else:
print("this directory has not exists")
if __name__ == '__main__':
path = Path("D:/WorkSVN/项目Prject/")
# 指定包名 变长参数必填
findAndConvert(path, "模块名","模块名2")
d、资源文件规范化
尽量每张图片只保留一份,一般准备一份xhdpi的就可以
资源图片尽量让后端返回,如果后端能根据前端上传的图片宽高进行裁剪再返回,这更加好,还能优化Bitmap相关内存
在resConfig中配置保留哪些资源,减少不必要的资源
android { ... defaultConfig { ... resConfigs "zh-rCN" resConfigs "xhdpi" } ... }
3、SO瘦身
这个不多说了,直接上代码
defaultConfig {
ndk {
abiFilters "armeabi"
}
}现在市面上大多都是armeabi架构,例如,微信APK分析一下,也能发现只有armeabi,虽然微信对加载armeabi做了CPU 架构的适配,什么时候加载armeabi的哪个so文件,都有适配。
详细可以了解 为何大厂APP如微信、支付宝、淘宝、手Q等只适配了armeabi-v7a/armeabi?
这里给出一个abi工作规则图
从图中可以得出另一个规则
只适配
armeabi的APP可以跑在armeabi,x86,x86_64,armeabi-v7a,arm64-v8上只适配
armeabi-v7a可以运行在armeabi-v7a和arm64-v8a只适配
arm64-v8a可以运行在arm64-v8a上
三、进阶瘦身方案
1、代码瘦身
a、三方库处理
其实这个都不能算做进阶瘦身方案,但在实际操作中,我确实没有想到对三方库进行处理,导致没必要的依赖加载进来的,毕竟将所有模块代码看一遍,很多模块不是自己写的,优化起来丢三落四的,所以放到进阶方案里了。
在选择第三方库时,需要评审一下,以性能为第一指标,包体积为第二指标,尽量选择性能好,体积小的第三方SDK,并且在使用到第三方SDK某个功能时,尽量继承某个功能依赖,而不是全部,例如只需要Fresco的某个webp功能,那就只集成webp依赖就好了。
我按照方案,将项目里所有依赖都过了一遍,发现图片相关Glide和Fresco都使用了,GG,并不能选择一个用,改动太大,涉及测试也很多,只能将这种方案在组内会议规范一下。(PS:Fresco加载长图会显示空白的)
b、去除debug信息与行号信息
dex文件结构图(摘自极客时间Andoid开发高手课包体积优化)
一般,我们都会在混淆配置中以下面的方式保留行号信息
-keepattributes SourceFile, LineNumberTable如果不保留行号信息,dex大约可以减少5%的体积,但是Crash上报后,会拿不到行号怎么办?问题该怎么定位?同学,莫慌,支付宝Android 包大小极致压缩方案 帮你解决。
当然,支付宝这个方案是参考的Facebook的 ReDex ,这个只支持linux、和mac。因为我之前了解过ReDex,所以在接到压缩体积包时,也调研过,可是编译阶段硬是没有编译过去,后面试了两次,还是有点问题,就没有使用该方案。
基于反馈(启动加载顺序测试)的Class字节码布局
混淆和压缩
内联函数,将一些函数直接展开到调用它的函数中,减少函数调用切换的时间消耗
删除无用的interface,删除只有一个实现的接口,用实现类直接代替,加快了函数调用时间,减少了内存空间及函数引用
删除无用代码,类似早期内存回收的标记-清除策略,删除无用代码
删除metadata,一些数据在运行中并不需要,用dex中已有的字符串代替文件引用以及删除无用的注解来减小Dex大小
c、Dex分包优化
上面讲到了ReDex的一个优点,基于反馈的Class字节码布局,这是啥意思呢?
类字节码在单个 Dex 中的布局是根据编译顺序而不是运行时行为决定,即便 Multidex 会将 App 定义的组件以及部分启动需要的类放在 Main Dex 中,但依然不是根据运行时顺序布局,这会导致程序启动时需要查找随机分布在 Dex 中的类。
ReDex 会将 APK 在 Lab 中试运行,并跟踪启动时哪些类需要加载,然后将这些类字节码放到 Dex 前部,减少启动时从闪存中寻找类的时间,从而提高 App 启动速度。
好的,大致了解了,那你可能会问,这个跟Dex分包优化有半毛钱关系?
在我们使用mutildex分包后,此时的每一个Dex可能会调用到其它的Dex中的方法,这种跨Dex调用的方式会造成许多冗余信息,具体有两点
method id爆表。每个dex 的method id 需要小于65536 ,因为method id的大量冗余导致每个Dex真正可以放的Class 变少,会造成最终编译的Dex数量增多
信息冗余 。因为我们需要记录跨Dex调用的方法的详细信息,所以在调用方还需要记录另一个Dex中相关类、方法的定义,造成string_ids、type_ids、proto_ids这几部分信息的冗余
为了保证Dex有效率在80%以上,通过以下公式来衡量优化效果
Dex 信息有效率 = define methods数量 / reference methods 数量为了实现上述所说的信息冗余去除,ReDex配置一下就可以
{
"redex" : {
"passes" : [
"InterDexPass",
"RegAllocPass"
]
},
"InterDexPass" : {
"minimize_cross_dex_refs": true,
"minimize_cross_dex_refs_method_ref_weight": 100,
"minimize_cross_dex_refs_field_ref_weight": 90,
"minimize_cross_dex_refs_type_ref_weight": 100,
"minimize_cross_dex_refs_string_ref_weight": 90
},
"RegAllocPass" : {
"live_range_splitting": false
},
"string_sort_mode" : "class_order",
"bytecode_sort_mode" : "class_order"
}d、Dex压缩
你可以分析一下Facebook APP的dex文件,他把真正的代码放到了assets下面,导致只有一个700多kb的classes.dex,他通过XZ Utils将所有dex压缩成了一个。
XZ 压缩算法 和 7-Zip 一样,内部使用的都是 LZMA 算法。对于 Dex 格式来说,XZ 的压缩率可以比 Zip 高 30% 左右。但是这套方案存在一些问题
首次启动解压。应用首次启动的时候,需要将 secondary.dex.jar.xzs 解压缩,根据上图的配置信息,应该一共有 11 个 Dex。Facebook 使用多线程解压的方式,这个耗时在高端机是几百毫秒左右,在低端机可能需要 3~5 秒。这里为什么不采用 Zstandard 或者 Brotli 呢?主要是压缩率与解压速度的权衡。
ODEX 文件生成。当 Dex 非常多的时候会增加应用的安装时间。对于 Facebook 的这个做法,首次生成 ODEX 的时间可能就会达到分钟级别。Facebook 为了解决这个问题,使用了 ReDex 另外一个超级硬核的方法,那就是oatmeal。
上面的讲解摘自极客时间Android开发高手课,这里仅当了解了一下,因为oatmeal还需要分版本适配,臣妾做不到,难度对于目前的我来说很高。
2、资源瘦身
真正去除无用资源
利用 android-arscblamer 分析,具体可以查看 在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现自动去除无用资源
重复资源优化
这个在项目开始时就需要制定好规范,但是也避免不了,多个地方用到的图片或资源存放在不同的模块下,毕竟这是团队开发。这个问题在任务开展过程中,确实不好搞,因为存在太多相同字符串,名称不同,文案一样,然后图片又有相同的,命名又不一样,难搞。
具体实施方案查看 重复资源优化
3、SO瘦身
a、对Library的压缩与合并、裁剪
跟Dex一样,可以通过XZ Utils压缩
在默认的lib目录中,我们只需要加载少数启动过程相关的 Library ,其它Library放在首次启动时解压。对于压缩率来说,可以比ZIP压缩高30%,效果很好。缺点和Dex类似
实施方案,配合采用Facebook的一个开源库 SoLoader
合并
在Android 4.3 之前,进程加载的Library数量是由限制的,可以点击查看
然后Facebook又出来方案了 Android native mergin 和 Demo
裁剪
又是Facebook的方案 relinker
原理:分析代码中的JNI方法以及不同Library方法调用,找到没有无用的导出symbol,然后删除掉,这样linker在编译的时候也会把对应的无用代码同时删除掉。
四、长效治理APK包体积
包体积监控 ApkChecker ,刚好项目中集成了Matrix
在需要引入依赖库或者jar包等第三方SDK时,需要进行分析,拉个小会讨论下最好
每位同学上传代码前,进行包体积大小对比,超过阀值必须优化,同时review一下是否不小心引入了一些不必要的资源
笔记一完结