本文作者:小木箱,原文发布于:小木箱成长营。
如果学完小木箱包体积优化的工具论、方法论和实战论,那么任何人做包体优化都可以拿到结果。
2.1 优化目标
2.1.1 包体分析
包体分析主要借助的是腾讯 AppChecker 完成的,AppChecker 分析包文件主要还是借助了 andoid build-tool 下面的 aapt 工具。关于 AppChecker 使用指南可以参考下面的链接:
2.1.2 版本对比
APP 安装包大小变化的趋势
2.1.3 竞品对比
2.1.4 攻坚目标
2.2 优化排期
2.3 优化记录
排期 A
排期 B
排期 C
2.4 阶段成果
2.5 衡量指标
打包后体积大小 安装速度 埋点
2.6 CI/CD 监控与预警
机器人告警能力
机器人告警能力模块,QA 机器人引入了开发环境,合码大小低于预估目标阈值发送警告通知如下:
APK 文件主图
APK 文件主图模块用 AppChecker 工具分析的文件大小占比饼状图汇总(如下,通过 Echarts 或其他组件渲染)。
APK 文件大小排行榜
重复资源分析
无用资源分析
依赖树结构图
依赖树版本管控可以通过版本进行映射对比分析,注意要展示仓库之间的依赖层级关系。
重复代码分析
无用代码分析
不合规图片转换压缩
方法数汇总报告图
构建产物版本差异图
APK 版本趋势折线图
绿盟黑盒质检报告
差异版本优化建议
2.7 采坑记录
2.7.1 问题与挑战
2.7.2 解决方式
2.7.3 思考
绿盟和隐私合规检查如何与 CI 结合渲染?产品设计怎样做更人性化? 推广业务接受度如何? 能否打成一个 jar 文件,然后通过命令方式将静态页面渲染生成一份可视化报告给社区使用? 代码混淆工作能否在打包过程完成? 对于通过 Google Play 分发的应用,不得采用 Google Play 更新机制以外的其他任何方式修改、替换或更新应用本身。同样地,应用不得从 Google Play 以外的其他来源下载可执行代码(例如 dex、JAR 和 .So 文件)。对此,资源动态化只能满足国内的需求。注意提供开关?
3.1 CI/CD 集成监控包体健康度
3.2 So 库压缩与解压机制
安装 APK 包的时候,PMS 根据当前设备的 abi 信息,从 APK 包里拷贝相应的 so 文件。到 data/data/[包名]/lib。 启动 APP 的时候,PMS 会把系统的 So 文件夹,以及安装包的 So 文件夹位置给 BaseDexClassLoader 中的属性 DexPathList 下面属性的 nativeLibraryDirectories 和 systemNativeLibraryDirectories 两个 File 集合 ,Android Framework 创建应用的 ClassLoader 实例,并将当前应用相关的所有 so 文件所在目录注入到当前 ClassLoader 相关字段。 调用 System.loadLibrary("xxx"), framework 从当前上下文 ClassLoader 实例(或者用户指定)的目录数组里查找并加载名为 libxxx.so 的文件。 调用 so 相关 JNI 方法。
实现思路
传送门:
https://github.com/Android-Mainli/Android-So-Handler
soCompressConfig {
// tarFileNameArray定义了了需要打包压缩的本地库⽂文件列列表
tarFileNameArray = ['test1.so', 'test2.so', 'test3.so']
// compressFileNameArray 需要压缩本地库⽂文件⽂文件名
compressFileNameArray = ['test4.so', 'test5.so']
// optinal属性 是否打印整个过程的⽇日志 , 默认false
printLog = true
// optional属性 本地库filter,默认armeabi-v7a
abiFilters = ['armeabi-v7a']
// optional属性 压缩算法,apache commons compress⽀支持的算法,默认为lzma algorithm = 'lzma'
// optional属性 debug包时是否执⾏行行本⼯工具,默认为false
debugModeEnable = false
// optional属性,压缩过程中是否对⽂文件进⾏行行校验,默认为true
verify = true
}
@TaskAction
void taskAction(){
// 如果输入文件目录和输出文件目录不存在,打断执行流程
if(inputFileDir==null||outputFileDir==null){
return
}
// optional属性 压缩算法,apache commons compress⽀支持的算法,默认为lzma ,内部不支持该压缩算法
if(!SUPPORT_ALGORITHM.contains(config.algorithm)){
throw new IllegalArgumentException( "only support one of ${Arrays.asList(SUPPORT_ALGORITHM).toString()}" )
}
def gradleVersion=0
project.rootProject.buildscript.configurations.classpath.resolvedConfiguration.resolvedArtifacts.each
{
if(it.name== 'gradle' ){
gradleVersion=it.moduleVersion.id.version.replace( '.' , '' ).toInteger()
}}
// 找到输⼊入输出⽬目录
def libInputFileDir=null def libOutputFileDir=null
inputFileDir.each{file->
if(file.getAbsolutePath().contains( 'transforms/mergeJniLibs' )){libInputFileDir=file}}
outputFileDir.forEach{file->
if(gradleVersion>=320&&file.getAbsolutePath().contains( 'intermediates/merged_assets' )){libOutputFileDir=file
}else if(gradleVersion< 320&&file.getAbsolutePath().contains( 'intermediates/assets' )){libOutputFileDir=file
}}
// 如果lib输入文件夹为空和lib输出文件夹为空,抛异常
if(libInputFileDir==null){
throw new IllegalStateException( 'libInputFileDir is null' )
}
if(libOutputFileDir==null){
throw new IllegalStateException( 'libOutputFileDir is null' )}
// tarFileNameArray定义了需要打包压缩的本地库⽂件列表
String[]tarFileArray=config.tarFileNameArray
// compressFileNameArray 需要压缩本地库⽂件名
String[]compressFileArray=config.compressFileNameArray
// 遍历lib的文件,需要打包压缩的本地库⽂件列表里面有目标文件
tarFileArray.each{fileName->
// 被压缩的文件目录里有目标文件
if(compressFileArray.contains(fileName)){
// 抛异常处理
throw new IllegalArgumentException( "${fileName} both in tarFileNameArray & compressFileNameArray" )
}}
def soCompressDir=new File(libOutputFileDir,CompressConstant.SO_COMPRESSED)soCompressDir.deleteDir()
if(tarFileArray.length!=0){
// 打包压缩的本地库⽂件进行排序
tarFileArray.sort()
compressTar(tarFileArray,libInputFileDir,libOutputFileDir)
}
if(compressFileArray.length!=0){
// 压缩本地库⽂件名进行排序
compressFileArray.sort()
// 压缩本地库⽂件名进行压缩
compressSoFileArray(compressFileArray,libInputFileDir,libOutputFileDir)
}}
3.3 动态加载 So 库与资源
动态资源加载主流程
下载资源包流程
动态资源的加载系统的下载一个资源包的主流程如下,首先根据资源包 id 创建对应的下载目录,之后判断资源包指定版本号和本地数据库版本号是否相同,如果想同,进入本地资源包校验流程,否则进入下载流程。
下载校验解压流程
本地资源包校验流程
文件校验流程
So 装载流程
动态资源加载业务痛点
痛点一 资源包下载功能由自己实现还是业务实现?
痛点二 如何确定使用网络资源包还是使用本地历史资源包?
痛点三 资源包如何校验,校验资源包信息,判断资源包是否正常?
痛点四 解压缩资源包的依据是什么
痛点五 如何保证第三方 sdk 缺少 So 文件时,不崩溃?
public class ThirdLib{
//静态方法加载so库
static{
System.loadLibrary("third");
}
}
protected void realSoLoad(Context c, String libName) {
try {
ReLinker. recursively ().loadLibrary(c, libName);
removeFormWaitList(libName);
} catch (Throwable t) {
addToWaitList(libName);
}
}
public void loadSo(DynamicSoInfo soInfo, ILoadSoListener listener) {
if (soInfo == null) {
return;
}
//根据本机abi,获取适合的动态资源实体类DynamicPkgInfo
DynamicPkgInfo pkg = soInfo.getPkgInfo(Build.SUPPORTED_ABIS);
if (pkg == null) {
return;
}
//如果该so资源,已经被加载缓存过了,直接listener的成功回调,并返回
if (isLoadAndDispatchSo(pkg, listener)) {
return;
}
//开启资源加载,和普通资源流程一致
DynamicResManager manager = DynamicResManager.getInstance();
manager.load(pkg, new DefaultLoadResListener() {
@Override
public void onSucceed(LoadResInfo info) {
super.onSucceed(info);
//so成功下载校验后,执行加载逻辑
handleLoadSoSucceed(pkg, info, listener);
}
});
}
痛点六 如何下载 So 文件,并保证它的正确性?
private static void install(ClassLoader classLoader, File soFolder) throws Throwable {
Field pathListField = findField(classLoader, "pathList" );
// DexPathList类的实例
Object dexPathList = pathListField.get(classLoader);
// 包含了本App自带so文件的查找路径(如data/app/包名/lib/arm64)
Field nativeLibraryDirectories = findField(dexPathList, "nativeLibraryDirectories" );
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
libDirs.add(0, soFolder);
// 包含系统so文件查找路径(如system/lib64)
Field systemNativeLibraryDirectories =
findField(dexPathList, "systemNativeLibraryDirectories" );
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
// 系统使用此方法,为所有so文件,生成对应的 NativeLibraryElement对象
Method makePathElements =
findMethod(dexPathList, "makePathElements" , List.class);
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.
invoke(dexPathList, libDirs);
// 系统用来存储所有的so文件路径
Field nativeLibraryPathElements = findField(dexPathList, "nativeLibraryPathElements" );
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
痛点七 怎么了解 APK 里所有 So 文件具体的依赖信息呢?
痛点八 对于 So 加载异常情况有具体的兜底方案吗?
痛点九 支持断点续传吗?会重复下载吗?
痛点十 有文件完整性校验吗?
痛点十一 怎么避免 64 位设备下到 32 位 So 文件?
private Map<String,DynamicPkgInfo> mSoInfos;
public DynamicPkgInfo getPkgInfo(){
//获取本地系统支持的abi列表String[] supportAbis = Build.SUPPORTED_ABIS;
if(supportAbis==null || supportAbis.length== 0 ){
return null;
}
//遍历abi支持列表for(String abi supportAbis){
//从so动态资源中,查找对应的abi信息DynamicPkgInfo pkg = mSoInfos.get(abi);
//找到则直接返回该信息if(pkg != null){
return pkg;
}
}
return null;
}
痛点十二 远程 So 的选定标准是什么?
痛点十三 统计上报功能,如何统计并上报资源加载的成功率?
痛点十四 动态资源应用如何加载到对应 View 上?
痛点十五 如何移除 apk 中的 So 文件,并将他们收集起来?
//获取系统的mergeTask
Task mergeNativeTask = TaskUtil.getMergeNativeTask(project);
//获取系统的skipTask
Task stripTask = TaskUtil.getStripSymbol(project);
//创建我们的DeleteAndCopySo task
Task deleteTask = project.getTasks().create(PluginConst.Task.DELETE_SO);
deleteTask.doLast(new Action<Task>() {
@Override
public void execute(Task task) {
deleteAndCopySo(project, param);
}
});
//将我们的Task插入到merge和strip之间
stripTask.dependsOn(deleteTask);
deleteTask.dependsOn(mergeNativeTask);
痛点十六 如何将多个 So 文件压缩打包,并生成对应的信息?
//创建DynamicResConst类,用来存储资源实体常量
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( "DynamicResConst" )
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
//遍历资源列表,生成对应实体类DynamicPkgInfo
for (DynamicPkgInfo pkg pkgs) {
FieldSpec fsc = createField(pkg);
typeBuilder.addField(fsc);
}
//插件java文件,并写入
JavaFile javaFile = JavaFile.builder(param.getmCreateJavaPkgName(), typeBuilder.build()).build();
try {
javaFile.writeTo(new File(param.getmOutputPath()));
} catch (Exception e) {
}
//创建DynamicResConst类,用来存储资源实体常量
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( "DynamicResConst" )
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
//遍历资源列表,生成对应实体类DynamicPkgInfo
for (DynamicPkgInfo pkg pkgs) {
FieldSpec fsc = createField(pkg);
typeBuilder.addField(fsc);
}
//插件java文件,并写入
JavaFile javaFile = JavaFile.builder(param.getmCreateJavaPkgName(), typeBuilder.build()).build();
try {
javaFile.writeTo(new File(param.getmOutputPath()));
} catch (Exception e) {
}
3.4 本地图片转网图
3.5 插件化技术预研
小木箱推荐大家使用 Shadow 插件化工具,因为 Shadow 主要具有五个特点,第一、复用独立安装 App 的源码,插件 App 的源码原本就是可以正常安装运行的。第二、零反射无 Hack 实现插件技术,从理论上就已经确定无需对任何系统做兼容开发,更无任何隐藏 API 调用和 Google 限制非公开 SDK 接口访问的策略完全不冲突。第三、全动态插件框架,一次性实现完美的插件框架很难,但 Shadow 将这些实现全部动态化起来,使插件框架的代码成为了插件的一部分。插件的迭代不再受宿主打包了旧版本插件框架所限制。第四、宿主增量极小,得益于全动态实现,真正合入宿主程序的代码量极小(15KB,160 方法数左右)。第五、Kotlin 实现,core.loader,core.transform 核心代码完全用 Kotlin 实现,代码简洁易维护,最重要的是 Shadow 经过腾讯线上亿级用户量检验,号称“零 hook”。感兴趣可以听一下Shadow[18]教程。
货拉拉 Android 动态资源管理系统原理与实践[19] 我的 Android 重构之旅,动态下发 So 库(上)[20] 动态下发 So 库在 Android APK 安装包瘦身方面的应用 【保姆级】包体积优化教程[21] SoLoader,android 动态加载 So 库[22] Android 动态加载 So!这一篇就够了![23] ReLinker[24] SoLoader[25] 阿里某淘 Android 体积优化方案[26] 货拉拉 Android 包体积优化实践[27]
参考资料
[1]包体积优化 · 方法论 · 揭开包体积优化神秘面纱:
https://juejin.cn/post/7177195746272215098
[2]包体积优化 · 工具论 · 初识包体积优化:
https://juejin.cn/post/7176622003888226362
[3]BaguTree包体积优化录播视频课:
https://www.bilibili.com/video/BV1g14y137SM/?spm_id_from=333.999.0.0&vd_source=4826f2b29f4c7bc518600645c161843b
[4]货拉拉 Android 动态资源管理系统原理与实践:
https://juejin.cn/post/7113703128733581342
[5]我的 Android 重构之旅,动态下发 So 库(上):
https://www.jianshu.com/p/260137fdf7c5
[6]动态下发 So 库在 Android APK 安装包瘦身方面的应用:
https://link.juejin.cn/?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FX58fK02imnNkvUMFt23OAg
[7]SoLoader,android动态加载So库:
https://blog.csdn.net/dzsw0117/article/details/89923599
[8]Android动态加载So!这一篇就够了!:
https://juejin.cn/post/7107958280097366030
[9]ReLinker:
https://github.com/KeepSafe/ReLinker
[10]SoLoader:
https://github.com/facebook/SoLoader
[11]阿里某淘Android体积优化方案:
https://juejin.cn/post/7094083986334433317
[12]FileDownloader:
https://github.com/lingochamp/FileDownloader
[13]货拉拉 Android 动态资源管理系统原理与实践:
https://juejin.cn/post/7113703128733581342#heading-24
[14]FileDownloader:
https://github.com/lingochamp/FileDownloader
[15]包体积优化 · 工具论 · 初识包体优化 :
https://juejin.cn/post/7176622003888226362
[16]-classyshark:
https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fgoogle%2Fandroid-classyshark
[17]oss-browser:
https://github.com/aliyun/oss-browser
[18]Shadow:
https://www.bilibili.com/video/BV1LZ4y1S7w1/?p=9&spm_id_from=pageDriver&vd_source=4826f2b29f4c7bc518600645c161843b
[19]货拉拉 Android 动态资源管理系统原理与实践:
https://juejin.cn/post/7113703128733581342
[20]我的 Android 重构之旅,动态下发 So 库(上):
https://www.jianshu.com/p/260137fdf7c5
[21]【保姆级】包体积优化教程:
https://juejin.cn/post/7116089040264232967
[22]SoLoader,android动态加载So库:
https://blog.csdn.net/dzsw0117/article/details/89923599
[23]Android动态加载So!这一篇就够了!:
https://juejin.cn/post/7107958280097366030
[24]ReLinker:
https://github.com/KeepSafe/ReLinker
[25]SoLoader:
https://github.com/facebook/SoLoader
[26]阿里某淘Android体积优化方案:
https://juejin.cn/post/7094083986334433317
[27]货拉拉 Android 包体积优化实践:
https://posts.careerengine.us/p/62d391084d56fc1cd1df032c
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!