随着业务不断迭代更新,App的规模也在快速增长。 2019年到2022年间一度超过117M。这期间我们也做了一些优化,如图1红色部分所示,但在做优化的同时,我们又面临了新的挑战。随着代码的增量,包的大小不断增大。包体积直接或间接影响下载转化率、安装时间、磁盘空间等重要指标,因此有必要投入精力探索更深层次的安装包体积优化。根据Google Store内部数据,APK大小每减少10M,下载转化率平均可提高约1.5%,如图2所示:
图1 2019年至2022年京东金融Android版本交易量变化过程(红色部分为期间所做的部分优化,但很快回升)
图2 Google Store应用转化率提升/10M[1]
2.APK分析
接下来我们简单分析一下Apk中的各个组件,以及Apk作为ZIP的标准结构是什么样的,为包瘦身目标设定和任务拆解提供数据支持。
2.1 APK内容分析
图3 APK结构
•classes.dex APK可能包含一个或多个classes.dex文件,应用内的Java/Kotlin源代码最终会以字节码的形式存在于classes.dex文件中。
•resources.arsc aapt 工具在编译资源时会将一些资源或资源索引打包到resources.arsc 中。
•res/源代码项目res目录下的值除外的资源文件。这些文件路径也会记录在resources.arsc中。
•lib/nativeLibraries,即源码项目jni目录下的so文件。二级目录是NDK支持的ABI。
•Assets/与res/资源目录不同。 asset/下的资源文件不会在resources.arsc中生成查询条目,并且assets/下的资源目录可以完全自定义并通过程序中的AssetManager对象获取。
•META-INF/该文件夹主要包含CERT.SF和CERT.RSA签名文件,以及MANIFEST.MF清单文件。
•AndroidManifest.xml应用程序清单文件,用于描述应用程序的基本信息,主要包括应用程序包名称、应用程序id、应用程序组件、所需权限、设备兼容性等。
2.2 SDK大小分析
通过我们自主研发的能效提升平台Pandora[7],我们可以直观地看到SDK的大小,如图4所示:
图4 SDK大小排序(含版本号)
图5 SDK包含的SO库列表及大小
基于SDK分析,结合业务,可以判断哪些业务适合插件,从而直观地减小包大小。
2.3 ZIP结构分析
您可以使用zipinfo命令输出压缩包中每个文件的详细信息日志。用法:zipinfo -l --t --h test.apk test.txt
打开输出日志文件如图6所示,每个文件有一行压缩信息,包括文件名、原始大小、压缩后大小等指标:
图6 APK中文件信息大小
对上述日志信息进行逐行分析,根据反混淆后的文件名路径和文件类型进行分类统计,获取Apk的概览信息,包括各类文件数量、总大小、单个文件大小等指标。并建立文件大小索引。
3、减肥练习
整体实现路径如图7所示,主要分为:
1、常规技术方案,通过Gradle插件(代码非侵入式、自动化)在编译时完成APP瘦身;
2、先进的技术方案,部分业务线可以通过插件或者SO动态下载进行差异化改造。转型的企业越多,收入就越高;
3、业务优化方案,针对业务线的数据埋点,生成接入UV进行排名,并将UV较低的业务线反馈给架构委员会,评估是否可以下线或者通过先进的技术方案进行改造(2),然后减小包装尺寸。
图7 总体实现路径
3-1 常规技术方案
3-1-1 图像处理
经过以上对APP的分析,得出图片占据体积最大的结论。因此,APP的SDK中的所有图片都会在编译打包过程中通过瘦身任务自动优化。整体优化方案如图8所示:
图8 图像优化方案
1.多DPI优化:
为了适应不同分辨率或模式的设备,Android为开发者设计了对同一资源有多种配置的资源路径。当app通过resources获取图片资源时,会根据设备配置自动加载适配的资源,但这些配置都伴随着问题,高分辨率设备包含低分辨率无用图片或者低分辨率设备包含高分辨率解决无用的图像。
2.转换为webp格式:
WebP是Google提供的一种图像文件格式,支持有损压缩和无损压缩,并且可以提供比JPEG或PNG更好的压缩。支持Android 4.0(API 级别14)中的有损WebP 图像、Android 4.3(API 级别18)及更高版本中的无损透明WebP 图像
因此:我们使用插件只保留编译时的图像,通过Google提供的shell程序进行格式转换。转换成功删除旧镜像,从而达到APK瘦身的效果。
3.png压缩
Pngquant 是一个有用的png 压缩工具。它是一个可以执行有损图像压缩的命令行工具。因此,在处理1和2之后,可以使用Pngquant进行二次压缩,以达到更好的图像瘦身效果。
3-1-2 R文件内联优化
DEX 是由Java/Kotlin 源代码编译而成的字节码文件。 DEX的优化其实就是如何优化字节码文件。 DEX包含大量资源索引R文件。这里主要讲一下如何通过资源ID进行内联。 R文件删除达到APK瘦身的目的:
R文件瘦身的可行性分析
日常开发阶段,在主项目中通过R.xx.xx引用资源。编译后,R类引用的相应常量将被编译到类中。
设置ContentView(2131427356);
这种变化称为内联,这是Java的一种机制(如果一个常量被标记为staticfinal,那么该常量会在Java编译过程中被内联到代码中,减少变量的内存寻址)。在非主项目中,R类资源ID通过引用编译成类,并且不会内联。
setContentView(R.layout.activity_main);
造成这种现象的原因是AGP封装工具。具体可以查看android gradle插件对R文件的处理过程。结论:R类id内联后程序可以运行,但并不是所有项目都会自动产生内联。我们需要利用技术手段,在合适的时候将R class id内嵌到程序中。内联完成后,由于缺少再次依赖R类文件,可以在应用程序正常运行的情况下删除R类文件,实现包瘦身。如图9所示,编译完成后会生成大量R文件:
图9 Project R文件生成图
总体规划如图10所示:
图10 R文件优化流程
注意:替换阶段必须添加二次检查,防止替换完成后运行时出现ResourceNotFind异常,如下图:
try { int value=RManager.checkInt(type, name);}catch (Exception e){ String errorMsg='找不到资源(I),className='+className+',fieldName='+owner+'.'+name ; throw new ResourceNotFoundException(errorMsg);}try { int[] value=RManager.checkIntArray(type, name);}catch (Exception e){ String errorMsg='资源未找到(I[]),className='+ className+ ',fieldName='+所有者+'.'+名称;抛出新的ResourceNotFoundException(errorMsg);}
3-1-3 AndResGuard进行资源混淆
1.资源加载过程分析
开发过程中,我们通过aapt生成的R.java中的常量来使用资源,编译后,使用常量的地方会被常量的值替换,如下所示:
最终视图布局=inflater.inflate(2131165182, 容器, false);
也就是说我们通过Resource使用一个int值来查找资源。那么Resource是如何通过int值来查找特定资源的呢?当我们解压apk后,我们可以看到里面有一个resources.arsc文件。该文件也是由aapt 生成的。该文件存储了资源id和资源key的映射关系。 Resource根据这个映射关系找到资源。
2.资源.arsc:
图11展示了resources.arsc中存储的映射关系。 resources.arsc可以理解为一个资源映射数据库,其中根据ID映射出具体的路径和名称。
图11 resources.arsc分析
解压APK后,将res/layout/hello.xml等资源文件名短链为r/l/a.xml,然后更改resources.arsc对应的值,达到整体瘦身的效果。
AndResGuard[5]是微信推出的资源优化工具。其基本思想与ProGuard中的混淆类似,可以实现上述解决方案。
3-1-4 7zip压缩
7zip命令解释:
-t: 指定压缩类型,支持7z、xz、split、zip、gzip、bzip2、tar、
-m: 指定压缩算法,默认为Deflate
具体流程如下:
第一步:使用7z命令将未签名的包解压到指定目录:7za x ${未签名的包} -o${7z解压目录}
第二步:首先使用7z命令对解压目录进行完全压缩:7za a -tzip -mx9 ${目标7z文件名} ${7z解压目录}
第三步:获取存储类型文件并获取压缩方式的文件列表通过Android SDK中的aapt命令存储:aapt l -v ${unsigned package}
第四步:更新存储类型文件,使用7z命令将存储类型文件更新为第二步生成的7zip安装包: 7za a -tzip -mx0 ${目标7z文件名} ${存储类型文件目录}
3-1-5 配置CPU架构
根据不同的CPU架构构建不同类型的安装包。目前主流设备都是64位机器,因此Android市场主要使用基于arm64-v8a编译构建的安装包。
ndk { abiFilters arm64-v8a}
3-1-6 arcc 压缩
resources.arsc的压缩体积增益较高,但压缩会影响启动速度和内存指标。原因是:系统加载arsc文件时,如果arsc文件没有被压缩,则可以使用mmap进行内存映射;如果arsc文件是压缩的,则需要解压并读入RAM缓冲区,这样会增加内存占用,同时也会减慢启动速度。
3-1-7国际语言处理
京东金融App目前仅在国内市场运营,但在连接的大量SDK中又增加了数十种语言,导致整个体量变得更大。评估后,可以通过配置resConfigs来删除无用的语言资源。
defaultConfig { resConfigs 'zh','en'}
3-1-8 收缩资源
ShrinkResources:在编译过程中用于检测并删除无用的资源文件,即没有引用的资源。
minifyEnabled:用于启用删除无用代码,例如未引用的代码,因此如果需要知道某个资源是否被引用,则必须使用minifyEnabled。只有当两者都为真时,它才会真正删除无效代码和未引用的资源。目的。
它的作用是用一个很小的格式文件替换未被引用的资源文件(仍然有占位卷,并且保留了资源条目,因此resources.arsc的大小不会减少),可以通过res/访问raw/keep.xml文件配置shrinkMode和白名单。
buildTypes { release { //不显示Log buildConfigField 'boolean', 'LOG_DEBUG', 'false' //混淆minifyEnabled true //删除无用的资源文件shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard - rules.pro'signingConfigsign.release}}
3-1-9 编码约束
尽量少用枚举类型,因为枚举编译成字节码后,会增加很多大小,如图12(22行代码编译后的字节码为86行)
•
图12 枚举类型编译后的字节码对比
•删除不必要的LOG日志输出
3-2 先进技术方案
SO库动态下载和插件技术本质上属于动态下载的范畴。两种方案可以在业务中长期连续使用。具体使用过程中如何选择如图13所示:
图13 企业如何选择先进的解决方案
3-2-1 SO库的动态加载
APP内部分服务不适合插件化改造。拆解后发现SO库占比很大。因此可以考虑采用动态下载进行改造,以减小体积。
加载SO库的两种方式
第一种方式是直接下载SO库并放到指定目录下。
第二种方式是加载环境变量设置的目录下的SO库。因此,我们需要将指定的目录追加到环境变量中,才能正常加载SO库。
System.load('{安全路径}/libxxx.so') System.load('xxx')
1.如何在APP中设置SO库的环境变量位置(向Tinker学习):
最终字段pathListField=ShareReflectUtil.findField(classLoader, 'pathList');最终对象dexPathList=pathListField.get(classLoader);最终字段nativeLibraryDirectories=ShareReflectUtil.findField(dexPathList, 'nativeLibraryDirectories');ListorigLibDirs=(List) nativeLibraryDirectories.get( dexPathList);if (origLibDirs==null) { origLibDirs=new ArrayList(2);}final IteratorlibDirIt=origLibDirs.iterator();while (libDirIt.hasNext()) { Final File libDir=libDirIt.next(); } if (folder.equals(libDir)) { libDirIt.remove();休息; }}origLibDirs.add(0, 文件夹);最终字段systemNativeLibraryDirectories=ShareReflectUtil.findField(dexPathList, 'systemNativeLibraryDirectories');ListorigSystemLibDirs=(List) systemNativeLibraryDirectories.get (dexPathList);if (origSystemLibDirs==null) { origSystemLibDirs=new ArrayList (2);}final ListnewLibDirs=new ArrayList(origLibDirs.size() + origSystemLibDirs.size() + 1);newLibDirs.addAll(origLibDirs);newLibDirs .addAll(origSystemLibDirs);final Method makeElements=ShareReflectUtil.findMethod(dexPathList, 'makePathElements', List.class);final Object[] elements=(Object[]) makeElements.invoke(dexPathList, newLibDirs);final Field nativeLibraryPathElements=ShareReflectUtil .findField(dexPathList, 'nativeLibraryPathElements');nativeLibraryPathElements.set(dexPathList,元素);
2、如何删除指定的SO库以及整个加载过程,如图14:
图14 SO库删除和加载流程
3-2-2 插件
什么是插件:
插件化就是根据业务功能将一个Apk拆分成不同的子Apk(即不同的插件)。每个子Apk都可以独立编译打包,集成后的Apk最终上线发布。使用Apk时,各个插件都是动态加载的,而且插件还可以热修复、热更新。
•Host:主App可以用来加载插件,也可以成为Host
•Plug-in:插件App,宿主机加载的App,可以和普通App是同一个Apk文件
哪些业态适合插件化改造:
•业务相对独立,与宿主App完全解耦
•装修成本低,回报相对较高
•占用空间较大
经过一系列评估,视频业务满足以上几点,改造后的效果如图15所示:
图15 视频营业厅插件改造后的效果
3-3 业务优化方案
随着业务越来越多,一些老业务UV越来越低,因此制定了一套业务线下优化流程,如图16所示:
图16 业务优化解决流程
4. 控制
减肥计划的实施很重要,后续管控防止反弹更重要。我们在做减肥管理的同时,也在探索常态化的管控机制,最终确定了一套管控规范和管控机制。管控的目的不是为了限制业务迭代或者新代码,而是为了在有限的代码中实现功能,提高工程师日常编码的减肥意识。
4.1 SDK接入规范
为了防止SDK无序扩展,制定了SDK接入规范,在保证功能的同时严格控制SDK的大小,最大程度地控制APP大小的反弹。
4.2 管控流程
图17 管控流程
根据资源文件的变化,如添加内容、删除内容、增加内容、减少内容、重复文件、代码管理等,结合治理和控制规范进行管理。包构建完成后,会与历史版本进行差异比较,获取变更内容。评估是否有优化空间并给出优化目标,优化后重新构建打包集成。
5. 结果及后续规划
5.1 结果
通过上述措施,京东金融的Android版本在两个季度内迭代了5个版本,从1.17亿到目前的7400万(图18)。 Android版本整体保持在可控范围内。同时,在下一个版本迭代中,我们将常态化APK瘦身,始终将包大小保持在可控范围内。
图18 金融APP减肥结果
5.2 后续规划
技术手段不断优化:
业务的不断积累和迭代总会产生一些无用的资源,所以安装包瘦身一定要定期清理这些无用的文件和代码;
做好每个版本的监控,比较版本之间的差异,发现可以在不影响业务的情况下用技术手段进行优化。
在线管控平台建设:
前期采用的是离线管控,实施起来有点耗时。未来我们将完善线上管控平台的建设,并与整个App发布建设平台融为一体,形成流水线机制,保证良好的管控。
【参考】
[1] 包大小和安装转化率https://medium.com/googleplaydev/shrinking-apks-forming-installs-5d3fcba23ce2
[2] ProGuard https://www.guardsquare.com/proguard
[3] R8https://r8.googlesource.com/r8
[4] ProGuard与R8对比https://www.guardsquare.com/blog/proguard-and-r8
[5]AndResGuardhttps://github.com/shwenzhang/AndResGuard
[6] AGPhttps://developer.android.com/studio/releases/gradle-plugin
用户评论
这篇文章写的真棒!我一直在想手机上的京东金融软件怎么总是占空间那么大,没想到他们已经开始了瘦身的探索了。 佩服他们的技术实力,希望后续能给用户带来更流畅的体验!
有7位网友表示赞同!
一直觉得Android京东金融的体积真是太吓人了,各种权限也占据很多地方,希望能看到这篇文章讲到的“实践”,真的一直用着都感觉很臃肿。 希望手机内存可以轻松一些,哈哈!
有5位网友表示赞同!
作为Android用户,对软件瘦身的探索和实践一直很关注。京东金融的这种做法很有意义,可以促进移动应用的健康发展。 文章解释的很细致,特别是“基于模块化的拆解”的设计理念,让我受益匪浅!
有17位网友表示赞同!
说实话,我对这篇文章不太感冒,瘦身了有什么用?关键是看功能和服务好不好! 如果能把我的贷款利率降低点,我感觉就算京东金融再大一点也没关系。 毕竟现在的利息真的很让人头疼啊!
有5位网友表示赞同!
我是一个技术人员,对文章内容非常感兴趣。“组件化”的思想在移动应用开发中越来越重要,看看京东金融是如何实施这个理念的,很有收获。期待他们分享更多技术细节。
有9位网友表示赞同!
最近手机卡顿得厉害,怀疑是京东金融app占用了太多资源!这篇文章提到瘦身方案很有希望,期待他们在下次更新时能实现这些改进! 希望我的内存不再受到威胁了!
有7位网友表示赞同!
文章讲的比较专业,我一个非技术人员不是很懂。但总的来说这种“Android瘦身探索与实践”对我来说没什么意义,我只想知道软件好用能不能解决我的问题才行啊。
有17位网友表示赞同!
其实我一直都不太习惯京东金融这个app,界面复杂,功能太多,反而感觉很累赘。希望他们继续优化体验,简化操作流程,让我用的更加轻松舒适!
有12位网友表示赞同!
这篇文章太专业了,对我不太有用。我主要想了解京东金融的利率、政策等信息。 希望后续能提供更直观的引导和服务。
有19位网友表示赞同!
对于科技感很强的朋友来说,这篇文章一定很有帮助吧!可惜我不太懂技术细节,只想简单方便地使用京东金融 app 做理财就可以了。
有11位网友表示赞同!
我觉得这个“瘦身”计划挺好,但更重要的是提高app的稳定性,减少运行过程中出现的问题。 希望京东金融能将更多精力投入到系统优化上,提升用户体验!
有20位网友表示赞同!
我感觉这篇博文写的有点枯燥,没有太多实质性的内容。我只是想知道他们做了哪些具体的操作来让手机软件变得更小巧,能够降低手机的内存占用率?
有15位网友表示赞同!
我一直觉得京东金融的安卓版操作不太友好,希望他们能借此机会改进用户界面设计,提升app的便捷性和美观度!
有12位网友表示赞同!
作为Android用户,我对京东金融的这款瘦身方案感到很欣慰和期待。 希望这种精细化的改进能够让手机运行更加流畅,同时提高软件的用户体验和稳定性!
有15位网友表示赞同!
对于我来说,京东金融的功能才是最重要的,希望能继续完善贷款、理财等方面的服务,而不是只关注app的大小变化!
有16位网友表示赞同!
希望京东金融能继续探索瘦身方案,让我这个Android用户可以拥有更轻便、流畅的手机使用体验。 期待后续更多技术细节的分享和应用实践!
有14位网友表示赞同!
我觉得这篇文章很有深度,作者很好的分析了“Android瘦身探索与实践”的关键点和挑战。 希望京东金融能持续投入研发,为我们提供更加优质的服务!
有5位网友表示赞同!