【译】Flutter 延迟组件原理与自定义 【包体积优化 _ 动态化】

文章详细描述了FlutterGallery在fullydeferred分支上采用延迟加载组件后,安装包体积大幅减小的情况,并解释了dart代码和资源如何被拆分成独立的加载单元,以及loadLibrary()调用的生命周期和自定义下载实现的过程。
摘要由CSDN通过智能技术生成

Flutter Gallery 在 fully deferred Flutter gallery branch 分支,将其中所有的 demo 改为了延迟加载组件,对比未使用延迟加载的情况下,安装 Apk 文件的大小数据如下:

使用延迟组件:

  • base-arm64_v8a.apk - 12,325,372 bytes
  • base-master.apk - 37,889,309 bytes
  • 安装包大小: 50,214,681 bytes

未使用延迟组件:

  • base-arm64_v8a.apk - 12,521,900 bytes
  • base-master.apk - 80,605,796 bytes
  • 安装包大小: 93,127,696 bytes

我们可以看到编译后的代码大小(base-arm64_v8a.apk) 减少了约 200kB,初始包体积(base-master.apk)减少了约 43MB。总体而言,初始安装尺寸减少了46%。dart代码、资源文件等会被移动到单独的组件中,只有在需要时才会在运行时下载。安装了所有组件后应用的体积只比非延迟安装的应用仅多几 KB。

延迟组件的应用结构

延期的 Dart 库通过 gen_snapshot(Dart编译器)生成「加载单元」,当以 profile 或者 release 模式构建的时候,每个加载单元输出为一个拆分的 AOT 共享库(.so 文件)。加载单元是代码中用 deferred 关键字引入库的最小的集合,可以从基础库中分离出来。

下图展示了使用延迟组件的应用程序结构,和延迟 dart 库被编译成加载单元并打包成 .aab 文件的「生命周期」。

这个例子有以下特点:

  • 四个 Dart 库,其中 Dart 库 lib1 依赖于 lib2。lib1、lib3 和 lib4 被作为延迟组件导入到 flutter 应用程序的主代码中。
  • 四个加载单元,其中 id 为 1 的是基本单元,加载单元 2 同时包含 lib1 和 lib2。加载单元 3 和 4 分别包含 lib3 和 lib4。
  • 三个定义的延迟组件,加上一个隐式基本组件。延迟组件 1 包含加载单元 2 和静态资源。延迟组件 2 包含加载单元 3 和 4,没有静态资源。而延迟组件 3 是一个仅含静态资源的组件。
  • app-release.aab 是完整的构建输出文件,包含三个延迟组件以及基本组件。

.aab 文件中总包含一个未显式声明的基本组件,它包含核心的 Flutter 包以及最基本的应用程序代码。任何未被延迟加载的库都会被包含在基本加载单元中。如果没有生成基本单元以外的加载单元,这可能意味着延迟导入的文件不正确。

对于延迟导入的库而言,里面非延迟导入的文件,会被编译到一个加载单元中:

loadLibrary() 调用的生命周期

延迟组件主要通过 dart 中的 loadLibrary() 调用来触发被下载、安装和加载。这个调用在 dart2js 和 aot/native 中的处理是不同的。这里,我们梳理 loadLibrary() 调用转换为一个延迟组件的安装过程:

dart 中 loadLibrary() 会调用的 native 层的 Dart_DeferredLoadHandler 函数,该回调函数在DartIsolate::Initialize 中由 Dart_SetDeferredLoadHandler 设置。Dart 在内部检索分配给库的加载单元 ID,并将其传递给回调函数。回调到 DartIsolate::OnDartLoadLibrary。

加载单元 ID 然后通过 runtime controller、engine 和 platform view 传递到 Android 嵌入层中的 FlutterJNI。这里,加载单元 ID 被传递到 DeferredComponentsManagerinstallDeferredComponent 方法中, ID 从一个整数映射为一个 String 名称,标识请求库所属的 pubspec 定义的延迟组件。这个转换由 AndroidManifest 中的 meta-data 映射,在构建阶段创建并验证。

之后 PlayStoreDeferredComponentManager 调用 API 下载 module。module 安装会定位到 .so 文件,并将路径传递给 engine 执行 dloopen。engine 将解析的符号发送到 dart isolate,以此将这些符号加载到 dart VM 中。整个加载过程必须与加载单元 ID 关联,否则 loadLibrary() 返回的 Future 对象不会完成。

请记住,多个加载单元可能包含在一个延迟组件中,但是 loadLibrary 只会从调用的特定 dart 库中加载 dart 符号。每个加载单元在使用前必须单独的调用 loadLibrary。对已经下载过的组件,后续调用 loadLibrary 不会二次加载,但是也并非同步完成,在调用和完成之间至少有一帧间隔。

通过延迟组件名称进行安装

我们还提供了 framework-side DeferredComponent utility class,它允许通过延迟组件名来直接安装。

这个方法可以有两个用途:

  • 安装只有静态资源的延迟组件。
  • 提前下载延迟组件以供以后使用。但是,为了使用提前下载组件中的 dart 代码,仍然必须调用loadLibrary() 。

这个直接的 API 通过 platform channels 直接调用 DynamicFeatureManagerinstallDeferredComponent 方法,并且由于未指定的加载单元,不会加载组件的任何 dart 代码。仅有静态资源被加载。要使用 dart 代码,还必须调用 loadLibrary()

卸载

DeferredComponent 的工具类中还提供了 uninstallDeferredComponent 方法,该方法使用 platform channels 请求操作系统卸载并删除与指定的延迟组件关联的文件。不同平台的卸载行为也不一样,在 Android 中文件的删除是排队的,在实际执行之前可能需要很长时间。

