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


    eac9791250f69ece864e1e4611de25db.png

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

    3bff1d5e17e52978bfc154ed66cebc38.png

    1. so资源动态化方案

    • so资源打包问题

    在打包so资源的过程中,我们遇到了如下问题。

    • 如何移除apk中的so文件,并将他们收集起来?

    • 如何将多个so文件压缩打包,并生成对应的信息?

    • 如何保证第三方sdk缺少so文件时,不崩溃?

    • so资源打包解决方案

    • 移除并收集apk中的so文件

    看到移除 so文件可能有些同学会问,这不是只要在as中删除libs目录就搞定了么?这样会有几个问题

    • 对于多个module的工程,我们需要逐个删除每个module下的libs目录,麻烦而且容易出错。

    • 对于三方aar包中的so文件,我们就没法删除了。

    • so文件变化需要人工维护,容易出错。

    出于以上考虑,我们认为,在编译时期,自动删除并收集so文件是最优解,那么在编译时期进行以上操作呢?我们注意到as在进行build时,会有大量的系统提供的task在运行,那么这些系统task是否就完成了编译并收集各个地方的so文件,并把他们打包进apk的任务呢?

    b59529d2c53cf5950e5b456c44255c5a.png

    看一眼这幅超级复杂的apk构建流程图,嗯,可以看到,系统确实会在apkBuilder构建前,将本地的c/c++文件编译成so库,并将第三方的so库一起打包到apk中,我们需要寻找的就是收集所有so库的系统Task

    24aa842ec8ce2dc4a1e94a429b771bbf.png

    通过查找资料,我们发现,确实有2个系统task会用来处理合并so库并且删除debug符号(注意,task名称可能与此处不完全相同)。

    Task名称实现类作用结果保存目录
    mergeDebugNativeLibsMergeNativeLibsTask合并所有依赖的 native 库intermediates/merged_native_libs
    stripDebugDebugSymbolsStripDebugSymbolsTask从 Native 库中移除 Debug 符号intermediates/stripped_native_libs
    • 一般来说,应该在stripSymbols结束后去剔除 stripped_native_libs 目录下的文件。

    • 但是剔除debug符号操作,可能导致不同as版本得到的so文件md5码不相同。

    • 因此,我们采用了可配置方案,可以由用户配置决定,在MergeNativeLibsTask或者stripDebugDebugSymbols后,执行删除输出文件夹中so文件操作。

    • 第三方 so 一般都是 Release 编译出来的,不进行strip影响也不大。而我们自己的so文件,则strip操作可能会对so体积造成较大影响。

    • 下面我们以在MergeNativeLibsTask之后,执行删除输出文件夹中so文件的方式,进行讲解。

    由于我们有多个gradle task需要执行,因此我们创建了一个名为dynamic_plugin的android plugin工程,内部包含了多个gradle task。关于as中新建插件的方法,请自行搜索其他博客,本文因为篇幅问题,不进行讲解。

    在我们的dynamic_plugin插件内部,我们新建一个名为DeleteAndCopySo的gradle task并将它插入到系统的merge和strip之间,利用该Task完成删除merged_native_libs目录下对应so文件,并将其拷贝到我们指定的新目录下。这样apk打包时,就不会包含动态化的so文件了

    1. //获取系统的mergeTask
    2. Task mergeNativeTask = TaskUtil.getMergeNativeTask(project);
    3. //获取系统的skipTask
    4. Task stripTask = TaskUtil.getStripSymbol(project);
    5. //创建我们的DeleteAndCopySo task
    6. Task deleteTask = project.getTasks().create(PluginConst.Task.DELETE_SO);
    7. deleteTask.doLast(new Action() {
    8.     @Override
    9.     public void execute(Task task) {
    10.         deleteAndCopySo(project, param);
    11.     }
    12. });
    13. //将我们的Task插入到merge和strip之间
    14. stripTask.dependsOn(deleteTask);
    15. deleteTask.dependsOn(mergeNativeTask);
    • 如何将多个so文件压缩打包,并生成对应的信息?

    上一步中,我们已经将so文件从系统apk构建流程中删除,并且拷贝到了指定目录下。那么现在我们应该做什么呢?

    • 将so文件打包成.zip压缩包。

    • 生成该资源对应的实体类DynamicPkgInfo。包括文件id,文件名称,文件类型,版本号,下载地址等基本信息,以及文件md5,文件长度等校验信息。以及压缩包下的所有子文件及文件夹相关信息。

    • 将该zip文件上传到服务器,以方便下载和使用。

    对于上述这些步骤,在我们的货拉拉动态管理系统初始版本中,我们采用了自己打zip包,自己写java代码来生成资源信息的方式。

    但是在后来的使用过程中,我们发现,手动进行这些步骤,很繁琐且容易出错,我们需要有一种自动化的方式进行上述过程。

    我们在dynamic_plugin插件内部,再新增一个ZipSoTask来进行压缩so文件夹,以及生成资源信息常量的操作。该task在DeleteAndCopySo之后,stripe系统task之前执行。

    1. //执行将so文件夹压缩成.zip操作
    2. List zips = DynamicUtil.zipFolder(new File(param.getmInputSo()),outDir);
    3. //根据so文件和zip压缩包信息,生成md5,length等校验信息并存储
    4. DynamicUtil.createPkgDatas(mPkgList,zips,PluginConst.Type.SO);
    5. //根据资源信息类生产java文件
    6. param.getmFileCreate().createFile(mPkgList,param);
    • 前2步,压缩so文件,和根据so文件,zip文件生成校验信息并存储比较简单,就不详细说了。

    • 第3步,根据前面的信息,直接生产java文件,我们使用了第三方的开源库javapoet。

    • JavaPoet 是 Square 公司推出的开源 Java代码生成框架,提供接口生成 Java 源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。

    • 大致的生产代码如下,首先生成一个DynamicResConst类,之后遍历zip压缩资源列表,为列表中的每一个资源,生成一个static final的常量,表示每个资源,最后生成java文件。

    1. //创建DynamicResConst类,用来存储资源实体常量
    2. TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( "DynamicResConst" )
    3.         .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
    4. //遍历资源列表,生成对应实体类DynamicPkgInfo        
    5. for (DynamicPkgInfo pkg : pkgs) {
    6.     FieldSpec fsc = createField(pkg);
    7.     typeBuilder.addField(fsc);
    8. }
    9. //插件java文件,并写入
    10. JavaFile javaFile = JavaFile.builder(param.getmCreateJavaPkgName(), typeBuilder.build()).build();
    11. try {
    12.     javaFile.writeTo(new File(param.getmOutputPath()));
    13. } catch (Exception e) {
    14.     
    15. }

    至于最后一步,将so压缩包上传到服务器,我们在配置文件中提供了一个上传方法,不过默认实现为空,用户可以手动上传也可以修改默认方法实现自动上传。自动生成的资源文件中,版本号需要手动修改控制,下载地址手动上传的话,也需要手动修改。

    • 保证第三方sdk在缺少so文件时,不崩溃

    很多三方sdk都要求在应用启动时,进行初始化,一个使用so库的类的典型类代码如下:

    1. public class ThirdLib{
    2. //静态方法加载so库
    3. static{
    4.     System.loadLibrary("third");
    5. }
    6. //native方法示例
    7. public native void testNative();
    8. //java方法示例
    9. public void test();
    10. //......其他内容省略
    11. }
    • 如果此时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文件。

    2f489965bb88a8b77312d8d2c834f654.jpeg

    具体执行替换的代码如下,在Asm框架中的MethodVisitor类中,重写visitMethodInsn方法,判断该方法的拥有者,名称和参数列表和System.loadLibrary对应,则我们将他替换为我们的SoLoadUtil.loadLibrary方法

    1. @Override
    2. public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
    3.     if(TextUtil.equals(owner, PluginConst.SYSTEM_CLASS) &&
    4.             TextUtil.equals(name, PluginConst.LOAD_LIBRARY_METHOD) &&
    5.             TextUtil.equals(descriptor, PluginConst.LOAD_LIBRARY_DESC)){
    6.         owner = "com/xxx/xxx/dynamicres/util/SoLoadUtil" ;
    7.         mv.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, false);
    8.         return;
    9.     }
    10.     mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    11. }

    替换后的方法主要逻辑为,使用第三方库Relinker替代System.loadLibrary方法进行so文件加载,并且catch住加载异常,来防止应用直接奔溃,并且在加载so库异常时,将该库的名称保存下来,在我们的so包被正常下发加载后,再次调用本方法,将so库load到系统中。

    1. protected void realSoLoad(Context c, String libName) {
    2.  try {
    3.  ReLinker. recursively ().loadLibrary(c, libName);
    4.  removeFormWaitList(libName);
    5.  } catch (Throwable t) {
    6.  addToWaitList(libName);
    7.  }
    8.  }
    • 对于so库没有加载完成,直接使用ThirdLib类导致System.loadLibrary方法被调用,导致的应用崩溃问题,我们已经解决了。

    • 而对于直接调用ThirdLib类的testNative方法,导致的应用崩溃问题,则无法解决。因此需要看情况决定是否能够接受该种崩溃,以及是否将引发该问题的so库进行动态化。

    我们只需要在工程的主Application中,直接调用loadSo方法,对so动态资源进行加载。加载完成后,so库就能正常使用了。

    1. public void loadSo(DynamicSoInfo soInfo, ILoadSoListener listener) {
    2.     if (soInfo == null) {
    3.         return;
    4.     }
    5.     //根据本机abi,获取适合的动态资源实体类DynamicPkgInfo
    6.     DynamicPkgInfo pkg = soInfo.getPkgInfo(Build.SUPPORTED_ABIS);
    7.     if (pkg == null) {
    8.         return;
    9.     }
    10.     //如果该so资源,已经被加载缓存过了,直接listener的成功回调,并返回
    11.     if (isLoadAndDispatchSo(pkg, listener)) {
    12.         return;
    13.     }
    14.     //开启资源加载,和普通资源流程一致
    15.     DynamicResManager manager = DynamicResManager.getInstance();
    16.     manager.load(pkg, new DefaultLoadResListener() {
    17.         @Override
    18.         public void onSucceed(LoadResInfo info) {
    19.             super.onSucceed(info);
    20.             //so成功下载校验后,执行加载逻辑
    21.             handleLoadSoSucceed(pkg, info, listener);
    22.         }
    23.     });
    24. }
    • so资源加载和应用问题

    在so资源的加载和应用过程中,我们发现了如下问题

    • 如何判断系统需要哪些so文件,并按需正确加载?

    • 如何下载so文件,并保证它的正确性?

    • 如何将下载的动态so文件,正确应用到系统中?

    • so资源加载和应用解决方案

    • 如何判断系统需要哪些so文件,并正确下载安装?

    我们把arm64-v8a,armeabi-v7a等abi分开打包,上传到服务器。使用时,本地判断abi支持,下载对应的abi包。这样做的优点是节省流量和下载后占据的空间。

    至于判断系统需要哪些abi的so包,并按需正确应用,则比较简单,读取系统的SUPPORTED_ABIS常量,这里包含了系统支持的abi列表,而排在前面的表示优先级更高。我们只要遍历它,然后查找我们的动态资源包是否有匹配,就达到了正确加载的目标。

    1. private Map mSoInfos;
    2. public DynamicPkgInfo getPkgInfo(){
    3.     //获取本地系统支持的abi列表
    4.     String[] supportAbis = Build.SUPPORTED_ABIS;
    5.     if(supportAbis==null || supportAbis.length== 0 ){
    6.         return null;
    7.     }
    8.     //遍历abi支持列表
    9.     for(String abi : supportAbis){
    10.         //从so动态资源中,查找对应的abi信息
    11.         DynamicPkgInfo pkg = mSoInfos.get(abi);
    12.         //找到则直接返回该信息
    13.         if(pkg != null){
    14.             return pkg;
    15.         }
    16.     }
    17.     return null;
    18. }
    • 如何下载so文件,并保证它的正确性?

    复用通过资源加载流程即可。

    • 如何将下载的动态so文件,正确应用到系统中?

    这里需要首先了解一下,系统加载so库的工作流程,当我们调用 System#loadLibrary("xxx" ) 后,Android Framework 都干了些了啥?Android 的 so 加载机制,大致可以分为以下四个环节。

    • 安装 APK 包的时候,PMS 根据当前设备的 abi 信息,从 APK 包里拷贝相应的 so 文件。

    • 启动 APP 的时候, Android Framework 创建应用的 ClassLoader 实例,并将当前应用相关的所有 so 文件所在目录注入到当前 ClassLoader 相关字段。

    • 调用 System.loadLibrary("xxx"), framework 从当前上下文 ClassLoader 实例(或者用户指定)的目录数组里查找并加载名为 libxxx.so 的文件。

    • 调用 so 相关 JNI 方法。

    而我们这里,由于so文件不存在于apk当中,而是需要动态下载,所以我们显然不能直接使用系统的System.loadLibrary方法加载so文件。

    而动态加载so的方法,在热修复和插件化框架中,已经比较成熟了,我们参考了市面上的开源框架后,选择了腾讯的Tinker框架的加载方案,即使用反射classloader 将 so 包的路径写入 nativeLibraryPathElements 数组最前面,其流程图和解释如下图所示 。注意,此方法不同的android版本将有不同的实现。下面示例代码基于android9.0版本。

    1. private static void install(ClassLoader classLoader, File soFolder) throws Throwable {
    2.  Field pathListField = findField(classLoader, "pathList" );
    3.             Object dexPathList = pathListField.get(classLoader);
    4.             Field nativeLibraryDirectories = findField(dexPathList, "nativeLibraryDirectories" );
    5.             List libDirs = (List) nativeLibraryDirectories.get(dexPathList);
    6.             libDirs.add(0, soFolder);
    7.             Field systemNativeLibraryDirectories =
    8.                     findField(dexPathList, "systemNativeLibraryDirectories" );
    9.             List systemLibDirs = (List) systemNativeLibraryDirectories.get(dexPathList);
    10.             Method makePathElements =
    11.                     findMethod(dexPathList, "makePathElements" , List.class);
    12.             libDirs.addAll(systemLibDirs);
    13.             Object[] elements = (Object[]) makePathElements.
    14.                     invoke(dexPathList, libDirs);
    15.             Field nativeLibraryPathElements = findField(dexPathList, "nativeLibraryPathElements" );
    16.             nativeLibraryPathElements.setAccessible(true);
    17.             nativeLibraryPathElements.set(dexPathList, elements);
    18.         }
    • pathList变量:DexPathList类的实例。

    • nativeLibraryDirectories列表:包含了本App自带so文件的查找路径(如data/app/包名/lib/arm64)

    • systemNativeLibraryDirectories列表:包含系统so文件查找路径(如system/lib64)

    • makePathElements:系统使用此方法,为所有so文件,生成对应的 NativeLibraryElement对象

    • nativeLibraryPathElements数组:系统用来存储所有的so文件路径

    当外界调用System.loadLibrary方法时,系统最终会调用到DexPathList类的findLibrary方法,该方法会在nativeLibraryPathElements数组中查找对应的路径,我们将自己的so加入到nativeLibraryPathElements最前面,由此达到动态加入so的目标。

    1. so资源动态化的tips

    • 为何要使用Relinker加载So文件

      • 假如我们有2个so文件,libA.so 和 libB.so,libA依赖libB,则当我们调用System.loadLibrary("libA") 的时候,android framework 会通过上面提到的调用链最终通过 dlopen 加载 libA.so 文件,并接着通过其依赖信息,自动使用 dlopen 加载 libB.so。

      • 在 Android N 以前,只要将 libA.so 和 libB.so 所在的文件目录路径都注入到当前 ClassLoader 的 nativeLibraryPathElements 里,则在加载 so 插件的时候,这两个文件都能正常被找到。

      • 从 N 开始,libA.so 能正常加载,而 libB.so 会出现加载失败错误。

      • 因为Android Native 用来链接 so 库的 Linker.cpp dlopen 函数 的具体实现变化比较大(主要是引入了 Namespace 机制):以往的实现里,Linker 会在 ClassLoder 实例的 nativeLibraryPathElements 里的所有路径查找相应的 so 文件。

      • 更新之后,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libA.so 文件能找到,而 libB.so 找不到的情况。

      • 至于 Namespace 机制的工作原理了,可以简单认为是一个以 ClassLoader 实例 HashCode 为 Key 的 Map,Native 层通过 ClassLoader 实例获取 Map 里存放的 Value(也就是 so 文件路径集合)。

    解决该问题有如下几种思路:

    • 自定义 System.loadLibrary,加载 SO 前,先解析 SO 的依赖信息,再递归加载其依赖的 SO 文件,这是开源库soLoader的解决方案。

    • 自定义 Linker,完全自己控制 SO 文件的检索逻辑 ,这是开源库Relinder的解决方案。

    • 替换 ClassLoader 。

    本着不重复造轮子的原则,项目中使用了Relinker开源库,用来加载so文件。

    • so库依赖分析工具

    想要把 so 动态化技术应用到 APK 的瘦身项目中来,除了分析哪些 so 文件体积占比比较大之外,最好的做法是将其依赖的所有 so 文件一定挪到插件包里。怎么了解 APK 里所有 so 文件具体的依赖信息呢?这里推荐一款 Google 开源的 APK 解析工具android-classyshark,除了提供分析 APK dex/so 依赖信息之外,它还提供了 GUI 可视化界面,非常适合快速上手。

    e38237d2589bcc83e9a73859d2f60a27.png
    1. so动态化流程

    • so资源应用流程

    • 获取系统支持abi列表,根据该列表,找到合适的so动态资源实体类。

    • 如果该资源已经被加载缓存,则直接回调加载成功。

    • 否则,开始资源通用加载流程,并异步等待资源加载成功(流程见第5章)。

    • 再次判断下载校验后的资源,是否支持本机abi。

    • 将so包路径加入DexPathList的数组头部。

    • 遍历等待加载so列表,尝试加载所有so文件,并将成功加载的so文件,移除该列表。

    • 将资源id和本地路径加入缓存,防止so被重复加载。

    • 回调加载完成监听器。

    b9061e52aff169ff7156d53f3b7462cf.png
    • SoLoadUtil.loadLibrary方法流程

    从上一章我们知道,我们会使用transform api加asm框架,将系统的System.loadLibrary方法替换成我们的SoloadUtil.loadLibrary方法。我们替换系统方法的目的。一个是为了保证so库不存在时,程序不崩溃,另外一个就是so库下载校验完成后,能自动完成之前失败的加载,为此,我们设计了如下流程。

    • 其他方法调用到我们的SoloadUtil时,我们判断我们的加载系统是否初始化完成

    • 已完成,则调用Relinkder库尝试加载so文件,未完成则将该so库加入待加载队列中。

    • 如果Relinker加载so文件成功,我们从待加载队列中移除so,并且完成本次加载。

    • 否则我们依然将so文件加入待加载队列中。

    • 根据上面的so加载流程,当so动态资源真正下载校验完成后,我们会遍历待加载队列,并完成所有之前未成功的so库加载。

    89ef7a3dd4260aeb49595a78df81d984.png
    1. dynamic_plugin插件流程

    • 整体流程

    前面我们已经分析了通用资源加载,内置资源应用,完成了动态资源管理系统的主要部分。只剩下资源打包部分了,而所有资源的打包操作,都由dyanmic_plugin插件来完成。为了完成打包功能,我们决定在这个dynamic_plugin插件内部,新建3个Task。

    • Hook System.loadLibrary方法的TransformTask。

    • 系统打包流程中,删除并拷贝so文件的DeleteAndCopySoTask。

    • 压缩so资源和其他多个文件资源(例如帧动画)的ZipResTask。

    • 为每个动态资源生成其对应的DynamicPkgInfo常量的功能,仅实现为一个普通方法。

    所以主流程也就出来了

    • 读取并解析dynamic_plugin.gradle配置文件。

    • 根据配置信息,决定是否将3个task加入任务队列。

    • 启动任务队列。

    fa2582ea6fc44cd7d8a98db604a8558b.png
    • TransformTask流程

    该task流程,主要就是通过tranform api和asm框架的使用,我们在其中加入了扫描class范围的可配置项。

    • 等待asm框架扫描class。

    • 判断该class名称是否在我们配置的替换列表中,如果不在,就直接返回。

    • 创建ClassVisitor和MethodVisitor,等待asm框架扫描每个方法。

    • 如果该方法的名称,参数列表和调用者,都和System.loadLibrary方法相符合。

    • 我们替换为自己的SoloadUtil.loadLibrary方法。

    b65473608cef339a7ba956c7ee9e2e5d.png
    • DeleteAndCopySoTask流程

    • 根据配置文件,找到系统的merge和strip task。

    • 将我们的task插入到2个系统task之间,并等待系统回调我们的doLast方法。

    • 遍历系统的mergeTask的输出目录,判断该so文件是否在我们配置的待扫描列表中。

    • 如果配置了需要拷贝so文件,则我们将它拷贝到指定位置。

    • 如果配置了需要删除so文件,则我们将该so文件删除。

    296f7ac1f06e4253181e53ed798dc865.png
    • ZipResTask流程

    • 拷贝字体文件,将文件信息加入资源列表。

    • 压缩帧动画文件,将压缩后的文件信息加入资源列表。

    • 压缩so文件,将压缩后的文件信息加入资源列表。

    • 压缩zip文件夹下文件,将压缩后的文件信息加入资源列表。

    • 遍历资源文件,为其生成相应的资源实体类DynamicPkgInfo。

    56003b92790410ac053274d322826cb1.png
    1. dynamic_plugin插件类设计

    可以与第4章,整体架构图结合起来看。

    • 系统插件层

    DynamicPlugin类,实现了系统gradle插件的plugin接口,为我们整个插件的入口,主要解析配置文件,并按照配置文件创建task信息。

    DynamicParam类,提供了存储并解析dyanmic_plugin配置文件的方法。

    • 任务模块层

    ITask接口,代表了一个我们定义的任务。

    DeleteAndCopySoTask,删除并拷贝so文件任务。

    TransfomrTask,替换系统System.loadLibrary方法任务。

    ZipResTask,压缩so和其他文件,并生成对应的java资源实体类方法。

    • 底层实现层

    SystemLoadClassVisitor类,Asm框架的class访问类。

    SystemLoadMethodVisitor类,Asm框架的method访问类,用于替换System.loadLibrary方法。

    JavaFileCreate类,使用javapoet框架产生java文件。

    其他辅助类,在此省略

    • 类uml图

    f1b1eae1c50a454ba22ed6c4e2390a33.png
    1. dynamic_config.gradle配置文件

    该配置文件主要包含了配置dynamic_plugin插件运行步骤,插件输入输出路径,so文件扫描路径等信息。

    1. dynamic_config = [
    2.         //是否执行替换System.loadlibrary操作
    3.   is_replace_load_library: false,
    4.         //是否执行替换System.load操作
    5.   is_replace_load : false,
    6.         //是否执行删除so文件操作
    7.   is_delete_so : false,
    8.         //是否执行将so文件拷贝到其他目录操作
    9.   is_copy_so : false,
    10.         //是否执行将动态资源打包,并生成java文件操作
    11.   is_zip_res : false,
    12.         //是否执行将so文件打包,并生成java文件操作
    13.   is_zip_so : false,
    14.         //是否自动上传所有资源,上传方法为dynamic_upload
    15.   is_upload_res : false,
    16.         //插件是否工作在Release模式下
    17.   is_release_type : isReleaseBuildType(),
    18.         //是否打印debug日志
    19.   is_debug_log : true,
    20.         //自动创建java文件时的包名
    21.   create_java_pkg_name : 'com.test' ,
    22. ]
    23.  /**
    24.  * 配置要删除和拷贝的so文件
    25.  * map的key为压缩包名称,值为压缩包包含的so文件列表
    26.  * key为debug_all_test时,会压缩所有so包
    27.  */
    28. dynamic_scan_so_map = [
    29.         guang_dong : [ 'libpajf.so' , 'libpajf_av.so' , 'libsqlite.so' ],
    30. ]
    31. dynamic_so_config = [
    32.         //so文件忽略列表,该表中的文件,不会被扫描。不在该列表中的文件都会被扫描
    33.  // (dynamic_scan_so_map为空时,本列表才生效)
    34.   ignore_so_files: [],
    35.         //so文件扫描abi目录,不在该目录下的so将不被扫描
    36.   scan_so_abis : [ "arm64-v8a" , "armeabi-v7a" ],
    37.         //拷贝出来的so文件夹前缀,ignore_so_files生效时使用
    38.   so_input_prefix: 'test' ,
    39. ]
    40. dynamic_lib_list = [
    41.         //只有该列表中的包名,才会执行替换System.loadlibrary操作
    42.  //输入debug_all_test,则会替换所有System.loadLibrary方法,用于测试
    43.   scan_load_library_pkgs : [],
    44.         //在该列表中的包名或者类名,不会执行替换System.loadlibrary操作,和上面的配置可以同时生效
    45.   ignore_load_library_pkgs: [],
    46. ]
    47.  //该配置不要改动内容,需要改变路径的,直接改变对应的方法内容即可
    48. dynamic_dir = [
    49.         //产生文件的输出目录
    50.   output : createOrGetOutputPath(),
    51.         //字体资源输入目录
    52.   typeface_input : createOrGetInputTypafacePath(),
    53.         //帧动画资源输入目录
    54.   frame_anim_input: createOrGetInputFrameAnimPath(),
    55.         //so文件资源输入目录
    56.   so_input : createOrGetInputSoPath(),
    57.         //zip包输入目录
    58.   zip_input : createOrGetInputZipPath()
    59. ]
    60.  //该配置项,配置了android 2个gradle task的名称
    61.  //主工程的mergeNativeLibs合并所有依赖的 native 库
    62.  //主工程的stripDebugSymbols从 Native 库中移除 Debug 符号。
    63. dynamic_task = [
    64.         //自定义的task运行哪里
    65.  //true为mergeNativeLibs之后,stripDebugSymbols之前
    66.  //false为stripDebugSymbols之后,package之前
    67.   isTaskRunAfterMerge : true,
    68.         //debug状态下,mergeNativeLibs的task名称
    69.   debugMergeNativeLibs : "mergeDebugNativeLibs" ,
    70.         //release状态下,mergeNativeLibs的task名称
    71.   releaseMergeNativeLibs : "mergeReleaseNativeLibs" ,
    72.         //debug状态下,stripDebugSymbols的task名称
    73.   debugStripDebugSymbols : "stripDebugDebugSymbols" ,
    74.         //release状态下,stripDebugSymbols的task名称
    75.   releaseStripDebugSymbols: "stripReleaseDebugSymbols" ,
    76.         //debug状态下,系统打包task名称
    77.   debugPackage : "packageDebug" ,
    78.         //release状态下,系统打包task名称
    79.   releasePackage : "packageRelease" ,
    80.         //debug状态下,mergeNativeLibs的输出目录
    81.   debugNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/debug/out/lib" ,
    82.         //release状态下,mergeNativeLibs的输出目录
    83.   releaseNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/release/out/lib" ,
    84.         //debug状态下,,stripDebugSymbols的输出目录
    85.   debugStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/debug/out/lib" ,
    86.         //release状态下,,stripDebugSymbols的输出目录
    87.   releaseStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/release/out/lib" ,
    88. ]
    89.  //该闭包可以自动将文件上传到服务器,参数列表为资源id,资源文件路径
    90.  //我们可以再次执行上传服务器操作,并返回对应的url。
    91.  //当然也可以不实现上传操作,并自己手动上传资源。
    92. dynamic_upload = {
    93.  id, path ->
    94.  println"dynamic_upload id $ { id } ,path $ { path }" )
    95.         return 'http://url'
    96.  }
    1. 优化效果

    通过引入动态资源管理系统,并将一键报警sdk相关的so文件和其他普通资源动态化后,货拉拉用户端的包体积减少了8M,从54M变为了46M。后继将会继续尝试进行其他so文件的动态化。

    ded82fdd1a7d70c0cc0011701fc3a527.png
    1. 参考文献

    https://www.jianshu.com/p/260137fdf7c5

    https://mp.weixin.qq.com/s/X58fK02imnNkvUMFt23OAg

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

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

    往期推荐

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

    < END >

    作者:徐宜生

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

  • 相关阅读:
    华为云x86架构下部署mysql
    【线性代数基础进阶】行列式
    写给自己:入职两个月的收获与变化
    STL教程7-set、pair对组和仿函数
    基于 Redis 实现共享 Session 登录
    8寸Windows 10/Android 4.4系统三防平板电脑
    css-定位position 理论
    洛谷 P3263 [JLOI2015]有意义的字符串
    【1++的Linux】之进程(三)
    CentOS(2)——rpm和yum区别与联系
  • 原文地址:https://blog.csdn.net/x359981514/article/details/125863326