页面中的各界面元素(Widget)以树的形式组织,即控件树。Flutter通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。而渲染对象树在Flutter的展示过程分为三个阶段:布局、绘制、合成和渲染。
创新互联是一家专注于成都网站建设、网站设计与策划设计,自流井网站建设哪家好?创新互联做网站,专注于网站建设十年,网设计领域的专业建站公司;建站业务涵盖:自流井等地区。自流井做网站价格咨询:18982081108
(一)布局
Flutter采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。
为了防止因子节点发生变化而导致整个控件树重新布局,Flutter加入了一个机制——布局边界(Relayout Boundary),可以在某些节点自动或手动地设置布局边界,当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。
二)绘制
布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。
以下图为例:节点1在绘制完自身后,会再绘制节点2,然后绘制它的子节点3、4和5,最后绘制节点6。
可以看到,由于一些其他原因(比如,视图手动合并)导致2的子节点5与它的兄弟节点6处于了同一层,这样会导致当节点2需要重绘的时候,与其无关的节点6也会被重绘,带来性能损耗。
为了解决这一问题,Flutter提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘。
重绘边界的一个典型场景是Scrollview。ScrollView滚动的时候需要刷新视图内容,从而触发内容重绘。而当滚动内容重绘时,一般情况下其他内容是不需要重绘的,这时候重绘边界就派上用场了。
(三)合成和渲染
终端设备的页面越来越复杂,因此Flutter的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。
合并完成后,Flutter会将几何图层数据交由Skia引擎加工成二维图像数据,最终交由GPU进行渲染,完成界面的展示。
四、总结
咱们从各种业界主流跨端方案与Flutter的对比开始,到Flutter的简要介绍以及Flutter的运行机制,并以界面渲染过程为例,从布局、绘制、合成和渲染三个阶段讲述了Flutter的实现原理。相信大家对Flutter已经有一个整体认知,赶快一起上手操作起来吧!
Dart作为高级语言,支持面向对象的很多特性,并且支持基于mixin的继承方式,基于mixin的继承方式是指:一个类可以继承自多个父类,相当于其他语言里的多继承。所有的类都有同一个基类Object,这和特性类似于Java语言,Java所有的类也都是继承自Object,也就是说一切皆对象。
Dart 是一门面向对象的语言, 全部的类都是继承自 Object , 除了支持传统的 继承、封装、多态 , 还有基于组合(Mixin-based)的继承特性
类型推导(var/final/const)
var
final和const的区别
3.非零即真( )
4.字符串
5.集合
Dart中变量初始值为null,即使是int类型也可以是null(java中int默认是0, boolean默认是false); Dart支持自识别,可以是用var定义变量,也可以直接指定具体类型; final或者const都可修饰不可变的变量,final变量只能赋值一次,const是编译时常量。
int和double是num子类,没有float类型; 支持字符串模板,用${expression}的方式来实现字符串效果,类似如字符串拼接; String可以使用单引号或者双引号; Dart没有数组,只有列表; 其中List,Set,Map不是抽象接口,是具体实现类,可直接使用; Map的key没有指定类型,key类型不一致不会报错;key不能相同,但是value可以相同,value可以为null。 var name = 'Tom';
方法也是对象,方法可赋值给一个变量; 如果方法的参数是解构出来的可以通过 @required 注解标注为必填 const Scrollbar({Key key, @required Widget child}); 支持可选参数,可选命名参数用{}包围,可选位置参数写在最后并且使用[]包围 String say(String from, String msg, [String device]); 支持默认参数 void enableFlags({bool bold = false, bool hidden = false}) {…}; 以_开头的方法都是私有的。 void main() {
支持闭包,闭包能够访问外部方法内部的局部变量
1.空替换?? expr1 ?? expr2,如果expr1是non-null,返回其值。否则执行expr2并返回其结果; 2.条件成员访问?.P?.y = 4; 如果p是non-null,则设置y的值等于4; 3.类型判定操作符(as,is,is!); 4.级联操作,可以在同一个对象上连续调用多个函数以及访问成员变量;
和java不同的是,Dart可以抛出任意类型的对象; 程序不会强制要求开发中处理异常,但若发生异常,程序会中断; 其中异常主要分为Error和Exception两种类型。
创建对象可以不使用new关键字; Dart中没有public,private,protected这些关键字; 没有interfaces关键字,每一个类都是一个接口。我们可以用抽象类来类比java中的接口; Dart把多重继承的类叫做Mixins。
支持语法糖 Point(this.x, this.y); 每个实例变量都会自动生成一个getter方法,Non-final变量还会自动生成一个setter; 命名构造函数,使用命名构造函数可以为一个类实现多个构造函数,也能更加清晰的表明你的意图;
断言是如果条件表达式不满足则停止代码执行; 断言只在检查模式下运行有效,如果在生产模式下运行则不会执行。
Dart 以两种模式运行: Dart 1.x 有生产模式和检查模式两种模式, Dart 2.x 中移除了检查模式。
注:建议在开发/测试模式中使用 检查模式 运行 Dart VM ,因为它会添加警告和错误以帮助开发和调试过程;选中的模式会强制执行各种检查,例如类型检查等。
dart标识符可以包括字符和数字,但不能以 数字开头 。
Dart 是一种面向对象的编程语言。
代码说明:定义了一个类 TestClass ,这个类拥有一个方法 disp() ,方法可以实现在终端打印字符串 Hello Dart! ,使用 new 关键字创建类的对象,该对象调用方法 disp() 。
关于dart的学习还有很多;我列出如下: Flutter高级工程师进阶学习资料;需要可以私信我。发送“核心笔记”或“手册”,即可领取资料!
Flutter可以算是当下最火热的新技术之一,我现在所在团队也准备将Flutter技术应用到线上工程中。
关于混合工程,官方文档其实写的已经比较清楚了,按着文档走一般问题不大,
但是有一点值得注意的是,Flutter工程引入的库的gradle的 buildTypes 要与原工程保持一致,如果不一致需要手工添加。
进入正题,现在Flutter官方默认只提供armeabi-v7a、arm64-v8a、x86和x86-64,其中x86和x86-64是为模拟器准备的。目前我们使用的SDK大部分只使用了armeabi架构,直接使用我们会遇见找不到 libflutter.so,libapp.so 的情况,所以我们需要对FlutterSDK做一定的改造。
首先我们要了解下Flutter编译产物,因为不同版本产物是不同的,这里我们只针对Flutter 1.9.1-hotfixes来说。除了资源文件之外,Flutter打包会生成两个非常重要的so库,他们分别是 libflutter.so,libapp.so 。其中 libflutter.so 是Flutter的SDK产物而 libapp.so 正是我们编写的dart文件的产物。默认情况下,这两个文件都会出现在armeabi-v7a中,因此我们要作出对应的改造。
libflutter.so 位于FlutterSDK中,这里顺带提一句,除了这对不同CPU架构,它还分为Debug版和Release版,它们的区别在于Debug是为JIT编译方式打造的,体积较大而Release是为AOT编译方式打造的,体积很小。对 libflutter.so 的改造,只要将其移动文件路径即可,运行以下脚本即可,此脚本来自美团分享的Flutter文章。
移动完了 libflutter.so 之后我们打包发现, libapp.so 仍然会出现在armeabi-v7a中,所以第二部我们就是移动 libapp.so 。这个需要更改 flutter.gradle ,我们在 flutter.gradle 的45行可以看到如下定义,它定义了我们的环境。
在524行我们可以看到,abiValue的取值就是根据上述定义值。
所以结论很简单,只要将
private static final String ARCH_ARM32 = "armeabi-v7a";
改为
private static final String ARCH_ARM32 = "armeabi";
就可以完成对与 libflutter.so 的移动。
前期工作我们都做好了,打成aar就非常简单了
直接使用 flutter build aar --target-platform android-arm
打出来后可以解压检查下 libflutter.so,libapp.so 是否都在armeabi文件夹下即可。
说完了armeabi适配问题,这里下说下有关于有关于FlutterBoost的接入。这个东西接入有两点要注意。
在主app内加上即可,常规操作,强制统一support包的版本号
注释flutter.gradle第655行。因为编译过程中,会去初始化插件项目的buildType下面的debug配置,而插件项目下并未配置debug,导致报错。
如果发现文章中有错误或者有更好的解决方案欢迎指正留言,当然如果本篇文章帮助你解决了问题,也不要吝啬你的感谢。谢谢各位。
APP 启动页在国内是最常见也是必备的场景,其中启动页在 iOS 上算是强制性的要求,其实配置启动页挺简单,因为在 Flutter 里现在只需要:
一般只要配置无误并且图片尺寸匹配,基本上就不会有什么问题, 那既然这样,还有什么需要适配的呢?
事实上大部分时候 iOS 是不会有什么问题, 因为 LaunchScreen.storyboard 的流程本就是 iOS 官方用来做应用启动的过渡;而对于 Andorid 而言,直到 12 之前 windowBackground 这种其实只能算“民间”野路子 ,所以对于 Andorid 来说,这其中就涉及到一个点:
所以下面主要介绍 Flutter 在 Android 上为了这个启动图做了哪些骚操作~
在已经忘记版本的“远古时期” , FlutterActivity 还在 io.flutter.app.FlutterActivity 路径下的时候,那时启动页的逻辑相对简单,主要是通过 App 的 AndroidManifest 文件里是否配置了 SplashScreenUntilFirstFrame 来进行判断。
在 FlutterActivity 内部 FlutterView 被创建的时候,会通过读取 meta-data 来判断是否需要使用 createLaunchView 逻辑 :
是不是很简单,那就会有人疑问为什么要这样做?我直接配置 Activity 的 android:windowBackground 不就完成了吗?
这就是上面提到的时间差问题, 因为启动页到 Flutter 渲染完第一帧画面中间,会出现概率出现黑屏的情况,所以才需要这个行为来实现过渡 。
经历了“远古时代”之后, FlutterActivity 来到了 io.flutter.embedding.android.FlutterActivity , 在到 2.5 版本发布之前,Flutter 又针对这个启动过程做了不少调整和优化,其中主要就是 SplashScreen 。
自从开始进入 embedding 阶段后, FlutterActivity 主要用于实现了一个叫 Host 的 interface ,其中和我们有关系的就是 provideSplashScreen 。
默认情况下它会从 AndroidManifest 文件里是否配置了 SplashScreenDrawable 来进行判断 。
默认情况下当 AndroidManifest 文件里配置了 SplashScreenDrawable ,那么这个 Drawable 就会在 FlutterActivity 创建 FlutterView 时被构建成 DrawableSplashScreen 。
DrawableSplashScreen 其实就是一个实现了 io.flutter.embedding.android.SplashScreen 接口的类,它的作用就是:
之后 FlutterActivity 内会创建出 FlutterSplashView ,它是个 FrameLayout。
FlutterSplashView 将 FlutterView 和 ImageView 添加到一起, 然后通过 transitionToFlutter 的方法来执行动画,最后动画结束时通过 onTransitionComplete 移除 splashScreenView 。
所以整体逻辑就是:
当然这里也是分状态:
当然这个阶段的 FlutterActivity 也可以通过 override provideSplashScreen 方法来自定义 SplashScreen 。
看到没有,做了这么多其实也就是为了弥补启动页和 Flutter 渲染之间, 另外还有一个优化,叫 NormalTheme 。
通过该配置 NormalTheme ,在 Activity 启动时,就会首先执行 switchLaunchThemeForNormalTheme(); 方法将主题从 LaunchTheme 切换到 NormalTheme 。
大概配置完就是如下样子, 前面分析那么多其实就是为了告诉你,如果出现问题了,你可以从哪个地方去找到对应的点 。
讲了那么多, Flutter 2.5 之后 provideSplashScreen 和 io.flutter.embedding.android.SplashScreenDrawable 就被弃用了,惊不喜惊喜,意不意外,开不开心 ?
通过源码你会发现,当你设置了 splashScreen 的时候,会看到一个 log 警告:
为什么会弃用?
其实这个提议是在 这个 issue 上,然后通过 这个 pr 完成调整。
大概意思就是: 原本的设计搞复杂了,用 OnPreDrawListener 更精准,而且不需要为了后面 Andorid12 的启动支持做其他兼容,只需要给 FlutterActivity 等类增加接口开关即可 。
也就是2.5之后 Flutter 使用 ViewTreeObserver.OnPreDrawListener 来实现延迟直到加载出 Flutter 的第一帧。
为什么说默认情况? 因为这个行为在 FlutterActivity 里,是在 getRenderMode() == RenderMode.surface 才会被调用,而 RenderMode 又和 BackgroundMode 有关心 。
所以在 2.5 版本后, FlutterActivity 内部创建完 FlutterView 后就会执行一个 delayFirstAndroidViewDraw 的操作。
这里主要注意一个参数: isFlutterUiDisplayed 。
当 Flutter 被完成展示的时候, isFlutterUiDisplayed 就会被设置为 true。
所以当 Flutter 没有执行完成之前, FlutterView 的 onPreDraw 就会一直返回 false ,这也是 Flutter 2.5 开始之后适配启动页的新调整。
看了这么多,大概可以看到其实开源项目的推进并不是一帆风顺的,没有什么是一开始就是最优解,而是经过多方尝试和交流,才有了现在的版本,事实上开源项目里,类似这样的经历数不胜数:
ChangeNotifierProvider、ChangeNotifier、Consumer的关系
1、ChangeNotifierProvider的父类ListenableProvider,ListenableProvider中实现了_startListening方法,_startListening主要是将Element的刷新方法添加到ChangeNotifier中的_listeners中
2、ListenableProvider将startListening传入其父类InheritedProvider,InheritedProvider主要是创建delegate = _CreateInheritedProvider。这个delegate就是_CreateInheritedProvider中delegate.startListening中的delegate
1、ChangNotifier负责更新UI
ChangeNotifier中notifyListeners,通过遍历_listeners,实现强制刷新UI
1、Consumer:包裹待刷新UI,在buildWithChild中将Provider.ofT(context)传入builder方法
2、解析 Provider.ofT(context)
Provider.ofT(context)是获取ChangeNotifier对象的方法
Provider.of中通过传入的context,获取父视图为inheritedElement的对象
获取到inheritedElement,通过inheritedElement.getValue的方式获取ChangeNotifier对象
7、继续查看inheritedElement?.value。通过断点可以进入_CreateInheritedProvider 类中get Value方法。调用startListening将当前Consumer包裹的element添加到Prorivder的_listeners数组中
通过调用notifyListeners()来刷新所有Consumer完整的Provider执行流程大概就是这样,流程图如下
最近项目中要集成flutter来进行混编,但是在集成后,突然遇到一个很神奇的问题,在debug模式下,用数据线连接真机打包可以打开flutter页面,但是一旦拔掉数据线,再打开flutter页面就不行了,开始以为是因为flutterSDK的原因,但是一查资料才发现,原来是因为debug模式下flutter实现了热重载,默认的编译方式是JIV,但是iOS14+之后的系统限制了JIV这种编译方式,所以连接Xcode重新run一个release包就可以了,因为flutter在release模式下的编译方式是AOT,iOS14+的系统是支持这种编译方式的,具体解决方案如下图
再运行就可以了。
当然还有另外一种解决方案,就是修改flutter的编译配置,强制设为release