只能用将要卸载的组件名称来请求卸载。目前还不支持通过加载单元 id 或直接调用 dart 来卸载。

工具

延迟组件必须构建为 Android App Bundles (.aab) 才能正常工作。如果构建为调试文件或 apk 文件,则 dart 将正常编译并生成一个 .so 文件。

延迟组件使用 $ flutter build appbundle 命令构建,它会检查 pubspec.yaml 中是否存在 deferred-components: 来决定是否延迟构建。当应用程序中包含延迟组件并且构建模式为 profile 或者release,gen_snapshot 会收到一个 ——loading_unit_manifest 路径,它告诉 gen_snapshot 生成拆分的 AOT 产物,包含一个基本文件,以及一个 .so 用于代码库中的每个延迟库。这些分割的单元称为「加载单元」,并被分配一个内部整数 ID,称为加载单元 ID。

构建过程还依赖于项目设置来发挥作用。每个延迟组件必须对应于应用程序的 android 目录下定义的 android module。基本 module 被构建为 app ,而每个附加组件应该有一个与该组件同名的 module。基本模块AndroidManifest.xml 也需要包含加载单元 id 和延迟组件之间的映射。

flutter build appbundle 命令会执行一个验证程序,这个验证程序会指导开发人员完成正确的构建。验证程序是必要的,因为在 gen_snapshot 完成编译之前,无法知道 gen_snapshot 生成的加载单元。因此,某些项目设置只能 gen_snapshot 步骤之后才能完成。

因为错误地将延迟组件库作为非延迟组件库导入会导致文件被编译到基本加载单元中,所以延迟组件验证程序也有一种机制来防止对应用最终生成的加载单元的意外更改。如果生成的加载单元与缓存在 deferred_components_loading_units.yaml 文件中前一次运行的结果不匹配,则此检查将导致构建失败。在检测到由更改而抛出错误后,如果没有进行其他更改,构建将在下次运行时自动通过此检查。这意味着这个检查不是错误证明,因为你仍然可以忽略不匹配的加载单元错误,并继续构建。

自定义实现

可以不通过 Android Play 商店实现自定义下载。这只推荐给高级开发者,主要针对具有特殊需求的应用,如超大的静态资源,某些特定地下载行为,或无法访问 Play 商店的地区(如中国)。

简介

Flutter 嵌入层允许自定义实现,处理自定义的的延迟组件下载和解压,同时仍然允许访问核心的 Dart 回调,该回调将加载单元注册到 Dart runtime。这个过程比默认的 play store 版本要复杂。

要实现一个自定义延迟组件系统,主要包含以下部分:

  • DeferredComponentManagerAndroid 嵌入层的实现,用于处理应用程序和服务器之间的通信,并从下载的组件中提取 .so 文件和静态资源。

  • DeferredComponentManager 兼容的打包组件的工具,并解释加载单元的 gen_snapshot 输出。

  • 存放组件的服务器,如果没有 Play 商店作为动态功能模块下发,这就必须定制的。

下面的部分会做详细的指导:

DeferredComponentManager- 安卓嵌入层

嵌入层负责下载和安装打包的组件文件。这可以通过在 Android 嵌入层中继承抽象类 DeferredComponentManager 来实现

installDeferredComponent 是这个类的入口 ,它提供了加载单元 id 和组件名称,来确定要安装什么组件。loadLibrary() 的调用传递唯一的一个加载单元 id,而框架层中的DeferredComponent.installDeferredComponent() 的调用需要传入位移的组件名来加载只含静态资源的组件。

为了将加载单元 id 解析为特定的组件,通常需要存储加载单元 id 到组件名称的映射。在默认实现中,我们通过在应用程序中的 AndroidManifest.xml 中存储一个键值对数据来实现,但这可以以任何想要的方式实现。

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上我搜集整理的2019-2021BATJ 面试真题解析,我把大厂面试中常被问到的技术点整理成了PDF,包知识脉络 + 诸多细节。

节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?

1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
1714946612223)]

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Flutter 中,你可以使用自定义组件来创建树状图。下面是一个简单的示例: 首先,创建一个名为 `TreeNode` 的自定义组件,用于表示树的节点: ```dart class TreeNode extends StatelessWidget { final String title; final List<TreeNode> children; TreeNode({required this.title, required this.children}); @override Widget build(BuildContext context) { return Column( children: [ Text(title), SizedBox(height: 10), Column( children: children.map((node) => TreeNode(title: node.title, children: node.children)).toList(), ), ], ); } } ``` 然后,可以使用 `TreeNode` 组件来构建树状图。例如,假设我们有以下的树结构: ``` - Root - Node 1 - Leaf 1.1 - Leaf 1.2 - Node 2 - Leaf 2.1 - Node 3 ``` 可以使用以下代码来构建树状图: ```dart TreeNode root = TreeNode( title: 'Root', children: [ TreeNode( title: 'Node 1', children: [ TreeNode(title: 'Leaf 1.1', children: []), TreeNode(title: 'Leaf 1.2', children: []), ], ), TreeNode( title: 'Node 2', children: [ TreeNode(title: 'Leaf 2.1', children: []), ], ), TreeNode(title: 'Node 3', children: []), ], ); ``` 最后,将 `root` 组件放入 `build` 方法中的 `Widget` 树中进行渲染: ```dart @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Tree Example'), ), body: SingleChildScrollView( child: TreeNode(title: root.title, children: root.children), ), ); } ``` 这样,你就可以在 Flutter 应用中显示一个简单的树状图了。根据实际需求,你可以对 `TreeNode` 组件进行更多的自定义和样式调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值