目标:
在上一次https://www.cnblogs.com/webor2006/p/14522736.html已经对于Flutter有了一个整体的认识,这次则来剖析一下它的原理,以及将咱们上次实现的Flutter推荐模块集成到主工程当中来,实现Flutter和Android的一个混编,这块知识还是很值得期待的,先来纵览一下此次学习的目标:
Flutter框架和原理剖析:
Flutter框架的整体结构:
Flutter是Google推出并开源的跨平台开发框架,主打跨平台、高保真、高性能。开发者可以通过Dart语言开发Flutter应用,一套代码同时运行在ios和Android平台。不仅如此,Flutter还支持Web、桌面、嵌入应用的开发。Flutter提供了丰富的组件、接口,开发者可以很快地为Flutter添加native扩展。同时Flutter还使用skia引擎渲染视图,这无疑能为用户提供良好的体验。
下面来看一下Flutter框架的整体结构组成,在之前也看到:
Flutter主要有三个主要组成部分:框架层、引擎层、平台层。
框架层:
Flutter框架建立在Dart语言的基础上:
- Foundation:Framework的最底层叫Foundation,其中定义的大都是非常基础的、提供给其他所有层使用的工具类和方法;
- Animation:动画相关的类库;
- Painting:绘制库(Painting)封装了Flutter Engine提供的绘制接口,主要是为了在绘制控制等固定样式的图形时提供更直观、更方便的接口,比如绘制缩放后的位图、绘制文本、插值生成阴影以及在盒子周围绘制边框等等;
- Gesture:提供了手势识别相关的功能,包括触摸事件类定义和多种内置的手势识别器;
- Widgets:在Flutter中一切UI皆widget,Flutter有两大不同风格的widget库:
1、一个是基于Material Design(材料设计)风格的组件库;
2、一个是基于cupertino的ios设计风格的组件库。
引擎层:
Flutter引擎使用的是基于c++的2D图形库(称为Skia)。在这一层中,提供了Dart VM,以提供一个执行环境,用于将Dart代码转换为本地平台可执行代码。Flutter引擎在Android、ios中运行,以为widget呈现对应的外观,并根据特定平台通过Channel进行通信;
平台层:
Flutter根据不同平台提供了其特定的shell(既Android Shell和IOS Shell),这些shell用来托管Dart VM,以提供对特定的平台API的访问;
Flutter绘制原理:
熟悉Flutter绘制原理有助于我们了解Flutter框架的原理机制。为了熟悉Flutter绘制原理,我们先从屏幕显示图像的基本原理开始说起:
我们在买显示器时,都会关注显示器的刷新频率;那么对于手机屏幕也是一样的,通常手机屏幕的刷新频率是60Hz,当然现在也有不少高刷新频率的手机也在推出,如:90Hz,120Hz。
一般来说,计算机系统中,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给GPU,GPU渲染后放放帧缓冲区,然后视频控制器按照VSync信号从帧缓冲区取帧数据传递给显示器显示。当一帧图像绘制完毕后准备绘制下一帧时,显示器就会发出一个垂直同步信号(VSync),所以60Hz的屏幕就会一秒内发生60次这样的信号。
上面是CPU、GPU和显示器协作方式,对于Flutter也不例外,Flutter也遵循了这种模式:
GPU的VSync信号同步给到UI线程,UI线程使用Dart来构建抽象的视图结构,这份数据结构在GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU。
Android UI绘制原理浅析:
在上面Flutter绘制原理的阐述中提到最终交由Skia引擎来进行图形渲染,听到这个词是不是可以联想到我们的Android呢?所以这里转一个视角,对Android UI的绘制原理进行一个简单回顾:
说到Android的UI绘制自然离不了Canvas,Android上层的UI绘制几乎都通过Canvas来完成的,那么Canvas又是怎么完成UI绘制的呢,接下来就让我们来通过追踪源码来一探究竟,下面以Canvas绘制圆形这个API来例进行分析:
Canvas.java:drawCircle
其中它的父类是:
BaseCanvas.java:drawCircle
BaseCanvas.java:nDrawCircle
此时就进入了c++的世界了。
Canvas.cpp:drawCircle
SkCanvas.h:drawCircle
由此可以看出Android UI绘制最终还是交给Skia来完成的。
Flutter渲染流程:
在Flutter框架中存在着一个渲染流程(Rendering pipline)。这个渲染流水线是由垂直同步信号(Vsync)驱动的,而Vsync信号是由系统提供的,如果你的Flutter app是运行在Android上的话,那Vsync信号就是我们熟悉的Android那个Vsync信号。
当Vsync信号到来以后,Fluttter框架会按照图里的顺序执行一系列动作:
- 动画(Animate)
- 构建(Build)
- 布局(Layout)
- 绘制(Paint)
最终生成一个场景(Scene)之后送往底层,由GPU绘制到屏幕上。
- 动画(Animate)阶段:因为动画会随每个Vsync信号的到来而改变状态(State),所以动画阶段是流水线的第一个阶段;
- 构建(Build)在这个阶段Flutter,在这个阶段那些需要被重新构建的Widget会在此时被重新构建。也就是我们熟悉的StatelessWidget.build()或者State.build()被调用的时候;
- 布局(Layout)阶段:这时会确定各个显示元素的位置,尺寸;此时是RenderObject.performLayout()被调用的时候;
- 绘制(Paint)阶段:此时是RenderObject.paint()被调用的时候;
以上是整个渲染流程的一个大致的工作过程。
Flutter组件的生命周期:
- createState():当框架要创建一个StatefulWidget时,它会立即调用State的createState();
- initState():当State的构造方法被执行后,会调用一次initState(),需要指出的是initState()在State生命周期内只被调用一次;
- build():这个方法会被经常调用,比如:setState以及配置改变都会触发build()方法的调用;
- didUpdateConfig():当收到一个新的config时调用;
- setState():当需要修改页面状态,比如刷新数据等的时候我们可以通过调用setState来实现;
- dispose():当移除State对象时,将调用dispose();通常在该方法中进行取消订阅,取消所有动画 ,流等操作;
FAQ:
有Flutter的应用体积为什么这么大?
这块可以参考:https://www.jianshu.com/p/0e223b472f41?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation,
为什么Flutter采用Dart语言做开发?
关于这块可以参考,https://www.jianshu.com/p/73a11e304ef1。
探析Flutter渲染机制之三棵树:
关于这块可以参考大神的这篇文章:https://blog.csdn.net/fengyuzhengfan/article/details/112442747,这里基于它进行一个梳理:
Flutter是一个优秀的UI框架,借助它开箱即用的Widgets我们能够构建出漂亮和高性能的用户界面。那这些Widgets到底是如何工作的又是如何完成渲染的。
所以接下来就来探析Widgets背后的故事-Flutter渲染机制之三棵树。
什么是三棵树?
在Flutter中和Widgets一起协同工作的还有另外两个伙伴:Elements和RenderObjects;由于它们都是有着树形结构,所以经常会称它们为三棵树。
- Widget:Widget是Flutter的核心部分,是用户界面的不可变描述。做Flutter开发接触最多的就是Widget,可以说Widget撑起了Flutter的半边天;
- Element:Element是实例化的 Widget 对象,通过 Widget 的 createElement() 方法,是在特定位置使用 Widget配置数据生成;
- RenderObject:用于应用界面的布局和绘制,保存了元素的大小,布局等信息;
初次运行时的三棵树:
初步认识了三棵树之后,那Flutter是如何创建布局的?以及三棵树之间他们是如何协同的呢?接下来就让我们通过一个简单的例子来剖析下它们内在的协同关系:
class ThreeTree extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Container(color: Colors.blue)
);
}
}
上面这个例子很简单,它由三个Widget组成:ThreeTree、Container、Text。那么当Flutter的runApp()方法被调用时会发生什么呢?下面在Flutter工程中先来构建这么一个简单的示例:
运行一下:
此时可以打开“Flutter Inspector”:
那第二棵树在哪里呢?此时需要跟一下源码了:
总结一下就是:
当runApp()被调用时,第一时间会在后台发生以下事件:
- Flutter会构建包含这三个Widget的Widgets树;
- Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对象,最后将这些对象组建成Element树;
- 接下来会创建第三个树,这个树中包含了与Widget对应的Element通过createRenderObject()创建的RenderObject;
而整个状态过程可以用下图来描述:
从图中可以看出Flutter创建了三个不同的树,一个对应着Widget,一个对应着Element,一个对应着RenderObject。每一个Element中都有着相对应的Widget和RenderObject的引用。可以说Element是存在于可变Widget树和不可变RenderObject树之间的桥梁。Element擅长比较两个Object,在Flutter里面就是Widget和RenderObject。它的作用是配置好Widget在树中的位置,并且保持对于相对应的RenderObject和Widget的引用。
三棵树的作用:
那这三棵树有啥意义呢?简而言之是为了性能,为了复用Element从而减少频繁创建和销毁RenderObject。因为实例化一个RenderObject的成本是很高的,频繁的实例化和销毁RenderObject对性能的影响比较大,所以当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的Widget树,接下来从源码中来体会一下:
此时也是只更新对应的element,接下来继续:
总结如下:
因为在框架中,Element是被抽离开来的,所以你不需要经常和它们打交道。每个Widget的build(BuildContext context)方法中传递的context就是实现了BuildContext接口的Element。
更新时的三棵树:
那如果此时我们修改一下程序:
因为Widget是不可变的,当某个Widget的配置改变的时候,整个Widget树都需要被重建。例如当我们改变一个Container的颜色为橙色的时候,框架就会触发一个重建整个Widget树的动作。因为有了Element的存在,Flutter会比较新的Widget树中的第一个Widget和之前的Widget。接下来比较Widget树中第二个Widget和之前Widget,以此类推,直到Widget树比较完成。
Flutter遵循一个最基本的原则:判断新的Widget和老的Widget是否是同一个类型:
- 如果不是同一个类型,那就把Widget、Element、RenderObject分别从它们的树(包括它们的子树)上移除,然后创建新的对象;
- 如果是一个类型,那就仅仅修改RenderObject中的配置,然后继续向下遍历;
在我们的例子中,ThreeTree Widget是和原来一样的类型,它的配置也是和原来的ThreeTreeRender一样的,所以什么都不会发生。下一个节点在Widget树中是Container Widget,它的类型和原来是一样的,但是它的颜色变化了,所以RenderObject的配置也会发生对应的变化,然后它会重新渲染,其他的对象都保持不变。
上面这个过程是非常快的,因为Widget的不变性和轻量级使得他能快速的创建,这个过程中那些重量级的RenderObject则是保持不变的,直到与其相对应类型的Widget从Widget树中被移除。
注意这三个树,配置发生改变之后,Element和RenderObject实例没有发生变化。
当Widget的类型发生改变时:
和刚才流程一样,Flutter会从新Widget树的顶端向下遍历,与原有树中的Widget类型进行对比。
因为FlatButton的类型与Element树中相对应位置的Element的类型不同,Flutter将会从各自的树上删除这个Element和相对应的ContainerRender,然后Flutter将会重建与FlatButton相对应的Element和RenderObject。如下:
很明显这个重新创建的过程相对耗时的,但是当新的RenderObject树被重建后将会计算布局,然后绘制在屏幕上面。Flutter内部使用了很多优化方法和缓存策略来处理,所以你不需要手动来处理这些。以上便是Flutter的整体渲染机制,可以看出Flutter利用了三棵树很巧妙的解决的性能的问题。
如何在已有的项目中集成Flutter:
在Flutter的应用场景中,有时候一个APP只有部分页面是由Flutter实现的,比如:我们常用的闲鱼APP,它宝贝详情而面是由Flutter实现的,这种开发模式被称为混合开发。接下来则将Flutter集成到咱们的主工程上来。
Warning:
- 从Flutter v1.17版本开始,Flutter module仅仅支持AndroidX的应用;所以如果想在木有使用AndroidX的应用中来使用Flutter就需要使用比它低一点的了。
- 在release模式下Flutter仅支持以下架构:x86_64,armeabi-v7a,arm64-v8a,不支持mips和x86;所以引入Flutter前需要选取Flutter支持的架构。
所以咱们配置一下:
混合开发的一些适用场景:
场景一:在原生项目中加入Flutter页面
也就是从原生页面项中点击会跳到一个Flutter页面。
场景二:在原生页面中嵌入Flutter模块
如上图,其Flutter只是做为原生的一部分出现。
场景三:Flutter页面中嵌入原生模块
也就是在Flutter页面中有局部是嵌入的原生模块。
以上这些都属于Flutter混合开发的范畴,那么如何将Flutter集成到现有的Android应用中呢?
主要步骤:
将Flutter集成到现有的Android应用中需要如下几个主要步骤:
- 首先,创建Flutter module;
- 为已存在的Android应用添加Flutter module依赖;
- 在Koltin中调用Flutter module;
- 编写Dart代码;
- 运行项目;
- 热重启/重新加载;
- 调试Dart代码;
- 发布应用;
- 升职加薪、迎娶白富美,走向人生巅峰!
提示:为了能够顺利的进行Flutter混合开发,请确认你的项目结构是这样子的:
flutter_hybird下面分别是flutter模块、原生Android模块、原生IOS模块,并且这三个模块是并列结构。
创建Flutter module:
概述:
在做混合开发之前我们首先需要创建一个Flutter module。假如你的Native项目是这样的:xxx/flutter_hybird/Native项目:
上面代码会切换到你的Android/Ios目录的上一级目录,并创建一个flutter模块:
上面是flutter_module中的文件结构,你会发现它里面包含.android和.ios,这两个文件夹是隐藏文件,也是这个flutter_module宿主工程:
- .android:flutter_module的Android宿主工程;
- .ios:flutter_module的ios宿主工程;
- lib:flutter_module的Dart部分的代码;
- pubspec.yaml:flutter_module的项目依赖配置文件;
因为宿主工程的存在,我们这个flutter_module在不加额外的配置的情况下是可以独立运行的,通过安装了Flutter与Dart插件的AndroidStudio打开这个flutter_module项目,通过运行按钮是可以直接运行它的。
实践:
接下来来到咱们的工程中照着上面的阐述操作一把,这里一定要先定位好目录,目前要创建flutter模块是需要在这里进行创建的:
而Flutter Module本身就已经考虑到了大企业开发的场景, 因为Flutter一般是由专门的仓库进行维护,所以它跟Android的主工程是并行互不干涉的,所以就不建议在Asproj工程里面进行flutter module的创建,而是在与Asproj主工程平行的目录中进行创建,好处就是各个仓库互不干扰可以并行开发,这样可以提高多团队开发的效率,接下来使用命令创建一下:
(base) xiongweideMacBook-Pro:studycode xiongwei$ ls -l
total 0
drwxr-xr-x 35 xiongwei staff 1120 3 7 07:36 Asproj
drwxr-xr-x 14 xiongwei staff 448 12 11 08:43 HiArch
drwxr-xr-x 14 xiongwei staff 448 12 7 13:32 HiDesignPattern
drwxr-xr-x 15 xiongwei staff 480 12 3 05:26 HiTinker
drwxr-xr-x 20 xiongwei staff 640 3 5 06:10 aapt2
drwxr-xr-x 3 xiongwei staff 96 9 16 2020 as-navigation
drwxr-xr-x 16 xiongwei staff 512 3 21 04:53 flutter_app
drwxr-xr-x 14 xiongwei staff 448 10 21 17:10 hi-concurrency
drwxr-xr-x 3 xiongwei staff 96 11 18 19:36 hi-jectpack
drwxr-xr-x 14 xiongwei staff 448 11 18 19:39 hi-jetpack
drwxr-xr-x 16 xiongwei staff 512 12 23 06:23 hi-library
drwxr-xr-x 16 xiongwei staff 512 12 23 06:33 hi-ui
(base) xiongweideMacBook-Pro:studycode xiongwei$ flutter create -t module --org org.devio.as.proj flutter_module
Creating project flutter_module...
flutter_module/test/widget_test.dart (created)
flutter_module/flutter_module.iml (created)
flutter_module/.gitignore (created)
flutter_module/.metadata (created)
flutter_module/pubspec.yaml (created)
flutter_module/README.md (created)
flutter_module/lib/main.dart (created)
flutter_module/flutter_module_android.iml (created)
flutter_module/.idea/libraries/Dart_SDK.xml (created)
flutter_module/.idea/modules.xml (created)
flutter_module/.idea/workspace.xml (created)
Running "flutter pub get" in flutter_module... 2.3s
Wrote 11 files.
All done!
Your module code is in flutter_module/lib/main.dart.
此时就创建好了:
打开看一下它的目录结构:
其中具体目录中的含义在上面已经阐述过了,这里就不过多说明了。
为已存在Android应用添加Flutter module依赖:
接下来就需要将创建的Flutter module依赖到我们的主工程,有如下两种方式可以依赖:
方式一:构建flutter aar(非必须):
如果需要将咱们的flutter module以aar的方式来集成到Android工程中,可以用如下命令:
也就是进入这个目录下来执行上面的命令:
但是这里不采用此方法。
方式二:添加依赖
阐述:
这种方式更加的顺畅,直接打开Android项目的settings.gradle来添加如下代码进行依赖:
其中setBinding与evaluate允许flutter模块包含它自己在内的任何Flutter插件,在settings.gradle中以类似:":flutter、package_info、:video_player"的方式存在;
实践:
1、下面咱们来到咱们的主工程中来配置一下:
其配置信息如下:
//for flutter
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'flutter_module/.android/include_flutter.groovy' // new
))
//可选,主要作用是可以在当前AS的Project下显示flutter_module以方便查看和编写Dart代码
include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')
此时再同步一下项目。
2、添加:flutter依赖:
这里则需要在我们需要使用的地方来添加依赖,由于咱们工程中很多地方都需要用到它,所以可以将其放到common module中,如下:
注意这里不传递依赖,只在需要使用的页面上进行使用,另外由于咱们还要在这个页面中进行使用:
所以,在这个module中也添加flutter的依赖:
3、添加Java8编译选项:
由于Flutter的Android engine使用了Java 8 的特性,所以在引入Flutter时需要配置你的项目的java 8 编译选项,在app的build.gradle文件的android{}节点下添加:
这块咱们已经添加过了,就不多说了。
在Koltin中调用Flutter module
至此,我们已经为我们的Android项目添加了Flutter所必须的依赖,接下来我们来看如何在项目上以Kotlin的方式在Fragment中调用Flutter模块:
1、创建HiFlutterFragment:
2、初始化FlutterEngine:
package org.devio.`as`.proj.common.flutter
import android.content.Context
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import org.devio.`as`.proj.common.ui.component.HiBaseFragment
abstract class HiFlutterFragment : HiBaseFragment() {
protected lateinit var flutterEngine: FlutterEngine
override fun onAttach(context: Context) {
super.onAttach(context)
flutterEngine = FlutterEngine(context)
//让引擎执行Dart代码
flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
}
}
3、添加flutter页面布局:
其中可以看到布局都是原生的元素,无任何flutter相关的元素。
4、setTitle():
5、在布局中添加Flutter View:
目前布局很显然不能承载Flutter,所以接下来动态创建一下它:
其中有一点需要稍加解释一下,就是图中注释的部分:
渲染FlutterView的两种方式,除了现在在用的FlutterTextureView之外,还有它:
但是这里不采用它的原因是由于当app切后台这种View是会有复用的问题,比如:
此时将app切到后台,然后再切到前台时,推荐的FlutterView会被复用到收藏的FlutterView上的,很显然是不对的,所以这一点需要明确。
6、重写各种生命周期:
为了能让Flutter感知到各个生命周期,所以需要重写一系列的方法,如下:
这样Flutter就能感知Fragment的生命周期了。
7、为啥不用FlutterFragment?
对于Flutter而言其实它有一个系统为咱们准备的Fragment:
那为啥不用它来承载Flutter页面呢?因为官方的封装得比较死,不利于咱们的扩展,所以咱们没有用此方法,而目前咱们使用的方式扩展性比较好,像之后的缓存策略啥的都可以进行添加。
8、RecommendFragment中来使用一下:
接下来则回到推荐Fragment来使用一下,目前它是一个java代码:
咱们将其转换成koltin,利用IDE的转换功能可以很轻松地进行转换:
此时有报错,木关系,自己稍加调整一下既可:
其中有个title文本资源:
精选推荐
一切就绪,接下来运行看一下效果:
等于就是把官方的demo给嵌入到了咱们的推荐页面上了,至此,混编的第一步已经搭建好了。
热重启/重新加载:
大家知道我们在做Flutter开发的时候,它带有热重启/重新加载的功能,但是你可能会发现,混合开发中在Android项目中集成了Flutter项目,Flutter的热重启/重新加载功能好像失效了,那怎么启用混合开发汇总的Flutter的热重启/重新加载呢?
- 打开一个模拟器,或连接一个设备到电脑上;
- 关闭我们的APP,然后运行flutter attach;
注意:如果你同时有多个模拟器或连接的设备,运行flutter attach会提示你选择一个设备:
接下来我们需要flutter attach -d 来指定一个设备:
注意:-d后面跟的设备ID。
此时运行APP你会看到:
说明连接成功了,接下为就可以通过上面的提示来进行热加载/热重启了,在终端输入:
- r:热加载;
- R:热重启;
- h:获取帮助;
- d:断开连接;
调试Dart代码:
混合开发的模式下,如何更好更高效的调试我们的代码呢?接下来就看一种混合开发模式下高效调试代码的方式:
- 关闭APP(这步很关键)
- 点击Android Studio的Flutter Attach按钮(需要首先安装Flutter和Dart插件)
- 启动APP:
接下来就可以像调试普通Flutter项目一样来调试混合开发模式下的Dart代码了。除了以上步骤不同之外,接下来的调试和我们之前所学习的Flutter调试技巧是通用的,但是还有一点需要注意:在运行Android工程时,一定要在Android模式下的AndroidStuio中运行,因为Flutter模式下的AndroidStudio运行的是Flutter module下的.android中的Android工程。
复杂场景下的Flutter混合架构设计:
概述:
通常Flutter混合设计是这样的形态:
这种是常规的,其中Flutter是一个单独的页面对吧,而像咱们目前的场景就算是一个复杂场景, 也就是:
为啥复杂呢?这是因为Flutter可以理解是一个单页面应用, 所以并不支持像咱们这种一个页面中既有native又有flutter,所以这次来学习一下在这种复杂场景下Flutter的架构设计。
实现:
优化:秒开Flutter模块:
目前我们初步在推荐模块中集成的Flutter很明显进来比较慢,感受一下:
因为我们目前是在Fragment中每次都来初始化Flutter引擎,如下:
要实现这样的效果,则需要使用预加载,但是预加载很显然会影响到首页加载的性能,所以如何让预加载不损失"首页"性能成了我们需要解决的问题,下面一个个来。
1、预加载逻辑实现:
a、HiFlutterCacheManager:
- 新建文件:
-
实现单例:
由于它会被多次调用,这里将其声明为单例,kotlin单例还知晓不?具体可以参考https://www.cnblogs.com/webor2006/p/14084038.html,咱们定义如下: - 初始化FlutterEngine:
package org.devio.`as`.proj.common.flutter import android.content.Context import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.view.FlutterMain /** * Flutter优化提升加载速度,实现秒开Flutter模块 * 0.如何让预加载不损失"首页"性能 * 1.如何实例化多个Flutter引擎并分别加载不同的dart 入口文件 */ class HiFlutterCacheManager private constructor() { /** * 初始化FlutterEngine */ private fun initFlutterEngine( context: Context, moduleName: String ): FlutterEngine { // Instantiate a FlutterEngine. val flutterEngine = FlutterEngine(context) // Start executing Dart code to pre-warm the FlutterEngine. flutterEngine.dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint( FlutterMain.findAppBundlePath(), moduleName ) ) // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment. FlutterEngineCache.getInstance().put(moduleName, flutterEngine) return flutterEngine } companion object { @JvmStatic @get:Synchronized var instance: HiFlutterCacheManager? = null get() { if (field == null) { field = HiFlutterCacheManager() } return field } private set } }
其中可以看到,缓存的key是moduleName,刚好供咱们之后具体模块的使用。
- 预加载FlutterEngine【预加载的核心逻辑】:
接下来就来提供一个预加载的方法,其中有一个小技巧值得学习:/** * 预加载FlutterEngine */ fun preLoad( context: Context ) { val messageQueue = Looper.myQueue() //在线程空闲时执行预加载任务,这样就不会和主线程进行争抢了,只有在主线程空闲时才会执行预加载 messageQueue. { initFlutterEngine(context, MODULE_NAME_FAVORITE) initFlutterEngine(context, MODULE_NAME_RECOMMEND) false } }
- 获取预加载的FlutterEngine:
/** * 获取预加载的FlutterEngine */ fun getCachedFlutterEngine(moduleName: String, context: Context?): FlutterEngine? { var engine = FlutterEngineCache.getInstance()[moduleName] if (engine == null && context != null) { engine = initFlutterEngine(context, moduleName) } return engine!! }
2、调用HiFlutterCacheManager开启预加载:
先回忆一下目前它的代码:
package org.devio.as.proj.common.ui.component;
import android.app.Application;
import com.google.gson.Gson;
import org.devio.hi.library.log.HiConsolePrinter;
import org.devio.hi.library.log.HiFilePrinter;
import org.devio.hi.library.log.HiLogConfig;
import org.devio.hi.library.log.HiLogManager;
public class HiBaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initLog();
}
private void initLog() {
HiLogManager.init(new HiLogConfig() {
@Override
public JsonParser injectJsonParser() {
return (src) -> new Gson().toJson(src);
}
@Override
public boolean includeThread() {
return true;
}
}, new HiConsolePrinter(), HiFilePrinter.getInstance(getCacheDir().getAbsolutePath(), 0));
}
}
也是java代码,将其转成kotlin,借助IDE功能,然后调用一下:
package org.devio.`as`.proj.common.ui.component
import android.app.Application
import com.google.gson.Gson
import org.devio.`as`.proj.common.flutter.HiFlutterCacheManager
import org.devio.hi.library.log.HiConsolePrinter
import org.devio.hi.library.log.HiFilePrinter
import org.devio.hi.library.log.HiLogConfig
import org.devio.hi.library.log.HiLogConfig.JsonParser
import org.devio.hi.library.log.HiLogManager
open class HiBaseApplication : Application() {
override fun onCreate() {
super.onCreate()
initLog()
//Flutter 引擎预加载,让其Flutter页面可以秒开
HiFlutterCacheManager.instance?.preLoad(this)
}
private fun initLog() {
HiLogManager.init(object : HiLogConfig() {
override fun injectJsonParser(): JsonParser {
return JsonParser { src: Any? ->
Gson().toJson(src)
}
}
override fun includeThread(): Boolean {
return true
}
}, HiConsolePrinter(), HiFilePrinter.getInstance(cacheDir.absolutePath, 0))
}
}
3、HiFlutterFragment使用HiFlutterCacheManager:
修改为:
4、修改RecommendFragment:
由于基类调整了,子类相应也得进行修改,如下:
5、改造FavoriteFragment:
为了看到效果,我们对收藏页面也进行相应的代码集成,同样由于它是Java代码,也将其改为Koltin:
其中涉及到一个字符串资源:
商品收藏
这样对于native的代码就已经改造完了,接下来则则可以来修改Flutter代码了。
6、修改flutter代码:
- 先找到flutter_module:
对于flutter代码的编写可以切到project视图,找到它:注意,它的出现,前提是一定要在这里进行配置这句话:
这样就省得在Android和Flutter之间的环境进行切换了,全在一个工程中都可以搞定了,还是非常有用的技巧。
- 新建page目录:
- 准备收藏和推荐两个页面的dart文件:
这里简单先显示一下,还没到正式编写页面逻辑的时候,因为主要是来学习Flutter多模块的混编技能,如下: - 修改main.dart【重点】:
如何实例化多个Flutter引擎并分别加载不同的dart 入口文件呢?此时就需要回到main.dart文件中来添加支持了,原本Flutter只支持一个main.dart入口的, 此时咱们要加载多个模块的dart入口,怎么办呢?此时就需要进行改造了:此时改为其中的home参数是可以动态进行widget更改的:
而其中main在我们注册Flutter引擎时意图是加载收藏模块的:
此时就会调用它了:
修改一下报错,很显然此时就是传收藏的页面:
而接下来则需要再创建一个推荐页面的入口了,依葫芦画瓢:但是!!!这样只是创建了一个recommend入口Flutter是不会加载它的, 需要向Flutter注册一下,具体方法(@pragma('vm:entry-point'))如下:
7、运行:
接下来运行看一下整体的效果:
加载速度还不错,虽说第一次打开app是要有一点加载延迟,但是之后都是秒开的,这种多模块的Flutter混编架构还是非常实用的。
Flutter与Native通信原理解析:
概述:
接下来将分场景来介绍Dart与Native之间的通信,分以下几种通信场景:
- Native发送数据给Dart;
- Dart发送数据给Native;
- Dart发送数据给Native,然后Native回传数据给Dart;
Flutter与Native通信机制:
在学习Flutter与Native之间是如何传递数据之前,我们先来了解一下Flutter与Native的通信机制,Flutter和Native的通信是通过Channel来完成的。
消息使用Channel(平台通道)在客户端(UI)和主机(平台)之间传递,如下图所示:
注意:Flutter中消息的传递是完全异步的;通信时无外乎就是传递一些数据,这些数据是有一些相关类型的约束的,下面来看一下Channel所支持的数据类型对照表:
Flutter定义了三种不同类型的Channel:
- BasicMessageChannel:用于传递字符串和半结构化的信息,持续通信,收到消息后可以回复此次消息,如:Native将遍历到的文件信息陆续传递到Dart,在比如:Flutter将从服务端陆陆续续获取到信息交给Native加工,Native处理完返回等;
- MethodChannel:用于传递方法调用(method invocation)一次性通信 ;如Flutter调用Native拍照;
- EventChannel:用于数据流(event streams)的通信,持续通,通常用于Native和Dart的通信,如:手机电量变化,网络连接变化,陀螺仪,传感器等;
这三种类型的类型的Channel都是全双工通信,既A<=>B,Dart可以主动发送消息给platform端,并且platform接收到消息后可以做出回应,同样,platform端可以主动发送消息给Dart端,dart端接收数据后返回给platform端。
通信原理:
- 无论是哪一种类型的Channel,它能和Flutter进行通信的主要是借助BinaryMessenger来实现的,BinaryMessenger也被称为Flutter和Native的一个消息信使。
- 三种类型的Channel在Flutter端都有对应的实现;
Flutter与Native通信源码分析:
接下来就以MethodChannel为例来通过原理探索Flutter与Native通信的原理。参考:https://www.imooc.com/article/286076?block_id=tuijian_wz
MethodChannel的用法:
Native端:
概述:
构造方法原型
//会构造一个StandardMethodCodec.INSTANCE类型的MethodCodec
public MethodChannel(BinaryMessenger messenger, String name)
public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec)
- BinaryMessenger messenger - 消息信使,是消息的发送与接收工具;
- String name - Channel的名称,也是其唯一的标识符;
- MethodCodec codec - 用作MethodChannel的编解码器;
setMethodCallHandler方法原型
public void setMethodCallHandler(@Nullable MethodChannel.MethodCallHandler handler)
- @Nullable MethodChannel.MethodCallHandler handler - 消息处理器,配合BinaryMessenger 完成消息的处理。
在创建好MethodChannel后,需要调用它的setMethodCallHandler方法为其设置一个消息处理器,用来接收来自flutter端的消息。
MethodChannel.MethodCallHandler原型
public interface MethodCallHandler {
void onMethodCall(MethodCall var1, MethodChannel.Result var2);
}
- onMethodCall(MethodCall var1, MethodChannel.Result var2) - 用于接收消息,call是消息内容,它有两个成员变量;String类型的call.method表示调用的方法名称,object类型的call.arguments表示调用方法所传递的入参;MethodChannel.Result是回复此消息的回调函数,提供了 void success(@Nullable Object var1); void error(String var1, @Nullable String var2, @Nullable Object var3);void notImplemented();三个方法。
整个的使用如:
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
MethodChannel methodChannel = new MethodChannel(getFlutterView(), "MethodChannel");
methodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if (methodCall.method.equals("MethodChannel")) {
String message = (String) methodCall.arguments; //获取参数
}
result.success("回复给flutter端的消息");
result.error("","","");//如果失败,回复给flutter端的消息
result.notImplemented();//表示android端没有此方法调用
}
});
}
}
源码分析:
1、构造方法:
它有两个构造方法:
其中看一下MethodCodec:
它是一个接口,看一下具体的实现:
有两种,其中还有一个Json方法编解码器。
2、setMethodCallHandler():
其中看一下MethodCallHandler:
其中看一下MethodCall这个参数的结构:
而Result参数的结构:
3、invokeMethod():
Native向Flutter来传递消息的触发方法:
而具体看一下它的实现:
其中对消息进行了编码:
另外对于结果回调,则是对Flutter返回的结果进行了解码:
Flutter端:
概述:
构造方法原型
const MethodChannel(this.name, [this.codec = const StandardMethodCodec()]);
- String name - channel的名字,要和native端保持一致;
- MethodChannel codec - 用作MethodChannel的编解码器,默认是StandardMethodCodec,要和native端保持一致;
invokeMethod方法原型
Future invokeMethod(String method, [dynamic arguments])
- String method - 要调用native的方法名;
- [dynamic arguments] - 调用native方法传递的参数,可不传;
具体使用如下:
import 'package:flutter/services.dart';//需要导入包
static const MethodChannel _methodChannel = MethodChannel('_methodChannel');
void _handleMessage() async {
try{
String respone = await _methodChannel.invokeMethod('方法名称','传递的值');
} on PlatformException catch (e) {
print(e);
}
}
源码分析:
1、构建方法:
2、invokeMethod():
其实实现逻辑跟native的差不多,发送消息时编码,得到结果时解码:
关于另外两个Channel(BasicMessageChannel、EventChannel)的具体用法可以参考大佬的文章,这里就不一一说明了,这里重点就是对MethodChannel有一个整体的认识。