目标:
在上一次https://www.cnblogs.com/webor2006/p/14522736.html已经对于Flutter有了一个整体的认识,这次则来剖析一下它的原理,以及将咱们上次实现的Flutter推荐模块集成到主工程当中来,实现Flutter和Android的一个混编,这块知识还是很值得期待的,先来纵览一下此次学习的目标:
![](https://i-blog.csdnimg.cn/blog_migrate/83de1d533ea2c956ece164168d7d7685.png)
Flutter框架和原理剖析:
Flutter框架的整体结构:
Flutter是Google推出并开源的跨平台开发框架,主打跨平台、高保真、高性能。开发者可以通过Dart语言开发Flutter应用,一套代码同时运行在ios和Android平台。不仅如此,Flutter还支持Web、桌面、嵌入应用的开发。Flutter提供了丰富的组件、接口,开发者可以很快地为Flutter添加native扩展。同时Flutter还使用skia引擎渲染视图,这无疑能为用户提供良好的体验。
下面来看一下Flutter框架的整体结构组成,在之前也看到:
![](https://i-blog.csdnimg.cn/blog_migrate/07505cad4fd806142e0a9b3e6cdeebd1.png)
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。
![](https://i-blog.csdnimg.cn/blog_migrate/36a2138c0a7312c28133af1fb356e479.png)
一般来说,计算机系统中,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给GPU,GPU渲染后放放帧缓冲区,然后视频控制器按照VSync信号从帧缓冲区取帧数据传递给显示器显示。当一帧图像绘制完毕后准备绘制下一帧时,显示器就会发出一个垂直同步信号(VSync),所以60Hz的屏幕就会一秒内发生60次这样的信号。
上面是CPU、GPU和显示器协作方式,对于Flutter也不例外,Flutter也遵循了这种模式:
![](https://i-blog.csdnimg.cn/blog_migrate/367be5bb4dda7c3b716367aa895af615.png)
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
![](https://i-blog.csdnimg.cn/blog_migrate/8b8efef724ee90d79559d94d8e61a414.png)
其中它的父类是:
![](https://i-blog.csdnimg.cn/blog_migrate/662b040172cabe2ea90f2d126e420aba.png)
BaseCanvas.java:drawCircle
![](https://i-blog.csdnimg.cn/blog_migrate/d7061451d918efa0d377942433941ff8.png)
BaseCanvas.java:nDrawCircle
![](https://i-blog.csdnimg.cn/blog_migrate/69e60f00d1ff5bf80e5b9df14cbc485b.png)
此时就进入了c++的世界了。
Canvas.cpp:drawCircle
![](https://i-blog.csdnimg.cn/blog_migrate/aa516d0b41799e17d1e0b69c354fb814.png)
SkCanvas.h:drawCircle
![](https://i-blog.csdnimg.cn/blog_migrate/4d1b43c6656861019fe9e5d321312c87.png)
由此可以看出Android UI绘制最终还是交给Skia来完成的。
Flutter渲染流程:
![](https://i-blog.csdnimg.cn/blog_migrate/2a3a1e440db087fb0ef859aa9bc75a78.png)
在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组件的生命周期:
![](https://i-blog.csdnimg.cn/blog_migrate/f500a8fee44d35979f406f5025ab2e87.png)
- createState():当框架要创建一个StatefulWidget时,它会立即调用State的createState();
- initState():当State的构造方法被执行后,会调用一次initState(),需要指出的是initState()在State生命周期内只被调用一次;
- build():这个方法会被经常调用,比如:setState以及配置改变都会触发build()方法的调用;
- didUpdateConfig():当收到一个新的config时调用;
- setState():当需要修改页面状态,比如刷新数据等的时候我们可以通过调用setState来实现;
- dispose():当移除State对象时,将调用dispose();通常在该方法中进行取消订阅,取消所有动画 ,流等操作;
FAQ:
有Flutter的应用体积为什么这么大?
![](https://i-blog.csdnimg.cn/blog_migrate/7aac59064e972ee8976870da732219f7.png)
为什么Flutter采用Dart语言做开发?
关于这块可以参考,https://www.jianshu.com/p/73a11e304ef1。
探析Flutter渲染机制之三棵树:
关于这块可以参考大神的这篇文章:https://blog.csdn.net/fengyuzhengfan/article/details/112442747,这里基于它进行一个梳理:
Flutter是一个优秀的UI框架,借助它开箱即用的Widgets我们能够构建出漂亮和高性能的用户界面。那这些Widgets到底是如何工作的又是如何完成渲染的。
所以接下来就来探析Widgets背后的故事-Flutter渲染机制之三棵树。
什么是三棵树?
![](https://i-blog.csdnimg.cn/blog_migrate/18d9565375e05606d69202c457068013.png)
在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工程中先来构建这么一个简单的示例:
![](https://i-blog.csdnimg.cn/blog_migrate/be218944288c24efd32549456590a433.png)
运行一下:
![](https://i-blog.csdnimg.cn/blog_migrate/ef7ceaf95fd2a7919ac3a0a2f15a5835.png)
此时可以打开“Flutter Inspector”:
![](https://i-blog.csdnimg.cn/blog_migrate/4d0ef42be6b8acc2f769aed0b086d9fb.png)
那第二棵树在哪里呢?此时需要跟一下源码了:
![](https://i-blog.csdnimg.cn/blog_migrate/e52f9e6a85f05a3da5267554b40e4355.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6d29db697f9a4c9105789feda597bd10.png)
总结一下就是:
当runApp()被调用时,第一时间会在后台发生以下事件:
- Flutter会构建包含这三个Widget的Widgets树;
- Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对象,最后将这些对象组建成Element树;
- 接下来会创建第三个树,这个树中包含了与Widget对应的Element通过createRenderObject()创建的RenderObject;
而整个状态过程可以用下图来描述:
![](https://i-blog.csdnimg.cn/blog_migrate/8ace5a39a17ea5ffdbec1b86e64a02b9.png)
从图中可以看出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树,接下来从源码中来体会一下:
![](https://i-blog.csdnimg.cn/blog_migrate/0a82c42067cb43ff2005cd8969ba1408.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8449bd4186c2563dd7abfceaeef75d0a.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2a34b885318c5464d70b9039b308c071.png)
![](https://i-blog.csdnimg.cn/blog_migrate/539eda0a1af50310180cfcce99edc965.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f1690412f0b596c69de8c4b3544f3877.png)
此时也是只更新对应的element,接下来继续:
![](https://i-blog.csdnimg.cn/blog_migrate/92cbd496ed62732fba7f0a99d396faef.png)
总结如下:
![](https://i-blog.csdnimg.cn/blog_migrate/92ce381a065f69fcd999bbc650cf535e.png)
因为在框架中,Element是被抽离开来的,所以你不需要经常和它们打交道。每个Widget的build(BuildContext context)方法中传递的context就是实现了BuildContext接口的Element。
更新时的三棵树:
那如果此时我们修改一下程序:
![](https://i-blog.csdnimg.cn/blog_migrate/1b24cf04daa37d01f71cbb30e174fffb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e67724171b50b4bdc9e714a14d3a1a3a.png)
因为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的类型发生改变时:
![](https://i-blog.csdnimg.cn/blog_migrate/90278cbbbc62d3c801a98dee26fd4062.png)
![](https://i-blog.csdnimg.cn/blog_migrate/41003f72fe19bd7991386581888629ab.png)
![](https://i-blog.csdnimg.cn/blog_migrate/938e37ac0eb393d5b33ab48ee8418fe7.png)
和刚才流程一样,Flutter会从新Widget树的顶端向下遍历,与原有树中的Widget类型进行对比。
![](https://i-blog.csdnimg.cn/blog_migrate/46159829036efbc5c781f988f96f2fce.png)
因为FlatButton的类型与Element树中相对应位置的Element的类型不同,Flutter将会从各自的树上删除这个Element和相对应的ContainerRender,然后Flutter将会重建与FlatButton相对应的Element和RenderObject。如下:
![](https://i-blog.csdnimg.cn/blog_migrate/c5f09da25189937794396aedba06beda.png)
很明显这个重新创建的过程相对耗时的,但是当新的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页面
![](https://i-blog.csdnimg.cn/blog_migrate/5285917fa8765cdb0eb027ce0b0f2563.png)
也就是从原生页面项中点击会跳到一个Flutter页面。
场景二:在原生页面中嵌入Flutter模块
![](https://i-blog.csdnimg.cn/blog_migrate/a8adbc41a72e7659d05fd6ce0442813b.png)
如上图,其Flutter只是做为原生的一部分出现。
场景三:Flutter页面中嵌入原生模块
![](https://i-blog.csdnimg.cn/blog_migrate/e7bdeb7fc329f27b4dc4ab7fa40b7e84.png)
也就是在Flutter页面中有局部是嵌入的原生模块。
以上这些都属于Flutter混合开发的范畴,那么如何将Flutter集成到现有的Android应用中呢?
主要步骤:
将Flutter集成到现有的Android应用中需要如下几个主要步骤:
- 首先,创建Flutter module;
- 为已存在的Android应用添加Flutter module依赖;
- 在Koltin中调用Flutter module;
- 编写Dart代码;
- 运行项目;
- 热重启/重新加载;
- 调试Dart代码;
- 发布应用;
- 升职加薪、迎娶白富美,走向人生巅峰!
提示:为了能够顺利的进行Flutter混合开发,请确认你的项目结构是这样子的:
![](https://i-blog.csdnimg.cn/blog_migrate/d0c7281c353d6a696c569fefa530e4d2.png)
flutter_hybird下面分别是flutter模块、原生Android模块、原生IOS模块,并且这三个模块是并列结构。
创建Flutter module:
概述:
在做混合开发之前我们首先需要创建一个Flutter module。假如你的Native项目是这样的:xxx/flutter_hybird/Native项目:
![](https://i-blog.csdnimg.cn/blog_migrate/1b6c296c8ceb8cd53efd1d591af02460.png)
上面代码会切换到你的Android/Ios目录的上一级目录,并创建一个flutter模块:
![](https://i-blog.csdnimg.cn/blog_migrate/c3da2cfa4821b34c551b1fe1e15c24f1.png)
上面是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模块是需要在这里进行创建的:
![](https://i-blog.csdnimg.cn/blog_migrate/8a7bc21b26cd7a7db1ddf65fa081ecee.png)
而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.
此时就创建好了:
![](https://i-blog.csdnimg.cn/blog_migrate/ecb1fbdca7a9f993e36eb9db37066fe1.png)
打开看一下它的目录结构:
![](https://i-blog.csdnimg.cn/blog_migrate/e082a7b5a4002ab69dbe6e2bdc2f0b03.png)
其中具体目录中的含义在上面已经阐述过了,这里就不过多说明了。
为已存在Android应用添加Flutter module依赖:
接下来就需要将创建的Flutter module依赖到我们的主工程,有如下两种方式可以依赖:
方式一:构建flutter aar(非必须):
如果需要将咱们的flutter module以aar的方式来集成到Android工程中,可以用如下命令:
![](https://i-blog.csdnimg.cn/blog_migrate/bb82a6b2ed935888138d230a75d45658.png)
也就是进入这个目录下来执行上面的命令:
![](https://i-blog.csdnimg.cn/blog_migrate/7d0de6a9c6a812422c98085ed2eaf187.png)
但是这里不采用此方法。
方式二:添加依赖
阐述:
这种方式更加的顺畅,直接打开Android项目的settings.gradle来添加如下代码进行依赖:
![](https://i-blog.csdnimg.cn/blog_migrate/e91597f7f7c0c0eca8d914e2aa754605.png)
其中setBinding与evaluate允许flutter模块包含它自己在内的任何Flutter插件,在settings.gradle中以类似:":flutter、package_info、:video_player"的方式存在;
实践:
1、下面咱们来到咱们的主工程中来配置一下:
![](https://i-blog.csdnimg.cn/blog_migrate/bad06272d4391dbd17c3dab7db87aed6.png)
其配置信息如下:
//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依赖:
![](https://i-blog.csdnimg.cn/blog_migrate/b92fb68b9822bdbcbc05d8086598597d.png)
这里则需要在我们需要使用的地方来添加依赖,由于咱们工程中很多地方都需要用到它,所以可以将其放到common module中,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8970e102802a5a3513948686aed5ae9f.png)
注意这里不传递依赖,只在需要使用的页面上进行使用,另外由于咱们还要在这个页面中进行使用:
![](https://i-blog.csdnimg.cn/blog_migrate/157e26c4bd22832a4ec45d05acfef9ea.png)
所以,在这个module中也添加flutter的依赖:
![](https://i-blog.csdnimg.cn/blog_migrate/a1729cf30b523913cc2fd0f025e6563c.png)
3、添加Java8编译选项:
由于Flutter的Android engine使用了Java 8 的特性,所以在引入Flutter时需要配置你的项目的java 8 编译选项,在app的build.gradle文件的android{}节点下添加:
![](https://i-blog.csdnimg.cn/blog_migrate/d81d4d77e5ffbe183ddf7f374027f75a.png)
这块咱们已经添加过了,就不多说了。
在Koltin中调用Flutter module
至此,我们已经为我们的Android项目添加了Flutter所必须的依赖,接下来我们来看如何在项目上以Kotlin的方式在Fragment中调用Flutter模块:
1、创建HiFlutterFragment:
![](https://i-blog.csdnimg.cn/blog_migrate/c1c6cd92d15b32b3beddda889df9cf90.png)
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页面布局:
![](https://i-blog.csdnimg.cn/blog_migrate/1cdc09f9dc917835ea399c20c9e7a66e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2fd77dc1d588c1ae46f9927f22fd822e.png)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/rl_title"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_45"
android:background="@color/color_white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@color/color_000"
android:textSize="16sp" />
</RelativeLayout>
<View
android:id="@+id/title_line"
android:layout_width="match_parent"
android:layout_height="2px"
android:background="@color/color_eee"
android:visibility="gone" />
</LinearLayout>
其中可以看到布局都是原生的元素,无任何flutter相关的元素。
4、setTitle():
![](https://i-blog.csdnimg.cn/blog_migrate/4e9f6f889b42dab20c639d1692c75c92.png)
5、在布局中添加Flutter View:
目前布局很显然不能承载Flutter,所以接下来动态创建一下它:
![](https://i-blog.csdnimg.cn/blog_migrate/ca05a53a81e97cf657348b7180340b6d.png)
其中有一点需要稍加解释一下,就是图中注释的部分:
![](https://i-blog.csdnimg.cn/blog_migrate/db011922eeadf9a866d8a4850835b7c9.png)
渲染FlutterView的两种方式,除了现在在用的FlutterTextureView之外,还有它:
![](https://i-blog.csdnimg.cn/blog_migrate/d96d778920af6da4ea877fcba8a2c29b.png)
但是这里不采用它的原因是由于当app切后台这种View是会有复用的问题,比如:
![](https://i-blog.csdnimg.cn/blog_migrate/aa02ef6ce1f2ab097aa4ceac344f1024.png)
此时将app切到后台,然后再切到前台时,推荐的FlutterView会被复用到收藏的FlutterView上的,很显然是不对的,所以这一点需要明确。
6、重写各种生命周期:
为了能让Flutter感知到各个生命周期,所以需要重写一系列的方法,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8b5727ff2ef596eaaa565ae5e30dc799.png)
这样Flutter就能感知Fragment的生命周期了。
7、为啥不用FlutterFragment?
对于Flutter而言其实它有一个系统为咱们准备的Fragment:
![](https://i-blog.csdnimg.cn/blog_migrate/8a892b1d362be5aa787d195b09cedeba.png)
那为啥不用它来承载Flutter页面呢?因为官方的封装得比较死,不利于咱们的扩展,所以咱们没有用此方法,而目前咱们使用的方式扩展性比较好,像之后的缓存策略啥的都可以进行添加。
8、RecommendFragment中来使用一下:
接下来则回到推荐Fragment来使用一下,目前它是一个java代码:
![](https://i-blog.csdnimg.cn/blog_migrate/2ccde948d08cb7abb576764c1f4c87bf.png)
咱们将其转换成koltin,利用IDE的转换功能可以很轻松地进行转换:
![](https://i-blog.csdnimg.cn/blog_migrate/785e9161f844ab7554236b2ddda03a6d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/659bcc419bf9992f912eddf957e96eae.png)
此时有报错,木关系,自己稍加调整一下既可:
![](https://i-blog.csdnimg.cn/blog_migrate/918bca04200e7ef99a94d71f905efad3.png)
其中有个title文本资源:
<string name="title_recommend">精选推荐</string>
一切就绪,接下来运行看一下效果:
![](https://i-blog.csdnimg.cn/blog_migrate/b279ae90739412381153c2ba7c618b27.png)
等于就是把官方的demo给嵌入到了咱们的推荐页面上了,至此,混编的第一步已经搭建好了。
热重启/重新加载:
![](https://i-blog.csdnimg.cn/blog_migrate/77d010e4524072c29037d3ae6c4c14a7.png)
大家知道我们在做Flutter开发的时候,它带有热重启/重新加载的功能,但是你可能会发现,混合开发中在Android项目中集成了Flutter项目,Flutter的热重启/重新加载功能好像失效了,那怎么启用混合开发汇总的Flutter的热重启/重新加载呢?
- 打开一个模拟器,或连接一个设备到电脑上;
- 关闭我们的APP,然后运行flutter attach;
![](https://i-blog.csdnimg.cn/blog_migrate/41a1db019308cf88df18c6d1f516e344.png)
注意:如果你同时有多个模拟器或连接的设备,运行flutter attach会提示你选择一个设备:
![](https://i-blog.csdnimg.cn/blog_migrate/56b0798c264fb3cc1ec8a33de519d6e1.png)
接下来我们需要flutter attach -d 来指定一个设备:
![](https://i-blog.csdnimg.cn/blog_migrate/e4ea6258028af2d54ded1b03b74d24cb.png)
注意:-d后面跟的设备ID。
此时运行APP你会看到:
![](https://i-blog.csdnimg.cn/blog_migrate/cd0ba6565f249fc6ad3e10ec6d3b665a.png)
说明连接成功了,接下为就可以通过上面的提示来进行热加载/热重启了,在终端输入:
- 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混合设计是这样的形态:
![](https://i-blog.csdnimg.cn/blog_migrate/709180049b8709cf9dfbc11a7b205299.png)
这种是常规的,其中Flutter是一个单独的页面对吧,而像咱们目前的场景就算是一个复杂场景, 也就是:
![](https://i-blog.csdnimg.cn/blog_migrate/5459fc57f2ea5609e8f8d393e6df2219.png)
为啥复杂呢?这是因为Flutter可以理解是一个单页面应用, 所以并不支持像咱们这种一个页面中既有native又有flutter,所以这次来学习一下在这种复杂场景下Flutter的架构设计。
实现:
优化:秒开Flutter模块:
目前我们初步在推荐模块中集成的Flutter很明显进来比较慢,感受一下:
![](https://i-blog.csdnimg.cn/blog_migrate/e0e8242d16249dffe36edcea01729349.gif)
因为我们目前是在Fragment中每次都来初始化Flutter引擎,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/358c7a22395a205a4af0be27c0b7b329.png)
要实现这样的效果,则需要使用预加载,但是预加载很显然会影响到首页加载的性能,所以如何让预加载不损失"首页"性能成了我们需要解决的问题,下面一个个来。
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开启预加载:
![](https://i-blog.csdnimg.cn/blog_migrate/f24224dc09f3c84bfbcdeb822568bce3.png)
先回忆一下目前它的代码:
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:
![](https://i-blog.csdnimg.cn/blog_migrate/a4f0d956447d3a701cd40219ab35d116.png)
修改为:
![](https://i-blog.csdnimg.cn/blog_migrate/e853d9f763fe3d429c0320fef59e73c1.png)
4、修改RecommendFragment:
由于基类调整了,子类相应也得进行修改,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/ef01d35859502105f31c890d1eef6edc.png)
5、改造FavoriteFragment:
为了看到效果,我们对收藏页面也进行相应的代码集成,同样由于它是Java代码,也将其改为Koltin:
![](https://i-blog.csdnimg.cn/blog_migrate/93fdb78ec548b7365aeb95f42cba4455.png)
![](https://i-blog.csdnimg.cn/blog_migrate/389f46805242e53d5bb99ea7c587ea2e.png)
其中涉及到一个字符串资源:
<string name="title_favorite">商品收藏</string>
这样对于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、运行:
接下来运行看一下整体的效果:
![](https://i-blog.csdnimg.cn/blog_migrate/0895d446e2780aded2df69fe51ea5142.gif)
加载速度还不错,虽说第一次打开app是要有一点加载延迟,但是之后都是秒开的,这种多模块的Flutter混编架构还是非常实用的。
Flutter与Native通信原理解析:
概述:
接下来将分场景来介绍Dart与Native之间的通信,分以下几种通信场景:
- Native发送数据给Dart;
- Dart发送数据给Native;
- Dart发送数据给Native,然后Native回传数据给Dart;
![](https://i-blog.csdnimg.cn/blog_migrate/e102eae0f79b37720c8fcc916d6344f1.png)
Flutter与Native通信机制:
在学习Flutter与Native之间是如何传递数据之前,我们先来了解一下Flutter与Native的通信机制,Flutter和Native的通信是通过Channel来完成的。
消息使用Channel(平台通道)在客户端(UI)和主机(平台)之间传递,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/984efcd8c9cf134c387fbf5ada90687f.png)
注意:Flutter中消息的传递是完全异步的;通信时无外乎就是传递一些数据,这些数据是有一些相关类型的约束的,下面来看一下Channel所支持的数据类型对照表:
![](https://i-blog.csdnimg.cn/blog_migrate/2011e6789009902c2494d8adb6716bb8.png)
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端。
通信原理:
![](https://i-blog.csdnimg.cn/blog_migrate/1f1782a889063ea9e94d0dd11be78fc2.png)
- 无论是哪一种类型的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、构造方法:
它有两个构造方法:
![](https://i-blog.csdnimg.cn/blog_migrate/7f9c9cd0641952a79f4b42fea817f81c.png)
其中看一下MethodCodec:
![](https://i-blog.csdnimg.cn/blog_migrate/968d079c6d155581d8e32c14860411e6.png)
它是一个接口,看一下具体的实现:
![](https://i-blog.csdnimg.cn/blog_migrate/e39f27709fce4b807e98a8b028f991fe.png)
有两种,其中还有一个Json方法编解码器。
2、setMethodCallHandler():
![](https://i-blog.csdnimg.cn/blog_migrate/d0d4fe7422e6f9e59ccaa99a4d7a9cc9.png)
其中看一下MethodCallHandler:
![](https://i-blog.csdnimg.cn/blog_migrate/c080ac425af0eca7edbc9f6f993abfb1.png)
其中看一下MethodCall这个参数的结构:
![](https://i-blog.csdnimg.cn/blog_migrate/5ce4047c247d1207642bbf04ee50a8b6.png)
而Result参数的结构:
![](https://i-blog.csdnimg.cn/blog_migrate/07e7bbc695c238421c295ddd5c3be5fc.png)
3、invokeMethod():
Native向Flutter来传递消息的触发方法:
![](https://i-blog.csdnimg.cn/blog_migrate/f3a26a3723a68b90004712877a69ade8.png)
而具体看一下它的实现:
![](https://i-blog.csdnimg.cn/blog_migrate/7bb2b241a9bf67e28a0736b336123adb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/027aea19afe12446f3aa810ba58b774c.png)
其中对消息进行了编码:
![](https://i-blog.csdnimg.cn/blog_migrate/302c7fe3abc1752258b085fe21fc1053.png)
另外对于结果回调,则是对Flutter返回的结果进行了解码:
![](https://i-blog.csdnimg.cn/blog_migrate/6a24b0fef8ef095990726e167670b504.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d106912b5cadf6c52aef5332d606c7c3.png)
Flutter端:
概述:
构造方法原型
const MethodChannel(this.name, [this.codec = const StandardMethodCodec()]);
- String name - channel的名字,要和native端保持一致;
- MethodChannel codec - 用作MethodChannel的编解码器,默认是StandardMethodCodec,要和native端保持一致;
invokeMethod方法原型
Future<T> invokeMethod<T>(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、构建方法:
![](https://i-blog.csdnimg.cn/blog_migrate/b9180b6e360ec5a9510668ee1b3215b5.png)
2、invokeMethod():
![](https://i-blog.csdnimg.cn/blog_migrate/25d6262638b6698982c24028e7c21ef5.png)
其实实现逻辑跟native的差不多,发送消息时编码,得到结果时解码:
![](https://i-blog.csdnimg.cn/blog_migrate/4b1e0600bd95415359d3c66e51680eb0.png)
关于另外两个Channel(BasicMessageChannel、EventChannel)的具体用法可以参考大佬的文章,这里就不一一说明了,这里重点就是对MethodChannel有一个整体的认识。