webor2006

混合架构设计与开发<十九>-------Flutter混合架构原理剖析与应用1【框架和原理剖析、复杂场景下的Flutter混合架构设计、Flutter与Native通信原理剖析】

目标:

在上一次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页面布局:

 

<?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():

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文本资源:

<string name="title_recommend">精选推荐</string>

一切就绪,接下来运行看一下效果:

 

等于就是把官方的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:

其中涉及到一个字符串资源:

<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、运行:

 接下来运行看一下整体的效果:

加载速度还不错,虽说第一次打开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<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、构建方法:

2、invokeMethod():

其实实现逻辑跟native的差不多,发送消息时编码,得到结果时解码:

关于另外两个Channel(BasicMessageChannel、EventChannel)的具体用法可以参考大佬的文章,这里就不一一说明了,这里重点就是对MethodChannel有一个整体的认识。

分类:

技术点:

相关文章: