Flutter 混合开发 - 动态下发 libflutter.so libapp.so

背景

最近在做包体积优化,在完成代码混淆、压缩,裁剪ndk支持架构,以及资源压缩(如图片转webp、mp3压缩等)后发现安装包的中占比较大的仍是 so 动态库依赖。
image.png具体查看发现 libflutter.so 和 libapp.so 的体积是最大的,这两个动态库都是 flutter 集成进来的。image.png结合项目中 Flutter 的应用,Flutter 页面都是作为二级页面使用,而且页面使用频率很低,所以是不是可以把这两个 so 从 apk 中剔除,在应用启动后再动态下发呢?
如果可以实现,那么包体积又可以缩减 13.8 M,包体积在原基础上立减一半,收益非常可观!开搞!

实战

libflutter.so & libapp.so 如何引入项目的?

项目是以远程依赖方式引入 flutter,即 flutter 开发完成后打包 aar 发布到公司 maven。通过解压已打包的 aar 发现,aar 中仅有 libapp.so,并没有 libflutter.so。而唯一提到 libflutter.so 的只有打包时生成的 pom 文件。
image.png
那么就从宿主项目入手。要远程依赖 flutter,需要指定 repositories{} 。通过配置发现,除了公司 maven 仓库地址,还需要额外配置一个 "https://storage.flutter-io.cn/download.flutter.io",结合打包时生成的 pom 文件,可以猜测 libflutter.so 是在依赖解析过程中引入到项目中的。

allprojects {repositories {google()mavenCentral()//flutter 需要的仓库配置:maven {url '******'  //公司 maven 仓库地址}maven {url 'https://storage.flutter-io.cn/download.flutter.io'}}
}

如何剔除与上传 libflutter.so & libapp.so

知道了这两个 so 文件如何引入到项目中的,那么接下来就要考虑怎么剔除与上传。剔除的时机有两个时间节点:打包 aar 时,打包 apk 时。结合已了解的 so 文件引入时机,打包 aar 时只能剔除 libapp.so,显然这个时机不合适,那么下面就来看打包 apk 时怎么实现剔除并上传这两个 so 文件。
既然要在打包 apk 时剔除并上传,毫无疑问需要自定义 Gradle Plugin 和 Gradle Task。如何自定义不细讲,网上相关文章太多,自行查看。

这里考虑只在项目中使用,所以直接在项目中新建 buildSrc Module,在里面实现 Gradle Plugin。

自定义 Gradle Plugin

  1. 明确只在打 release 包时才需要剔除(因为谁关心 debug 包包体积呀!)
  2. 确定剔除 Task 执行的时机。剔除要在 merge 所有 so 之后才行,通过查看 task 列表,发现 “mergeReleaseNativeLibs” 就是非常不错的时机。
public class FlutterDynamicPlugin implements Plugin<Project> {@Overridepublic void apply(Project project) {if (project.getPlugins().hasPlugin("com.android.application")) {project.afterEvaluate(project1 -> {AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);appExtension.getApplicationVariants().all(variant -> {String variantName = StringUtil.capitalize(variant.getName());//只在 release 变体下生效if (!variantName.equalsIgnoreCase("release")) return;//自定义 Gradle TaskEngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);//指定自定义 Task 执行时机:mergeReleaseNativeLibs -> flutterSoDynamicReleaseTask mergeSOTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");mergeSOTask.finalizedBy(engineSoDynamicTask);});});}}
}

自定义 Gradle Task

  1. 找到 libflutter.so
  2. 上传
  3. 剔除
  4. 记录上传信息(用于运行时下载)

public class EngineSoDynamicTask extends DefaultTask {@Inputpublic String mergeNativeLibsOutputPath;@TaskActionpublic void optimizeEngineSo() {//从 app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a 中找到 libflutter.soFile soFile = FileUtil.findSpecificFile(mergeNativeLibsOutputPath, "arm64-v8a", "libflutter.so");if (soFile == null || !soFile.exists()) return;//上传String url = HttpUtil.getInstance().upload(soFile);if (url != null){//记录上传信息write2Assets(url);//剔除soFile.delete();}}private void write2Assets(String url) {String content = "\"flutterSoUrl\":\"" + url + "\"";Write2AssetsUtil.getInstance().writeContent(content);}
}

这里以剔除 libflutter.so 为例,由于项目中只支持 arm64-v8a,所以只剔除了该架构下的。

坑点: 记录上传信息是通过向 assets 中插入 json 文件实现的,而上面只指定了自定义 Task 在 mergeReleaseNativeLibs Task 之后执行,这里就会偶现 assets 插入成功了,但打出的 apk 的 asstes 中并没有 json 文件。

原因: mergeReleaseNativeLibs Task 与 mergeReleaseAssets Task 没有指定的先后顺序,这就导致 assets 插入成功了,但被后续的 mergeReleaseAssets Task 覆盖掉了。

解决办法: 指定自定义 Task 、mergeReleaseNativeLibs Task、mergeReleaseAssets Task 三者先后顺序

EngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);
Task mergeNativeLibsTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");
Task mergeAssetsTask = project.getTasks().findByName("merge" + variantName + "Assets");                    // mergeReleaseNativeLibs -> flutterSoDynamicRelease -> mergeReleaseAssets
mergeNativeLibsTask.finalizedBy(engineSoDynamicTask);
mergeAssetsTask.dependsOn(engineSoDynamicTask);

运行时动态加载

libflutter.so & libapp.so 使用时机

要实现动态加载,先明确这两个 so 文件在何时用到,找到这个时间点,只要在其之前下载完成就,理论上就实现了运行时动态加载。
项目中使用的是官方多引擎方案(即 EngineGroup),所以先看它的构造函数中有何逻辑。

public class FlutterEngineGroup {public FlutterEngineGroup(@NonNull Context context) {this(context, null);}public FlutterEngineGroup(@NonNull Context context, @Nullable String[] dartVmArgs) {// FlutterInjector.instance() 该方法会创建一个 FlutterInjector 单例,//   FlutterInjector 实例创建过程中会创建 FlutterLoader 对象并赋值给 flutterLoader 变量FlutterLoader loader = FlutterInjector.instance().flutterLoader();if (!loader.initialized()) {loader.startInitialization(context.getApplicationContext());loader.ensureInitializationComplete(context.getApplicationContext(), dartVmArgs);}}
}

FlutterEngineGroup 构造函数中直接创建获取 FlutterLoader 对象,然后调用其 startInitialization() 和 ensureInitializationComplete()。限于篇幅,这里直接说结论:

  • startInitialization() 最终会执行 FlutterJNI#loadLibrary(),其内部调用 System.loadLibrary(“flutter”),实现加载 libflutter.so。
  • ensureInitializationComplete() 内部会准备一个 shellArgs 配置,最终调用 FlutterJNI#init() 执行。shellArgs 中有两条是关于 libapp.so 的。
public void ensureInitializationComplete({//...List<String> shellArgs = new ArrayList<>();//...shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName);shellArgs.add("--"+ AOT_SHARED_LIBRARY_NAME+ "="+ flutterApplicationInfo.nativeLibraryDir+ File.separator+ flutterApplicationInfo.aotSharedLibraryName);//...
}

通过上面可知,libflutter.so 和 libapp.so 都是在 FlutterEngineGroup 构造时调用的,那么只要在 FlutterEngineGroup 构造之前下载完成即可。

动态加载 libflutter.so

查看 FlutterEngineGroup 构造函数源码可知,libflutter.so 是通过 System.loadLibrary(“flutter”) 来实现加载的。结合 so 加载流程可知,将自定义的 so 文件路径注入到 classLoader#pathList#nativeLibraryDirectories 就可以实现优先加载,就可以实现 so 的动态加载了。这里我们直接复用 Tinker 的 TinkerLoadLibrary#installNativeLibraryPath() 。

动态加载 libapp.so

查看 FlutterEngineGroup 构造函数源码可知,libapp.so 是添加到一个配置中,然后调用 native 方法执行,所以无法想 libflutter.so 来实现。首先能想到的是能不能 hook 方法来自己实现配置,再次查看 FlutterEngineGroup 代码。
首先拿到 FlutterLoader 对象,那么看下 FlutterLoader 是怎么来的。

FlutterLoader loader = FlutterInjector.instance().flutterLoader();

public final class FlutterInjector {public static void setInstance(@NonNull FlutterInjector injector) {instance = injector;}public static FlutterInjector instance() {accessed = true;if (instance == null) {instance = new Builder().build();}return instance;}public static final class Builder {public Builder setFlutterJNIFactory(@NonNull FlutterJNI.Factory factory) {this.flutterJniFactory = factory;return this;}private void fillDefaults() {if (flutterJniFactory == null) {flutterJniFactory = new FlutterJNI.Factory();}if (executorService == null) {executorService = Executors.newCachedThreadPool(new NamedThreadFactory());}if (flutterLoader == null) {flutterLoader = new FlutterLoader(flutterJniFactory.provideFlutterJNI(), executorService);}}public FlutterInjector build() {fillDefaults();return new FlutterInjector(flutterLoader, deferredComponentManager, flutterJniFactory, executorService);}}
}

通过上面的代码可知,FlutterLoader 时在 FlutterInjector 构造时默认创建。同时值得注意的两点:

  • FlutterInjector 是单例模式,并提供 setInstance() 自行创建。
  • FlutterInjector 通过构造模式构建,并提供自行创建 FlutterJNI.Factory、FlutterLoader 等。

有这两点完全可以 hook FlutterLoader#ensureInitializationComplete()了,但实操下来发现代码量太大,实现难度太高。虽然没法 hook ensureInitializationComplete() 来修改配置,但在实操过程中发现重要信息。
image.png
大致意思是,下面的配置是为上面做兜底。如果我们把 libapp.so 剔除,那么这俩配置都无法生效,那我们可以再加一条来兜底啊,即把下载后 libapp.so 的存储路径配置上去。
结合之前的代码逻辑,shellArgs 最终会在 FlutterJNI#init() 中使用,而 FlutterJNI 又可以在 FlutterInjector 自行创建,那么问题不就简单了:

  • 新建自定义的 FlutterJNI 继承自 FlutterJNI,内部重写 init(),将下载后下载后 libapp.so 的存储路径添加到 shellArgs 中。
  • 在调用 FlutterEngineGroup 构造之前调用 FlutterInjector#setInstance() 将自定义的 FlutterJNI 注入进去。
class CustomFlutterJNI(private val appSOSavePath: String) : FlutterJNI(){override fun init(context: Context,args: Array<out String>,bundlePath: String?,appStoragePath: String,engineCachesPath: String,initTimeMillis: Long) {val hookArgs = args.toMutableList().run {add("--aot-shared-library-name=$appSOSavePath")toTypedArray()}super.init(context, hookArgs, bundlePath, appStoragePath, engineCachesPath, initTimeMillis)}class CustomFactory(private val appSOSavePath: String) : Factory(){override fun provideFlutterJNI(): FlutterJNI {return CustomFlutterJNI(appSOSavePath)}}
}
val appSOSavePath = "******"  // libapp.so 下载保存的存储路径
FlutterInjector.setInstance(FlutterInjector.Builder().setFlutterJNIFactory(CustomFlutterJNI.CustomFactory(appSOSavePath)).build())
val engineGroup = FlutterEngineGroup(context)

小结

通过如下几步实现了 libflutter.so 和 libapp.so 的剔除、上传、动态加载:

  • 自定义 GradleTask 实现在 merged_native_libs/ 中查找指定 so 文件、上传、记录上传信息(写入 assets 中)、剔除。
  • 自定义 GradlePlugin 指定仅在 release 打包中使用,并指定自定义 GradleTask 执行时机。
  • 读取 asstes 信息并下载,下载完成后通过注入 so 加载目录和 hook FlutterJNI 实现动态加载 so 文件,最后调用 FlutterEngineGroup 实现 Flutter 初始化。

实现后的效果非常显著:
image.png

完整代码(仅供参考)

GitHub - StefanShan/flutterSoDynamic: 从 apk 中剔除 libflutter.so 和 libapp.so,并动态下发加载

优化

上面把所有流程跑通了,但有些地方还需要优化:

  • libflutter.so 是根据 flutter 版本生成的,libapp.so 为业务代码生成,所以需要区分上传,即做版本控制,减少重复上传。
  • 同样在下载时,也要根据版本判断,避免重复下载。
  • 动态加载失败时,需要做兜底处理,例如用 H5 页面来替代。

文章来源(更多文章请点击) 青杉

参考资料

到家Flutter动态化瘦身方案的探索 - 墨天轮
Android 重构之旅:动态下发 SO 库
Android 动态链接库 So 的加载
Android编译期动态添加assets





Hi,我是“青杉”,您可以通过如下方式关注我:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/593192.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LobeChat:搭建你的私人 GPT!

前端训练营&#xff1a;1v1私教&#xff0c;终身辅导计划&#xff0c;帮你拿到满意的 offer。 已帮助数百位同学拿到了中大厂 offer。欢迎来撩~~~~~~~~ Hello&#xff0c;大家好&#xff0c;我是 Sunday。 之前有同学问我&#xff1a;“老师&#xff0c;我想要搭建一个个人的 …

TDD-LTE 附着流程和去附着流程

目录 1. 附着流程 1.1. 正常附着流程 2. 异常附着流程 2.1 RRC建立失败 2.2 核心网拒绝 2.3 eNodeB未收到初始化上下文建立请求 2.4 RRC重配置请求丢失 2. 去附着流程 2.1 非关机去附着流程 2.1.1 连接态非关机去附着 2.1.2 空闲态非关机去附着 2.2 关机去附着流程 …

实战Keras3.0:自定义图片数据集分类任务

一、创建自定义图片数据集 1、数据收集 以10张小狗图片和10张小猫图片为例 2、数据预处理 1、创建Excel表格&#xff0c;并在其中创建两列&#xff0c;一列是图片路径&#xff0c;另一列是对应的标签&#xff08;狗0、猫1&#xff09; 2、用pandas库的read_excel函数读取Exc…

java企业人事信息管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 java Web企业人事信息管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境 为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为M…

RTC第二个功能和应用程序

一般RTC模块设备管理时间日历、计时器等。从年到二。一些爱普生RTC 模块可以通过使用来自32768 Hz的分割频率来管理次第二功能。本文件 描述了RTC模块的三个具体的应用程序。&#xff08;表1&#xff09; 表1中的功能和产品 [FOUT函数应用程序] 图1描述了RTC模块&#xff0…

Python数据挖掘与机器学习实践技术应用

近年来&#xff0c;Python编程语言受到越来越多科研人员的喜爱&#xff0c;在多个编程语言排行榜中持续夺冠。同时&#xff0c;伴随着深度学习的快速发展&#xff0c;人工智能技术在各个领域中的应用越来越广泛。机器学习是人工智能的基础&#xff0c;因此&#xff0c;掌握常用…

qt .pro工程转vs工程

1. 新建vs空项目或者Qt Console Application&#xff1b; 2. 扩展 -》Qt VS Tools -》Open Qt Project (.pro) 打开对应的pro文件即可将.pro工程转成vs工程&#xff1b; 注意&#xff1a; &#xff08;1&#xff09;转成的vs工程在pro文件同级目录下&#xff0c;双击打开vcx…

桶式移位器

前言 本篇文章介绍CPU的核心部件之一&#xff1a;桶式移位器&#xff0c;简称BS&#xff0c;英文全称为Barrel Shifter 桶式移位器最大的特点就是能在单周期内完成多种方式&#xff0c;各种位数的移位操作 常见的移位操作 常见的移位操作种类如下&#xff1a; 算术右移 是指…

实验笔记之——基于windows复现Instant-NGP

之前博客对NeRF-SLAM进行了调研&#xff0c;本博文先复现一下Intant-NGP。 学习笔记之——NeRF SLAM&#xff08;基于神经辐射场的SLAM&#xff09;-CSDN博客文章浏览阅读851次&#xff0c;点赞22次&#xff0c;收藏21次。NeRF 所做的任务是 Novel View Synthesis&#xff08;…

在前端开发中,如何优化网站的加载速度?

在前端开发中&#xff0c;网站的加载速度是一个至关重要的因素&#xff0c;它直接影响着用户体验和搜索引擎优化&#xff08;SEO&#xff09;。一个快速、响应迅速的网站不仅能让用户更加满意&#xff0c;还能提高网站的排名和流量。那么&#xff0c;如何优化网站的加载速度呢&…

el-radio-button自适应充满盒子的写法

业务场景&#xff1a;当盒子较宽时&#xff0c;希望 el-radio-button 自适应充满盒子。 el-radio-button 自适应充满盒子的写法 <el-form :inline"true" :model"searchForm"><el-form-item style"display: flex; align-items: center;"…

【Java EE初阶七】多线程案例(阻塞队列与生产者消费者模型)

1. 阻塞队列 队列是先进先出的一种数据结构&#xff1b; 阻塞队列&#xff0c;是基于队列&#xff0c;做了一些扩展&#xff0c;适用于多线程编程中&#xff1b; 阻塞队列特点如下&#xff1a; 1、是线程安全的 2、具有阻塞的特性 2.1、当队列满了时&#xff0c;就不能往队列里…

串口通信要点解析

目录 简介&#xff1a; UART 协议解析&#xff1a; 串口协议工作过程&#xff1a; 简介&#xff1a; 串行通信协议包括&#xff1a; UART通用异步收发传输器 (Universal Asynchronous ReceiverTransmitter) 是一种串行异步收发协议 (异步是指通信双方使用各自的时钟控制数据…

fineBI web组件传参

1、fineBI web组件传参 1.1、 Web组件- FineBI帮助文档 FineBI帮助文档1. 概述1.1 版本FineBI 版本HTML5移动端展现功能变动6.0--V11.0.83web组件适配移动端效果优化6.0.13-web组件支持传递参数 ${过滤组件https://help.fanruan.com/finebi/doc-view-143.html 1.2、自己做的例…

分布式(6)

目录 26.雪花算法如何实现的&#xff1f; 27.雪花算法有什么问题&#xff1f;有哪些解决思路&#xff1f; 28.有哪些方案实现分布式锁&#xff1f; 29.基于数据库如何实现分布式锁&#xff1f;有什么缺陷&#xff1f; 30.基于Redis如何实现分布式锁&#xff1f;有什么缺陷&…

VS 2022 控制台程序运行时不显示控制台

Visual Studio 2022&#xff0c;C#控制台程序运行时不显示控制台。此外&#xff0c;C#程序修改运行时的程序名。 文章目录 不显示控制台修改运行时的程序名打包成.exe 文件 不显示控制台 1 选中需要项目&#xff0c;右击属性&#xff0c;选中常规。 2 将输出类型从控制台改为…

微服务-@FeignClient 与 Feign 隔离

FeignClient 扫描 FeignClientsRegistrar#registerBeanDefinitions public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) { // 注册默认配置 registerDefaultConfiguration(metadata, registry); registerFeignClients(metada…

服务异步通讯---RabbitMQ实用篇

目录 一、初识MQ 一、同步调用 1、同步通讯和异步通讯 2、同步调用的问题 3.同步调用总结 二、异步调用 1、优势&#xff1a; ​编辑 2、异步总结 二、什么是MQ 一、RabbitMQ快速入门 1、RabbitMQ的结构和概念 2、常见消息模型 2.1、基础消息队列模型 2.2、总结 二…

Sharding-JDBC快速使用【笔记】

1 引言 最近在使用Sharding-JDBC实现项目中数据分片、读写分离需求&#xff0c;参考官方文档&#xff08;Sharding官方文档&#xff09;感觉内容庞杂不够有条理&#xff0c;重复内容比较多&#xff1b;现结合项目应用整理笔记如下供大家参考和自己回忆使用&#xff1b; 在…

为什么要太空探索?未来万亿人口 人类移居太空是不可避免的。大语言模型是发现 贝索斯

管理的思考 「最以客户为中心」「果断」「如何决策」 贝索斯给亚马逊的使命是「世上最以客户为中心的公司」(以客户需求为起点&#xff0c;反向推动工作)。贝索斯给蓝色起源的使命是「世上最果断的公司」(我们将变得非常擅长在技术上恰当地冒险&#xff0c;并快速地作出那些决…