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

Android群英传

共 5389字,需浏览 11分钟

 · 2022-07-13

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


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

  1. 前言

  • 随着公司业务的扩展,货拉拉用户端apk包的体积也不断变大,过去一年,用户端android组进行了大量的瘦身工作,取得了较为显著的成果。再使用常规方法,已经很难优化包体积了。
  • 我们可以把一些使用频率相对较低的资源不打包进apk,并在需要时下载到本地(例如动画文件,字体,zip压缩包,so库等)
  • 我们注意到,货拉拉用户端apk中,使用了35个以上的so库,并且都支持arm64-v8a和armeabi-v7a这2种abi,结果就是so体积成倍上涨。用户端生产环境下的apk,解压缩后,存放so包的lib目录,占据了整个应用41%的大小。
  • 因此动态资源管理系统是下一个优化的重点,动画,字体和zip包只是普通文件,完全可以支持动态下载并使用。而so文件本质上就是一种可动态加载并执行的文件,将 so文件动态下发是切实可行的,但是要将它从 apk中剔除并保证稳定性并不是一件易事。
  1. 行业方案

  • 未找到现成的github项目或者三方sdk方案,来实现动态资源管理。
  • 部分博客提供了动态管理so文件的思路,但是缺少完整流程。
  • 行业目前并未提供完整的成熟方案供我们使用,需要我们自己造轮子。
  1. 功能和方案

  • 实现功能

  • 资源分类,预定义了字体,帧动画,so这3种内置资源,以及单个文件,多个文件这2种可自定义资源。
  • 提供通用的加载动态资源方法,所有资源均可由此加载。
  • 内置资源,提供默认的应用方法,外部可以直接应用。自定义资源,用户自行决定如何应用。
  • 对于所有资源,提供可配置的方便快捷打包方式,减少手动操作。
  • 几个概念

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

该过程对所有资源通用,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文件,该文件中生成了所有资源的信息。
  • DynamicResConst.java文件的内容,我们在这里也稍微看一下,图中为字体资源和帧动画资源的java描述。可以看到所有动态资源,都用DynamicPkgInfo类来描述。
  • 单个文件资源,包含了资源的id,文件名称,资源类型,下载地址,版本号,文件长度以及md5码。
  • 多个文件资源,除了包含上述信息外。还包含了该压缩包解压后,里面每个文件的名称,文件长度以及md5码
  1. 整体架构

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

  • lib_dynamic_base:只包含md5,压缩解压等通用操作以及代表资源的实体类DynamicPkgInfo,该module为后面2个module的基础。
  • lib_dynamic_res:提供了资源的加载和应用功能,目前包含字体资源,帧动画资源,so资源以及自定义资源。
  • dynamic_plugin:为一个gradle plugin工程,提供了资源打包功能。
  • lib_dynamic_res模块架构

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

  • 外部接口层,主要为加载管理器和加载监听器,提供了所有外部的接口。
  • 资源应用层,封装了几种内置动态资源的应用,字体资源,帧动画资源,so资源。
  • 加载流程层,具体完成了资源的加载过程,主要采用状态模式实现,包括一个状态管理器,以及各种状态,例如检查本地版本状态,下载状态,校验文件状态等。
  • 接口隔离层,主要是一些功能接口,例如下载功能,解压缩功能,上报功能等,隔离了底层实现。
  • 具体实现层,各个具体功能的实现,例如数据库操作,java zip库等。
  • dynamic_plugin插件架构

  • 系统插件层,主要为系统gradle plugin的实现,以及对dynamic_plugin.gradle配置文件的读取和解析
  • 任务模块层,包含了各个任务,例如删除并拷贝so文件任务,压缩zip包任务等。
  • 底层实现层,包含了具体功能的实现,例如asm框架和transform api,zip压缩,javepoet代码生成等。
  1. 通用资源加载,内置资源应用流程

  • 通用资源加载主流程

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

  • 下载校验解压流程

    • 我们首先调用下载接口下载资源。
    • 如果下载成功,我们校验下载文件,下载失败,则尝试删除文件,并直接跳到失败结果。
    • 校验下载文件成功,我们在判断是否为zip文件,对于zip文件,我们执行解压缩操作,非zip文件,直接成功。
    • 解压缩完成后,我们在对解压后的所有文件执行校验操作。
  • 本地资源校验流程

    • 对于下载并解压的压缩包资源,以及本地数据库版本和资源实体类版本号相同的资源,我们需要进行本地资源校验流程。
    • 遍历资源包指定的字文件列表,对他们进行逐个文件检验就可以了。
  • 单个文件校验流程

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

  • 加载恢复流程

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

  • 下载过程的恢复和断点续传,需要下载接口的实现者负责。
  • 其他状态,我们在状态改变时,将资源id,当前状态和待处理文件路径,保存到数据库。
  • 每次加载动态开始时,根据资源id查找数据库中是否有待恢复数据。
  • 有待恢复数据,转到待恢复的状态,否则,直接去检查版本号状态。
  • 资源加载成功或者失败时,从数据库中删除当前资源id对应的恢复状态。
  • 内置资源应用流程

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

  • 根据资源id,从缓存中获取动态资源对应的本地文件。
  • 文件获取成功,直接设置到view上,获取失败,进入下一步。
  • 参数列表中的占位资源不为空,则将占位资源设置到View上。
  • 将资源id设置到View的tag上,尝试清除上次动态资源加载失败状态。
  • 使用管理器Manager类的load方法,执行之前的加载流程。
  • 异步等待加载完成回调,判断资源id是否和View的tag相同,防止view被复用,导致的资源错乱情况。
  • 如果Activity没有被销毁,则将资源设置到View上。
  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图

……未完待续……


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

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



往期推荐


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

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


浏览 34
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报