5

货拉拉 Android 动态资源管理系统原理与实践(上)

 2 years ago
source link: https://blog.csdn.net/eclipsexys/article/details/125755158
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

货拉拉 Android 动态资源管理系统原理与实践(上)

5418b525b1f37af71bd23d1c208ab313.png

点击上方蓝字关注我,知识会给你力量

be22107de2852d85528f8997e3702e48.png

jary,货拉拉高级客户端工程师,目前负责货拉拉App Android端稳定性提升,包体积优化相关工作。

  • 随着公司业务的扩展,货拉拉用户端apk包的体积也不断变大,过去一年,用户端android组进行了大量的瘦身工作,取得了较为显著的成果。再使用常规方法,已经很难优化包体积了。

  • 我们可以把一些使用频率相对较低的资源不打包进apk,并在需要时下载到本地(例如动画文件,字体,zip压缩包,so库等)

  • 我们注意到,货拉拉用户端apk中,使用了35个以上的so库,并且都支持arm64-v8a和armeabi-v7a这2种abi,结果就是so体积成倍上涨。用户端生产环境下的apk,解压缩后,存放so包的lib目录,占据了整个应用41%的大小。

  • 因此动态资源管理系统是下一个优化的重点,动画,字体和zip包只是普通文件,完全可以支持动态下载并使用。而so文件本质上就是一种可动态加载并执行的文件,将 so文件动态下发是切实可行的,但是要将它从 apk中剔除并保证稳定性并不是一件易事。

  • 未找到现成的github项目或者三方sdk方案,来实现动态资源管理。

  • 部分博客提供了动态管理so文件的思路,但是缺少完整流程。

  • 行业目前并未提供完整的成熟方案供我们使用,需要我们自己造轮子。

  1. 功能和方案

  • 资源分类,预定义了字体,帧动画,so这3种内置资源,以及单个文件,多个文件这2种可自定义资源。

  • 提供通用的加载动态资源方法,所有资源均可由此加载。

  • 内置资源,提供默认的应用方法,外部可以直接应用。自定义资源,用户自行决定如何应用。

  • 对于所有资源,提供可配置的方便快捷打包方式,减少手动操作。

fe277ba25ac1d4b7ebc0b5e35dbfbf1f.png
  • 资源加载:将动态资源通过下载,校验,解压等方式,映射到本地文件的过程。

该过程对所有资源通用,sdk使用方无需修改资源加载方式。

  • 资源应用:动态资源对应的本地文件应用到具体业务中。例如动态字体资源的应用,就是为TextView设置一个新的字体。

该过程每个资源不同,sdk使用方无需修改内置资源的应用方式,对于自定义资源,需要使用方自行决定应用方式。

  • 资源打包:包括生成一个待上传的资源文件,以及生成资源的Java描述(DynamicPkgInfo类)。so资源还包含了一些方法的hook操作。

该过程对所有资源都适用,统一使用可配置的dynamic_plugin插件完成。sdk使用方无需修改资源打包方式,但是可通过配置dyanmic_plugin.gradle文件,配置打包过程。

  • 通用资源加载

  • 如何确定资源已经下载过了,避免重复下载?

Java代码中,使用DynamicPkgInfo类来描述资源,该类中包含了资源的版本号。我们比较该类和本地数据库中的资源版本号,如果不同,才会下载资源。

  • 下载资源是否提供多线程下载,断点续传等功能?

本sdk只提供了下载接口,未提供实际下载功能,因此如需这些功能,需要调用者自己实现。

  • 如何校验资源,防止被篡改?

DynamicPkgInfo类中包含了资源校验信息,我们利用该类,对下载好的文件进行md5码,文件长度,文件名称的校验。

  • 如何判断资源是否压缩包,以及如何解压缩?

目前简单的采用后缀名是否为.zip判断,使用使用Java内置java.util.zip包下工具解压。

  • 如何校验解压后的资源子文件,防止被篡改?

DynamicPkgInfo同样包含了zip包中所有子文件的校验信息,我们利用它,来校验所有解压后的文件。

  • 字体资源应用,从加载好的本地文件中,创建系统Typeface字体对象,并设置到TextView上。

  • 帧动画资源应用,从加载好的本地文件中,创建系统AnimationDrawable帧动画对象,并设置到ImageView上。

  • 字体和帧动画资源的应用流程,见第5章,内置资源应用流程。

  • so资源应用流程,见第7章,so资源加载和应用解决方案。

  • 自定义资源的应用,需要sdk使用者自己定义。

