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

news2024/12/25 23:51:04

背景

最近在做包体积优化,在完成代码混淆、压缩,裁剪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> {
  @Override
  public 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 Task
          EngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);
          
          //指定自定义 Task 执行时机:mergeReleaseNativeLibs -> flutterSoDynamicRelease
          Task mergeSOTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");
          mergeSOTask.finalizedBy(engineSoDynamicTask);
        });
      });
    }
  }
}

自定义 Gradle Task

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

public class EngineSoDynamicTask extends DefaultTask {
    @Input
    public String mergeNativeLibsOutputPath;

    @TaskAction
    public void optimizeEngineSo() {

        //从 app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a 中找到 libflutter.so
        File 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.coloradmin.cn/o/1352357.html

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

相关文章

solidity显示以太坊美元价格

看过以太坊白皮书的都知道&#xff0c;以太坊比较比特币而言所提升的地方中&#xff0c;我认为最重要的一点就是能够访问外部的数据&#xff0c;这一点在赌博、金融领域应用会很广泛&#xff0c;但是区块链是一个确定的系统&#xff0c;包括里面的所有数值包括交易ID等都是确定…

pyqt6 + pycharm 搭建+使用入门

首先安装PyQt6和PyQt6-tools。使用如下命令&#xff1a; pip install PyQt6 PyQt6-tools 但是运行后会报如下错误&#xff1a; 这个时候按照提示执行命令升级pip即可 python.exe -m pip install --upgrade pip 配置pycharm&#xff1a; 打开pycharm&#xff0c;进入setting&am…

STM32 ESP8266 物联网智能温室大棚 (附源码 PCB 原理图 设计文档)

资料下载: https://download.csdn.net/download/vvoennvv/88680924 一、概述 本系统以STM32F103C8T6单片机为主控芯片&#xff0c;采用相关传感器构建系统硬件电路。其中使用DHT11温湿度传感器对温度和湿度的采集&#xff0c;MQ-7一氧化碳传感器检测CO浓度&#xff0c;GP2Y101…

SpringCloud微服务架构,适合接私(附源码)

一个由商业级项目升级优化而来的微服务架构&#xff0c;采用SpringBoot 2.7 、SpringCloud 等核心技术构建&#xff0c;提供基于React和Vue的两个前端框架用于快速搭建企业级的SaaS多租户微服务平台。 架构图 项目介绍 用户权益 仅允许免费用于学习、毕设、公司项目、私活等。…

软件测试之---测试设计方法

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

studio3T mongodb 根据查询条件去更新集合

mongodb 等于、不等于$ne、不包含 $nin 以及批量更新数据的使用。 业务场景&#xff1a; 在集合中&#xff0c;根据查询条件&#xff0c;更新数据状态。 实现代码&#xff1a; 1. 部门名称为XXX、状态不等于“完好”的、并且不包含这些编码的数据先查询出来2. 再把状态更新成…

STM32入门教程-2023版【3-2】推挽输出和开漏输出驱动问题

关注 点赞 不错过精彩内容 大家好&#xff0c;我是硬核王同学&#xff0c;最近在做免费的嵌入式知识分享&#xff0c;帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作! 二、正式点亮一个LED灯 &#xff08;4&#xff09;推挽输出和开漏输出驱动问题 把LED的正负极对换&…

Sharding Sphere 教程 简介

一 文档简介 1.1 分库分表诞生的前景 随着系统用户运行时间还有用户数量越来越多&#xff0c;整个数据库某些表的体积急剧上升&#xff0c;导致CRUD的时候性能严重下降&#xff0c;还容易造成系统假死。 这时候系统都会做一些基本的优化&#xff0c;比如加索引…

Linux之下载安装

rpm包管理 rpm介绍 rpm用于互联网下载包的打包及安装工具&#xff0c;他包含在某些linux分发版本中。他生成具有.rpm扩展名的文件。RPM是RedHat Package Manager(RedHat软件包管理工具&#xff09;的缩写&#xff0c;类似windows的steup.exe。 rpm包的查询指令 查询已经安装…

MFC综合实验二学习记录

文章目录 虚函数和纯虚函数的区别&#xff1f;MFC中什么是UPDATE_COMMAND_UI 消息如何查看控件对应的成员变量模态对话框的理解HGDIOBJ" 类型的值不能用于初始化 "CBrush *" 类型的实体错误MFC编程中CDC类型和HDC类型有什么区别&#xff1f;关于WIDING和ALTERNA…

深挖小白必会指针笔试题<一>

目录 引言 关键解决办法&#xff1a; 学会画图确定指向关系 例题一&#xff1a; 画图分析&#xff1a; 例题二&#xff1a; 画图分析&#xff1a; 例题三&#xff1a; 注&#xff1a;%x是按十六进制打印 画图分析&#xff1a; 例题四&#xff1a; 画图分析&…

常见的算法交易类型,一文看懂个人如何开通算法交易程序?

算法交易是指由计算机系统根据证券的历史数据分析、实时市场行情和交易员选择的策略及参数等&#xff0c;利用计算机程序和数学模型来决定交易下单的时机、价格和数量等&#xff0c;通过将大单拆为小单&#xff0c;以减小市场冲击成本&#xff0c;提高交易效率和交易隐蔽性的智…

裂变新模式:分销市场的翘楚

在当今的商业世界&#xff0c;推荐机制已经成为一种重要的营销策略。通过用户推荐&#xff0c;企业不仅能够扩大品牌影响力&#xff0c;还能有效降低获客成本。然而&#xff0c;如何设计一个合理的推荐机制&#xff0c;使得用户有足够的动力去推荐新人&#xff0c;同时保持团队…

1分钟生成爆款风景视频,Stable Video Diffusion最简教程

AI视频是2024年的重头戏&#xff0c;各大AI厂商都在跑视频技术&#xff0c;快速推出更牛的黑科技&#xff0c;SD其实在11月底就出了一款官方视频大模型-SVD&#xff0c;来跟runway、pika抢这块大蛋糕。 之前生成的视频效果还不是很理想&#xff0c;远没runway效果好&#xff0c…

Leetcod面试经典150题刷题记录 —— 链表篇

Leetcod面试经典150题刷题记录-系列Leetcod面试经典150题刷题记录——数组 / 字符串篇Leetcod面试经典150题刷题记录 —— 双指针篇Leetcod面试经典150题刷题记录 —— 矩阵篇Leetcod面试经典150题刷题记录 —— 滑动窗口篇Leetcod面试经典150题刷题记录 —— 哈希表篇Leetcod面…

数据库选择题 (期末复习)

数据库第一章 概论简答题 数据库第二章 关系数据库简答题 数据库第三章 SQL简答题 数据库第四第五章 安全性和完整性简答题 数据库第七章 数据库设计简答题 数据库第九章 查询处理和优化简答题 数据库第十第十一章 恢复和并发简答题 2015期末 1、在数据库中&#xff0c;下列说…

红酒送礼选对不选贵,这些挑选技巧一定要收藏好

遇到过节的时候&#xff0c;大家都张罗着买点什么东西送给亲朋好友老丈人&#xff0c;领导同事丈母娘。云仓酒庄的品牌雷盛红酒LEESON分享选择最多的就是烟酒茶&#xff0c;烟和茶已经成为常态&#xff0c;送红酒却是一种新风尚。在琳琅满目的红酒品类中&#xff0c;怎么才能选…

羊大师讲解喝羊奶的好处,让女性坚持下去!

羊大师讲解喝羊奶的好处&#xff0c;让女性坚持下去&#xff01; 羊奶是一种富含营养价值的健康饮品&#xff0c;且被广泛认为对女性有诸多好处。喝羊奶不仅能够提供丰富的营养物质&#xff0c;还含有一些特殊的成分&#xff0c;对女性健康起到了积极的作用。那么&#xff0c;…

RTC第二个功能和应用程序

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

Codeforces Good Bye 2023 A~E

A.2023(思维) 题意&#xff1a; 有一个序列 A a 1 , a 2 , . . . , a n k A a_1, a_2, ..., a_{n k} Aa1​,a2​,...,ank​&#xff0c;且这个序列满足 ∏ i 1 n k a i 2023 \prod\limits_{i 1}^{n k}a_i 2023 i1∏nk​ai​2023&#xff0c;而这个序列中的 k k k个…