一、导言
Hello,我是小木箱,欢迎来到小木箱生长营系列教程,今日将共享Android功用优化 实战论 怎样做包体优化? 做好能提升吗? 能涨多少钱?
上一次共享,小木箱从三个维度将Android包体优化办法论解释清楚,榜首部分内容是针对So优化,第二部分内容是针对Res资源优化,第三部分内容是针对Assets/Raw资源优化。
关于怎样做包体优化? 小木箱首要是共享两部分内容解说,榜首部分内容是包体优化进程,第二部分内容是包体优化面对的事务痛点。包体优化的进程首要分为七部分,榜首部分是优化方针,第二部分是优化排期,第三部分是优化记载,第四部分是阶段效果,第五部分是衡量方针,第六部分是CI/CD监控与预警,第七部分是采坑记载。而事务痛点首要分为五部分。榜首部分是CI/CD集成监控包体健康度,第二部分是So库紧缩与解压机制,第三部分是动态加载So库与资源,第四部分是本地图片转网图,第五部分是插件化技能预研。
假如学完小木箱Android功用优化的东西论、办法论和实战论,那么任何人做包体优化都能够拿到效果。
二、优化进程
2.1 优化方针
首要小木箱来解说榜首部分内容优化方针,优化方针首要分为四部分内容,包体剖析、 版别比照 、 竞品比照和攻坚方针。
2.1.1 包体剖析
包体剖析首要凭借的是腾讯AppChecker完结的,AppChecker剖析包文件首要仍是凭借了andoid-build/tool下面的 aapt东西。关于AppChecker运用指南能够参阅下面的链接:
github.com/Tencent/mat…
2.1.2 版别比照
APP装置包巨细改动的趋势
然后小木箱利用运用市场发包状况能够制作iOS/Android的装置包巨细改动趋势图
不同版别包体波动因子概况剖析
接着,小木箱拿最近四个版别的APP解包剖析资源占用状况,对不同版别包体波动因子概况进行剖析,其间倒数第四个版别作为基准线,当然也能够利用git东西比照剖析代码特征.
2.1.3 竞品比照
剖析完自身APP资源占用状况今后,再拿竞品APP进行剖析,优化流程一起帮咱们承认优化方针。
2.1.4 攻坚方针
确保中心事务稳定性前提下,小木箱就确认优化方针,如 main.apk size 低于 simulation1.apk size xx%。
2.2 优化排期
承认好优化方针后,小木箱对优化内容进行排期分工,排期表格模板如下
2.3 优化记载
每一个上线的需求咱们要做好留档,便利复盘、沉淀、总结
排期A
排期B
排期C
2.4 阶段效果
最终,要向上输出阶段性效果,如 以V2.11正式包提交的节点xxxx为基准,apk巨细为xxxM
2.5 衡量方针
测验进行回归测验首要的测验点有两个
- 打包后体积巨细
- 装置速度
2.6 CI/CD监控与预警
接着,咱们来聊聊CI/CD监控与预警,CI/CD监控与预警首要分为14部分内容,别离是机器人告警才能、APK文件主图、APK文件巨细排行榜、重复资源剖析、无用资源剖析、依靠树结构图、重复代码剖析、无用代码剖析、不合规图片转化紧缩、办法数汇总陈述图、构建产品版别差异图、APK版别趋势折线图、爱加密黑盒质检陈述和差异版别优化主张
-
机器人告警才能
机器人告警才能模块,机器人机器人引入了开发环境,合码巨细低于预估方针阈值发送警告告诉如下
-
APK文件主图
APK文件主图模块用AppChecker东西剖析的文件巨细占比饼状图汇总(如下,通过Echarts或其他组件烘托)
明晰需求后,运用墨刀制作产品设计稿给前端展现apk_file_size、apk_download_size、运用包名、版别称号、版别号、发动Activity、方针SDK版别号、app及arr依靠权限列表等APK表格数据
-
APK文件巨细排行榜
APK文件巨细排行榜能够参阅下图,按照巨细从上至下进行排序即可
-
重复资源剖析
类似图片监测或许需求运用AI技能,重复资源剖析用AppChecker即可完结目的。
-
无用资源剖析
当然无用资源剖析也能够用AppChecker完结。
-
依靠树结构图
依靠树版别管控能够通过版别进行映射比照剖析,注意要展现仓库之间的依靠层级关系。
-
重复代码剖析
重复代码咱们能够用FireLine进行扫描剖析.
-
无用代码剖析
无用代码比较费事一点,需求咱们自界说Lint来完结.
-
不合规图片转化紧缩
不合规图片转化也需求通过插件来完结,假如不想侵入代码,用脚本履行后把图片透传给前端烘托即可.
-
办法数汇总陈述图
-
构建产品版别差异图
-
APK版别趋势折线图
-
爱加密黑盒质检陈述
企业需求和爱加密商务协作,后台服务生成监测陈述,CI/CD不设置卡口阻断流程。但会通过机器人将文档链接供给事务整改。
-
差异版别优化主张
2.7 采坑记载
2.7.1 问题
常识层面,尽力提高Docker 、计算机组成原理 、K8S、Linux 、gitwebhook 、机器人、Gitlab-ruuner、Githook、Kubernetes、反编译技能栈
产品层面,熟练把握墨刀、Axure、Pencil、 Mockups和Visio等产品东西运用
作业办法层面,熟练把握根底作业办法论,多进行脑筋风暴,有时间挨怼的心态,交流才能十分重要
事务层面,包体检测独立使命并发仍是并行?
2.7.2 原因
2.7.3 处理办法
看金字塔原理?
Devops && SRE开发常识巩固?
…..
2.7.4 考虑
-
爱加密和隐私合规查看结合烘托产品设计怎样做更人性化?
-
推广事务承受度怎样?
-
能否打成一个jar文件,然后通过命令办法将静态页面烘托生成一份可视化陈述给社区运用?
-
代码混杂作业能否在打包进程完结?
-
关于通过 Google Play 分发的运用,不得选用 Google Play 更新机制以外的其他任何办法修正、替换或更新运用自身。相同地,运用不得从 Google Play 以外的其他来源下载可履行代码(例如 dex、JAR 和 .So 文件)。对此,资源动态化只能满足国内的需求。注意供给开关?
三、事务痛点
3.1 CI/CD集成监控包体健康度
包体健康度是一项比较重要但是或许被忽略的质量方针。臃肿繁杂的运用装置包不但存在更高的质量和稳定性隐患,使得问题排查的复杂度相对更大、成本更大;另一方面,装置包巨细直接影响着用户的下载或保存运用的意愿。
单纯的重视整包巨细并不能处理实际问题。许多版别发布流程或平台对运用整包巨细都会有一些约束,从实际状况看发挥的效果十分有限,即便超越阈值也常常会由于事务需求开绿灯。假如没有一个有效的计划对运用包中存留以及新增的代码和资源的合理性进行查看和评估,并给出准确的判别效果辅导事务方进行优化,运用包体积操控就会变成一个痛苦的重复讨论、比照的进程,甚至会常态化的挣扎在包体积巨细的阈值线上面。为此咱们技能质量部推出了包巨细查看才能,取得了不错的效果。
所谓包巨细查看,是依据影响包体积巨细的现实问题别离列出对应的方针,例如资源文件的巨细和引用状况,PNG 图片的运用状况,代码混杂状况,或许各个模块在线上被拜访的热度等等。咱们通过这种查看办法防止了对运用体积简略粗犷的一刀切式办理办法,转为数据驱动、以现实说话的办法,让新的需求能够合理的集成进来,一起又最大极限的坚持了运用体积处在一个健康的状况。
3.2 So库紧缩与解压机制
把握So库紧缩与解压机制之前咱们首要需求把握正常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 办法。
其间System加载SO的代码如下,有两种办法,System.load是加载data/data/包名/lib下面的so文件,System.loadloadLibrary是全途径加载。
关于apk中常⽤到本地类库(so)进⾏紧缩,达成优化包⼤小的目的。不过这儿也有一个前提,能够优化的so是能够推迟加载的,即不是有必要app发动时就要即时加载的 。
完结思路
传送门 github.com/Android-Mai…
SO紧缩和解压思路比较简略,即⼲预gradle apk打包流程,在gradle merge本地库之后,打包apk之前将SO进行紧缩,生成紧缩⽂件保存到assets⽬录之下。
task的执⾏顺序(Develop为productFlavor称号)如下
在app发动时,解压assets⽬目录下的紧缩⽂件,反射classloader,参加解压后的本地库途径 。紧缩和解压装备脚本如下
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
}
自界说Task紧缩代码如下
@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库与资源
动态资源办理首要分为两个方向 ,榜首个方向是动态加载So库,第二个方向是动态加载资源。
首要,咱们先考虑一个问题,Android开发动态加载So库技能是什么?
动态加载So库其实是一些边缘功用的So库或许运用机遇比较晚的So库能够考虑动态加载;其间咱们需求处理32、64位两套动态加载So库。
其二,动态加载资源,首要包含动画包资源或许drawable、assets的字体或html等其他类型资源,当然咱们也能够加载单个文件,多个文件这2种可自界说资源。
动态资源加载主流程
关于So动态化笔者的确没有什么实战经验,既然没有实战经验就权当我做技能计划好了。So动态加载技能考虑来源于货拉拉 Android 动态资源办理体系原理与实践 、我的 Android 重构之旅,动态下发 So 库(上)、动态下发 So 库在 Android APK 装置包减肥方面的运用 、【保姆级】包体积优化教程 、SoLoader,android动态加载So库 、Android动态加载So!这一篇就够了! ReLinker 、SoLoader 和 阿里某淘Android体积优化计划九篇文章。
众所周知的原因,即便咱们从APK资源文件和Dex的巨细动刀,占有APK体积最大的一块仍然是So和Res资源。那么咱们的包体积还有没有优化空间呢?其实仍是有的,咱们能够把一些运用频率相对低一些的资源不打包进apk,需求的时分鄙人载到本地进行运用(这些资源或许包含动画文件,字体文件,So库,zip紧缩包等)。针对此状况,咱们需求一个动态资源的加载体系。
动态资源下载主流程大约分为四个,别离是下载资源包流程、 下载校验解压流程、本地资源包校验流程、文件校验流程和So装载流程。
下载资源包流程
动态资源的加载体系的下载一个资源包的主流程如下,首要依据资源包id创立对应的下载目录,之后判别资源包指定版别号和本地数据库版别号是否相同,假如想同,进入本地资源包校验流程,不然进入下载流程。
下载校验解压流程
咱们鄙人载前,首要判别资源版别号是否和本地数据版别号一致,假如一致,直接走本地资源包校验流程,假如不一致,先删去去本地文件。之后判别存储空间是否满足,存储空间满足时,调用FileDownloader进行资源的下载,下载完结后,咱们进行下载文件的校验,假如校验成功,再判别该文件是否为紧缩包,关于紧缩包,咱们还需求进行解紧缩操作,这便是咱们整个下载校验解压流程。
本地资源包校验流程
关于下载并解压的紧缩包资源,以及本地数据库版别和资源包版别号相同的资源,咱们需求进行本次资源包校验流程。该流程很简略,只需遍历资源包指定的字文件列表,对他们进行逐个文件检验就能够了
文件校验流程
单个文件资源,包含了资源的id,文件称号,资源类型,下载地址,版别号,文件长度以及md5码。多个文件资源,除了包含上述信息外。还包含了该紧缩包解压后,里边每个文件的称号,文件长度以及md5码
单个文件校验的流程,当资源包中指定的文件称号,文件长度,文件md5码和本地文件相同本地文件相一起,咱们以为该文件校验成功了
So装载流程
完结文件的校验的流程,咱们进入So的装载流程,首要获取体系支撑abi列表,依据该列表,找到适宜的So动态资源实体类。假如该资源现已被加载缓存,则回调加载完结监听器。不然,开始资源通用加载流程,并异步等候资源加载成功。再次判别下载校验后的资源,是否支撑本机abi。将So包途径参加DexPathList的数组头部。遍历等候加载So列表,测验加载一切So文件,并将成功加载的So文件,移除该列表。将资源id和本地途径参加缓存,防止So被重复加载。回调加载完结监听器。
为了确保So库不存在时,程序不溃散和So库下载检索完结后,能主动完结之前失利的加载 ,咱们运用开源库Relinder的封装成一个东西类SoLoadUtil.loadLibrary进行加载,流程如下,当接收到SoLoadUtil.loadLibrary办法调用,判别加载体系是否初始化完结,假如已完结,则调用Relinkder库测验加载So文件,未完结则将该So库参加待加载行列中。假如Relinker加载So文件成功,咱们从待加载行列中移除So,并且完结本次加载。不然咱们仍然将So文件参加待加载行列中。依据上面的So加载流程,当So动态资源真正下载校验完结后,咱们会遍历待加载行列,并完结一切之前未成功的So库加载。
那么,怎样运用拦截并将System.loadLabrary替换成咱们封装的SoLoadUtil.loadLibrary办法呢?当然是自界说Plugin+ASM的办法呢。自界说Plugin一共有三个使命,榜首个使命是Hook System.loadLibrary,第二个使命是删去并复制So文件,第三个使命是紧缩So资源和其他多个文件资源。主流程是 首要读取并解析自界说Plugin装备文件。然后依据装备信息,决议是否将3个task参加使命行列。最终发动使命行列。
System.loadLibrary()和System.load()最终都会调用DexPathList 的 findLibrary(),通过 DexPathList 中的 nativeLibraryDirectories 和systemNativeLibraryDirectories两个文件夹调集,生成一个NativeLibraryElement[],然后从这儿面找对应的So,回来全途径,hook了DexPathList 中的 nativeLibraryDirectories,在这个文件夹调集中又添加一个咱们自己界说的文件夹
关于Hook System.loadLibrary使命,我们能够参阅下面一张图,首要便是通过tranform api和asm结构的运用,咱们在其间参加了扫描class规模的可装备项,等候asm结构扫描class。判别该class称号是否在咱们装备的替换列表中,假如不在,就直接回来。创立ClassVisitor和MethodVisitor,等候asm结构扫描每个办法。假如该办法的称号,参数列表和调用者,都和System.loadLibrary办法相符合。咱们替换为自己的SoloadUtil.loadLibrary办法
关于删去并复制So文件使命,详见下图,首要是依据装备文件,找到体系的merge和strip task。然后将咱们的task刺进到2个体系task之间,并等候体系回调咱们的doLast办法。接着遍历体系的mergeTask的输出目录,判别该so文件是否在咱们装备的待扫描列表中。假如装备了需求复制so文件,则咱们将它复制到指定方位。假如装备了需求删去so文件,则咱们将该so文件删去。
关于紧缩So资源和其他多个文件资源 , 首要,复制字体文件,将文件信息参加资源列表。然后,紧缩帧动画文件,将紧缩后的文件信息参加资源列表。其次,紧缩so文件,将紧缩后的文件信息参加资源列表。接着,紧缩zip文件夹下文件,将紧缩后的文件信息参加资源列表。最终,遍历资源文件,为其生成相应的资源实体类。
自界说Plugin三个使命说完了,小木箱简略总结一下,整个工程模块咱们能够看做两个大的方面,榜首方面是SO和资源的加载和运用,也便是打包构建后的根底行为才能,全体架构图能够参阅如下
第二个才能是插件层的才能,也便是小木箱上面所说的三个使命,Hook System.loadLibrary、删去并复制So文件和紧缩So资源和其他多个文件资源。 模块分层明晰合理,大约分为体系插件层、使命模块层和底层完结层 。
全体设计流程大约如此,关于动态加载So库与资源首要是参阅自 货拉拉 Android 动态资源办理体系原理与实践一文,尽管源码没有开源,但文章解说十分详尽,假如企业APP刚好有动态化缩包事务,那么到学习一下此文章事半功倍,也等待货拉拉更优秀的开源项目提前与我们见面。
动态资源加载事务痛点
完结这个动态资源加载计划有16个痛点需求处理。
痛点一 资源包下载功用由自己完结仍是事务完结?
关于下载才能,企业一般是有相关的根底才能支撑,当然业界也有不错的开源东西能够学习,如
英语流利说的FileDownloader,为了让组件责任单一化,防止重复造轮子。因而,咱们将要完结的才能聚集在资源包版别比照、资源包校验、 解压、加载So和计算上报五种才能。
痛点二 怎样确认运用网络资源包仍是运用本地历史资源包?
关于资源包版别比照,咱们无妨把一切的资源信息存储在一个Bean目标里边,如文件称号、长度、md5、下载地址、版别号等以常量形式写在java文件中,并且打包到apk中,后继能够考虑主动生成该java文件。
痛点三 资源包怎样校验,校验资源包信息,判别资源包是否正常?
通过资源信息存储类的Bean目标的版别号比照功用,运用数据库记载本地资源版别号,和资源包信息比照即可,假如资源包校验、校验文件称号、长度和md5码都相同,以为校验通过
痛点四 解紧缩资源包的依据是什么
判别文件格式是否为zip文件,假如是Zip文件,那么就运用Java内置java.util.zip包下东西解压
痛点五 怎样确保第三方sdk短少So文件时,不溃散?
许多三方sdk都要求在运用发动时,进行初始化,一个运用so库的类的典型类代码如下,
public class ThirdLib{
//静态办法加载so库
static{
System.loadLibrary("third");
}
}
假如此刻so库没有被加载好,直接运用ThirdLib类,则会履行static代码段中的System.loadLibrary办法,导致UnsatisfiedLinkError的过错,形成App溃散。由于咱们无法直接修正第三方sdk的源码,因而咱们只能选用动态字节码技能,替换掉System.loadLibrary办法了。咱们选用android的transform加asm技能,动态的将System.loadLibrary替换成咱们自己的SoLoadUtil中的loadLibrary办法。Gradle Transform 是 Android 官方供给给开发者在项目构建阶段,即由 .class 到 .dex 转化期间修正 .class 文件的一套 API, 无论是class仍是jar都能够操控。ASM是一种通用Java字节码操作和剖析结构。它能够用于修正现有的class文件或动态生成class文件。
替换后的办法首要逻辑为,运用第三方库Relinker替代System.loadLibrary办法进行so文件加载,并且catch住加载反常,来防止运用直接奔溃,并且在加载so库反常时,将该库的称号保存下来,在咱们的so包被正常下发加载后,再次调用本办法,将so库load到体系中
protected void realSoLoad(Context c, String libName) {
try {
ReLinker. recursively ().loadLibrary(c, libName);
removeFormWaitList(libName);
} catch (Throwable t) {
addToWaitList(libName);
}
}
这样就处理了SO动态加载溃散的问题。只需求在工程的主Application中,直接调用loadSo办法,对so动态资源进行加载。加载完结后,so库就能正常运用了。
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文件,并确保它的正确性?
当外界调用System.loadLibrary办法时,体系最终会调用到DexPathList类的findLibrary办法,该办法会在nativeLibraryPathElements数组中查找对应的途径,咱们将自己的so参加到nativeLibraryPathElements最前面,由此达到动态参加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 文件详细的依靠信息呢?
参阅Android功用优化 东西论 初识包体优化 #4.2.2.7 Android-classyshark 的运用
痛点八 关于So加载反常状况有详细的兜底计划吗?
假如so下载或许运用失利,sdk运用者会收到失利回调。运用者在此回调处对此失利状况进行处理,例如弹出toast提示用户,或许界面上展现其他失利提示信息等。所以用户是否感知此状况,取决于sdk运用者。
痛点九 支撑断点续传吗?会重复下载吗?
Java代码中,运用DynamicPkgInfo类来描述资源,该类中包含了资源的版别号。咱们比较该类和本地数据库中的资源版别号,假如不同,才会下载资源。本sdk只供给了下载接口,未供给实际下载功用,因而如需这些功用,需求调用者自己完结。
痛点十 有文件完整性校验吗?
DynamicPkgInfo相同包含了zip包中一切子文件的校验信息,咱们利用它,来校验一切解压后的文件。
痛点十一 怎样防止64位设备下到32位So文件?
咱们把arm64-v8a,armeabi-v7a等abi分隔打包,上传到服务器。运用时,本地判别abi支撑,下载对应的abi包。这样做的优点是节约流量和下载后占有的空间。
至于判别体系需求哪些abi的so包,并按需正确运用,则比较简略,读取体系的SUPPORTED_ABIS常量,这儿包含了体系支撑的abi列表,而排在前面的表明优先级更高。咱们只需遍历它,然后查找咱们的动态资源包是否有匹配,就达到了正确加载的方针。
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的选定标准是什么?
动态加载So库其实是一些边缘功用的So库或许运用机遇比较晚的So库能够考虑动态加载;
痛点十三 计算上报功用,怎样计算并上报资源加载的成功率?
计算上报首要埋点信息由success、error code/message、so name、retry、demotion、storage size、download type、download time、设备信息、网络信息和用户信息这些功用,
为了躲避动态资源加载进程中,或许由于各种原因,导致加载未能得到成功或许失利的效果,而在中间状况被中止,如运用进程被杀死,手机关机等等。为了防止加载意外中止的状况下,彻底从头开始进行加载,咱们设计了一个动态资源加载的康复流程,假如反常中止,咱们下次加载资源时,能够康复到当时状况,继续进行加载。首要,下载进程的康复和断点续传,需求下载接口的完结者担任。然后,其他状况,咱们在状况改动时,将资源id,当时状况和待处理文件途径,保存到数据库。其次,每次加载动态开始时,依据资源id查找数据库中是否有待康复数据。接着,有待康复数据,转到待康复的状况,不然,直接去查看版别号状况。
最终,资源加载成功或许失利时,从数据库中删去当时资源id对应的康复状况。并供给回调给事务要求进行成功率埋点。
痛点十四 动态资源运用怎样加载到对应View上?
首要,依据资源id,从缓存中获取动态资源对应的本地文件。然后,文件获取成功,直接设置到view上,获取失利,进入下一步。其次,参数列表中的占位资源不为空,则将占位资源设置到View上。其四,将资源id设置到View的tag上,测验铲除上次动态资源加载失利状况。其五,运用办理器Manager类的load办法,履行之前的加载流程。接着,异步等候加载完结回调,判别资源id是否和View的tag相同,防止view被复用,导致的资源错乱状况。最终,假如Activity没有被毁掉,则将资源设置到View上。
痛点十五 怎样移除apk中的So文件,并将他们搜集起来?
在编译时期,主动删去并搜集so文件是最优解,那么在编译时期进行以上操作呢?小木箱注意到as在进行build时,会有很多的体系供给的task在运行,那么这些体系task是否就完结了编译并搜集各个地方的so文件,并把他们打包进apk的使命。
有2个体系task会用来处理兼并so库并且删去debug符号 。一般来说,应该在stripSymbols完毕后去除掉 stripped_native_libs 目录下的文件。但是除掉debug符号操作,或许导致不同as版别得到的so文件md5码不相同。
因而,咱们选用了可装备计划,能够由用户装备决议,在MergeNativeLibsTask或许stripDebugDebugSymbols后,履行删去输出文件夹中so文件操作。第三方 so 一般都是 Release 编译出来的,不进行strip影响也不大。而咱们自己的so文件,则strip操作或许会对so体积形成较大影响。下面咱们以在MergeNativeLibsTask之后,履行删去输出文件夹中so文件的办法
通过自界说gradle task并将它刺进到体系的merge和strip之间,利用该Task完结删去merged_native_libs目录下对应so文件,并将其复制到咱们指定的新目录下。这样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文件紧缩打包,并生成对应的信息?
首要,将so文件打包成.zip紧缩包。运用java.util.zip内置包完结即可,比较简略
然后,生成该资源对应的实体类DynamicPkgInfo。包含文件id,文件称号,文件类型,版别号,下载地址等基本信息,以及文件md5,文件长度等校验信息。以及紧缩包下的一切子文件及文件夹相关信息。利用了开源库javapoet完结的。
//创立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) {
}
最终,将该zip文件上传到服务器,以便利下载和运用。将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) {
}
至此,SO和资源动态化办理就悉数说完了
3.4 本地图片转网图
说完动态加载So库与资源,小木箱再说说本地图片转网图,咱们能够手动把本地图片上传到oss-browser进行预加载,然后删去本地图片,修正代码加载网络图片。
假如厌弃费事,能够用插桩的办法去完结,详细思路是编译时,批量上传图片,删去图片源文件并保存链接信息。然后在运行时,解析链接信息,Hook Android Drawable图片加载流程,自界说Drawable,触发网络图片下载,还原体系的Drawable图片制作流程。详细思路如下,腾讯的ImageBus(闭源)应该是最好的实践。
3.5 插件化技能预研
插件化,商业收益十分显着,基本上各个大厂都有做插件化,便利生成轻量级Android运用,通过插件化去加载非中心模块,我们能够看一下市面上常见的八种插件化东西比照图,再挑选更合适自己企业的插件化东西。
小木箱引荐我们运用Shadow插件化东西,由于Shadow首要具有五个特点,榜首、复用独立装置App的源码,插件App的源码本来便是能够正常装置运行的。第二、零反射无Hack完结插件技能,从理论上就现已确认无需对任何体系做兼容开发,更无任何躲藏API调用和Google约束非公开SDK接口拜访的战略彻底不冲突。第三、全动态插件结构,一次性完结完美的插件结构很难,但Shadow将这些完结悉数动态化起来,使插件结构的代码成为了插件的一部分。插件的迭代不再受宿主打包了旧版别插件结构所约束。第四、宿主增量极小,得益于全动态完结,真正合入宿主程序的代码量极小(15KB,160办法数左右)。第五、Kotlin完结,core.loader,core.transform中心代码彻底用Kotlin完结,代码简洁易维护,最重要的是Shadow通过腾讯线上亿级用户量检验,声称“零hook”。感兴趣能够听一下Shadow教程。
四、 总结与展望
回归到主题,做好包体优化能不能提升和加薪呢,问这个问题不如问提升和加薪的底层逻辑是什么?首要是看包体健康度是否归入当年年度规划方针。关于一个技能型互联网公司而言,在未来相当一段时间,包体健康度一定是长期有效的监控方针,所以对缩包有杰出贡献的主力开发,向上汇报是相对亮眼的,且不说提升和加薪,年度绩效不至于丑陋。
本次共享,小木箱首要是共享两部分内容,榜首部分内容是包体优化的进程,第二部分内容是包体优化面对的事务痛点。包体优化的进程首要分为七部分,榜首部分是优化方针,第二部分是优化排期,第三部分是优化记载,第四部分是阶段效果,第五部分是衡量方针,第六部分是CI/CD监控与预警,第七部分是采坑记载。而事务痛点首要分为五部分。榜首部分是CI/CD集成监控包体健康度,第二部分是So库紧缩与解压机制,第三部分是动态加载So库与资源,第四部分是本地图片转网图,第五部分是插件化技能预研。
包体优化的系列文章共享现已完毕,包巨细健康度查看和动态资源办理是实战论的重中之重。包巨细健康度查看,在构建打包阶段,通过合理的才能调整和布置、针对性的处理履行环节和完结常态化的痛点,能够完结质量操控才能的有效落实。涉及到比较有挑战性的技能栈如逆向解包、Docker、K8S、Githook等等。动态资源办理阶段,降级计划处理、CI/CD撤包和ASM插桩Hook等等有许许多多的坑,做一个高稳定和高可用的动态化SDK不仅作业量大,并且需求长期有耐心。项目复杂,需求设计合理的架构以支撑扩展,遇到疑难杂症,咱们要对问题坚持满足的决心。总结下来八个字 “胆大心细,小步快跑“。
我是小木箱,假如本文对你有启发,点赞和重视吧~
优质技能计划引荐
- 货拉拉 Android 动态资源办理体系原理与实践
- 我的 Android 重构之旅,动态下发 So 库(上)
- 动态下发 So 库在 Android APK 装置包减肥方面的运用
- 【保姆级】包体积优化教程
- SoLoader,android动态加载So库
- Android动态加载So!这一篇就够了! ReLinker
- SoLoader
- 阿里某淘Android体积优化计划
- 货拉拉 Android 包体积优化实践