我们使用dynamic_plugin gradle插件来完成所有资源的打包。

  • 字体资源打包

  • 扫描输入目录字体文件,将他们拷贝到输出目录。

  • 为每个字体生成一个DynamicPkgInfo类的常量,代表该动态资源。

  • 帧动画资源打包

  • 扫描输入目录帧动画文件夹,将它们逐个压缩,并将压缩包输出到指定目录。

  • 为每一组帧动画生成一个DynamicPkgInfo类的常量,代表该动态资源。

  • so资源打包

  • Hook系统System.loadLibrary方法的调用。

  • 系统打包流程中,删除配置文件指定so文件,并将他们拷贝到指定目录。

  • 扫描上面的so文件目录,将他们逐个压缩,并将压缩包输出到指定目录。

  • 为每一个so压缩包生产一个DynamicPkgInfo类的常量,代表该动态资源。

  • 自定义资源打包

  • 单个文件的资源打包同字体资源

  • 多个文件的资源打包同帧动画资源

  • 下图为该打包插件运行一次之后的产物。

  • input目录,所有待打包资源的存放目录,我们需要手动把要打包的资源拷贝这里,例如字体文件拷贝到input/typeface目录下。注意so资源会在打包过程中,自动生成,无需手动处理。

  • output目录,则是打包出来的产物,包括字体资源,so资源,帧动画资源等,我们可以手动将此目录下的打包后资源上传到服务器。

  • DynamicResConst.java文件,该文件中生成了所有资源的信息。

4b0182d28ae75474201a28ce5639551c.png
  • DynamicResConst.java文件的内容,我们在这里也稍微看一下,图中为字体资源和帧动画资源的java描述。可以看到所有动态资源,都用DynamicPkgInfo类来描述。

  • 单个文件资源,包含了资源的id,文件名称,资源类型,下载地址,版本号,文件长度以及md5码。

  • 多个文件资源,除了包含上述信息外。还包含了该压缩包解压后,里面每个文件的名称,文件长度以及md5码

bd8943ac7d4b20c2a6436e03694a9bdb.png

由于整个系统功能较复杂,我们将其分为3个module。

  • lib_dynamic_base:只包含md5,压缩解压等通用操作以及代表资源的实体类DynamicPkgInfo,该module为后面2个module的基础。

  • lib_dynamic_res:提供了资源的加载和应用功能,目前包含字体资源,帧动画资源,so资源以及自定义资源。

  • dynamic_plugin:为一个gradle plugin工程,提供了资源打包功能。

96206430b4176813b633b3f5e8571acf.png

  • lib_dynamic_res模块架构

该库包括了动态资源加载和应用全过程,我们分为5层实现

  • 外部接口层,主要为加载管理器和加载监听器,提供了所有外部的接口。

  • 资源应用层,封装了几种内置动态资源的应用,字体资源,帧动画资源,so资源。

  • 加载流程层,具体完成了资源的加载过程,主要采用状态模式实现,包括一个状态管理器,以及各种状态,例如检查本地版本状态,下载状态,校验文件状态等。

  • 接口隔离层,主要是一些功能接口,例如下载功能,解压缩功能,上报功能等,隔离了底层实现。

  • 具体实现层,各个具体功能的实现,例如数据库操作,java zip库等。

4670fd20207fcf282a61c0fdd9f7d222.png
  • dynamic_plugin插件架构

  • 系统插件层,主要为系统gradle plugin的实现,以及对dynamic_plugin.gradle配置文件的读取和解析

  • 任务模块层,包含了各个任务,例如删除并拷贝so文件任务,压缩zip包任务等。

  • 底层实现层,包含了具体功能的实现,例如asm框架和transform api,zip压缩,javepoet代码生成等。

8e198fdb747d5babba0e05f1660ef10f.png
  1. 通用资源加载,内置资源应用流程

  • 通用资源加载主流程

加载普通资源的主流程如下,首先判断资源包指定版本号和本地数据库版本号是否相同,如果想同,进入本地资源校验流程,否则进入下载流程。

a4d071c3713417cfd28122a2bb9f1bc6.png
  • 下载校验解压流程

    • 我们首先调用下载接口下载资源。

    • 如果下载成功,我们校验下载文件,下载失败,则尝试删除文件,并直接跳到失败结果。

    • 校验下载文件成功,我们在判断是否为zip文件,对于zip文件,我们执行解压缩操作,非zip文件,直接成功。

    • 解压缩完成后,我们在对解压后的所有文件执行校验操作。

de2be2f77a70d6c84c4514263e383b40.png
  • 本地资源校验流程

    • 对于下载并解压的压缩包资源,以及本地数据库版本和资源实体类版本号相同的资源,我们需要进行本地资源校验流程。

    • 遍历资源包指定的字文件列表,对他们进行逐个文件检验就可以了。

65d45c0a339382c99c034478a52bf8cd.png
  • 单个文件校验流程

资源实体类中指定的文件名称,文件长度,文件md5码和本地文件相同时,我们认为该文件校验成功了

0cd89d701e9c419588ea6b6e63a02e24.png
  • 加载恢复流程

动态资源加载过程中,可能因为各种原因,导致加载未能得到成功或者失败的结果,而在中间状态被中断,如应用进程被杀死,手机关机等等。为了避免加载意外中断的情况下,完全从头开始进行加载,我们设计了一个动态资源加载的恢复流程,如果异常中断,我们下次加载资源时,可以恢复到当前状态,继续进行加载。

  • 下载过程的恢复和断点续传,需要下载接口的实现者负责。

  • 其他状态,我们在状态改变时,将资源id,当前状态和待处理文件路径,保存到数据库。

  • 每次加载动态开始时,根据资源id查找数据库中是否有待恢复数据。

  • 有待恢复数据,转到待恢复的状态,否则,直接去检查版本号状态。

  • 资源加载成功或者失败时,从数据库中删除当前资源id对应的恢复状态。

bb18046c1fbac908bec26f80ca400983.png
  • 内置资源应用流程

前面我们总结了动态资源的加载流程,资源加载完成后,我们还需要将该资源进行应用,而这里我们要说的就是将动态资源应用到对应View上的流程。

  • 根据资源id,从缓存中获取动态资源对应的本地文件。

  • 文件获取成功,直接设置到view上,获取失败,进入下一步。

  • 参数列表中的占位资源不为空,则将占位资源设置到View上。

  • 将资源id设置到View的tag上,尝试清除上次动态资源加载失败状态。

  • 使用管理器Manager类的load方法,执行之前的加载流程。

  • 异步等待加载完成回调,判断资源id是否和View的tag相同,防止view被复用,导致的资源错乱情况。

  • 如果Activity没有被销毁,则将资源设置到View上。

d12d7e931bacf5997ea3a4851eb3d311.png
  1. lib_dynamic_res模块类设计

可与第4章,整体架构分层图对照着看

  • 外部接口层

DynamicResManager类负责和外部交互,提供了初始化(init),加载资源(load),isResReady(判断资源是否就绪),clearFailState(清除错误状态等方法)等方法。

Config类,则可以向管理器提供线程池,下载器接口,本地资源信息接口,本地资源状态接口等配置信息。

AbsResInfo抽象类,代表动态资源。

DynamicPkgInfo类,AbsResInfo的子类,提供给外部使用,代表了一个动态资源实体。

DynamicPkgInfo.FileInfo,AbsResInfo的子类,资源实体内部类,代表了资源中的一个子文件。

DynamicPkgInfo.FolderInfo,AbsResInfo的子类,资源实体内部类,代表了资源中的一个子文件夹。

ILoadResListener接口,提供了加载资源时的回调功能,会回调加载成功,失败,状态变化,下载中进度

  • 资源应用层

AbsResApply抽象类,实现了动态资源在ui元素上的应用。

TypefaceResApply类,AbsResApply的子类,代表了字体资源的应用。

FrameAnimApply类,AbsResApply的子类,代表了帧动画资源的应用。

AbsSoLoad抽象类,实现了so动态资源的应用。

RelinkerSoLoad类,AbsSoLoad的子类,使用Relinker第三方库最终load so库。

SystemSoLoad类,AbsSoLoad的子类,使用系统System.loadLibrary方法最终load so库

  • 加载流程层

我们使用状态模式来控制整个动态资源的加载流程。

IState,状态接口,代表了加载流程中的一个状态。

InitState类,初始化状态。

CheckVersionState类,检查资源实体类版本号与数据库版本号是否相同状态。

DownloadState类,下载资源状态。

VerrifyFileState,校验下载资源状态。

UnZipState,解压缩下载资源状态。

VerifyZipState,校验解压后的所有文件状态。

IStateMechine,状态管理机接口,负责管理前面所有的IState对象。

DefaultStateMachine类,状态管理机的默认实现。

ResCtx类,状态管理机运行过程中的全局context对象,存储了路径信息,加载成功信息,加载失败异常等全局信息。

  • 接口隔离和具体实现层

这2层的类,较为杂乱,限于篇幅,我们就不一一列举了。

  • 类uml图

0804d1d87dc8845dff88cd59b7072166.png

……未完待续……

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问

本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

< END >

作者:徐宜生

更文不易,点个“三连”支持一下👇


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK