Flutter 基础知识查漏补缺

Hot reload原理

热重载分为这几个步骤

  • 扫描项目改动:检查是否有新增,删除或者改动,直到找到上次编译后发生改变的dart代码
  • 增量编译:找到改变的dart代码,将其转化为增量支持动态编译的dart kernel
  • 推送更新:热重载模块将增量更新的代码通过HTTP端口发送到在虚拟机上的Dart VM
  • 代码合并:Dart Vm收到增量的dart kernel代码,将其与原有的dart vm代码合并,并加载新的dart kernel代码
  • widget重建:在确认dart vm资源加载成功后,flutter会将Ui线程重置,通知flutter framework重建widget

Hot reload是debug下的JIT(Just In Time)动态编译模式,dart代码会被编译成可以在dart VM上的Dart kernel中间代码,而Dart kernel代码是支持动态更新的。

JIT由于包括了很多debug工具和中间层代码,所以性能没有AOT(Ahead Of Time)编译模式好,但是AOT编译需要花费大量时间,适合release版本,JIT虽然性能没有那么好,但是支持动态编译,所以适合debug模式

var、dynamic、final、const的区别

  • var在创建时会推断变量的类型,并且确定之后无法再更改,dynamic是动态类型,可以随时更改类型,如下
//A value of type 'int' can't be assigned
to a variable of type 'String'.
var a = 'String';
a = 1;
//成功运行
dynamic b = 'String';
b = 12;
  • final是运行时常量,类型会在运行时确定,且只能赋值一次,const是编译时常量,类型在编译时确定,无法被赋值
//The final variable 'b' can only
be set once.
final b = '';
b = '';
//Constant variables can't be
assigned a value.
const a = '';
a = '';

??和??=的区别

?? 是给A赋值给B,如果A为null,就将??后面的值赋给B

??= 是如果A为空就将??=后面的值赋值给A

String? A;
String B = A ?? 'B';
A ??= 'A';

Dart中是值传递还是引用传递

基本类型值传递,类是引用传递

Widget 、 Element 、Render Object三者之间的联系

简单说一说,后面再继续总结

  • Widget是“描述一个UI元素的配置信息”,并不是最终绘制在屏幕上的显示元素,所谓的配置信息就是Widget接收的参数,比如一个Text的配置信息就是文本内容,对齐方式,文本样式等。Widget有一些方法,比较重要的是@immutable,代表不可变的(final),因为Flutter中如果发生属性改变机会重新构建Widget,会创建新的Widget实例替代老的Widget实例,所以Widget中的变量如果可变的话就没有任何意义。还有canUpdate方法,其作用是是否用新的Widget对象去更新旧UI树上Element对象的配置,如果新老Widget的runtimeType和key相同会返回true。Widget还有一个createElement方法来创建Element对象。
  • 当新建一个Widget时,会创建一个Element对象,Element树的节点都继承自Element。
  • 然后根据Element树生成Render树,也就是渲染树,渲染树的节点都继承自RenderObject。
  • 根据渲染树生成Layer树,Layer树的节点都继承自Layer类。
  • 而真正的绘制,渲染逻辑都是由渲染树完成。Element可以说是Widget和RenderObject的粘合剂。是Widget在整个UI树中的实例,UI树是由一个个独立的Element节点构成。Widget ==> Element ==> RenderObject。Element树根据Widget树生成,Render树又依赖于Element树。我们一般不会直接操纵Element,Flutter框架已经将Widget树映射到Element树上,极大的降低复杂度,提高开发效率。
  • Widget和Element是一一对应的,但Element并不会和RenderObject一一对应。只有需要渲染的Widget才有对应的RenderObject节点。Layer树以后再总结。

总结

  • 总结一下就是,Widget是我们通过代码创建的UI配置信息,Flutter框架通过遍历Widget树来创建Element树,Element树又根据需要渲染的Widget来创建renderObject树进行绘制和渲染等逻辑的操作。Widget负责管理配置信息,render负责渲染,Element是一个中间层,相当于一个大管家。当Widget配置信息改变时,通过比较Widget和老Widget的key和runtimeType来确定Element和renderObject是否需要重建,不需要重建的直接更新Element的属性就可以了,这样可以以最小的开销来更新renderObject,从而达到在Widget不断变化时达到高性能的渲染。这里面的知识点太多了,以后再来慢慢深究。

有关extends 、 implements 、 mixins

参考

  • 书写顺序是extends(继承) ==> with(混入) ==> implements(实现)

abstract class

abstract class C {
///这是一个抽象方法因为没有实现
c(); ///这是一个抽象getter
int get type; ///这个方法不会被强制重写因为他有实现
cc() {}
} ///继承自抽象类的子类必须重写父类的抽象方法
class D extends C {
@override
c() {} @override
int get type => 1;
}

implements

  • implements是对接口的实现,当一个类implements另一个类时,会被强制重写其父类的方法。
class A {
void a() {}
} class B implements A {
@override
void a() {
print('b a');
}
}

mixins

  • mixins可以被关联到另外一个class,为了重用代码但是又不用继承,需要用with关键字
  • 一个类可以拥有无数个mixins,一旦将mixins混入了一个类,这个类就持有所有mixins的方法
mixin Run {
void running() {}
void same() {
print('Run');
}
} mixin Walk {
void walking() {}
void same() {
print('Walk');
}
} mixin Talk {
void talking() {}
void same() {
print('Talk');
}
} ///现在Man拥有talk,walk,run,并且如果多个mixin有同名方法,取最后的实现
class Man with Run, Walk, Talk {} void main() {
//打印talk
Man().same();
Man().running();
Man().walking();
Man().talking();
}
  • mixins可以指定异常类型,用on关键字
class F {
f() {}
} mixin E on F {} ///G类想要混入E时,本身必须是实现了F接口或者继承于F或者继承于实现了F的类才能混入E
class G extends F with E {
@override
f() {}
} //实现了F的类
class Gimp implements F {
@override
f() {}
} class Gext extends Gimp with E {}

extends

Dart中的继承是单继承,子类重写父类的方法要用@override,不会强制继承父类的方法,子类调用父类的方法要用super

class Parent {
work() {}
study() {}
} class Child extends Parent {
@override
work() {
super.work();
super.study();
}
}

关于Dart单线程模型

Dart是单线程语言,所有的main函数中的代码都是在一个main isolate中完成的。我们一般的异步操作,实际上是通过单线程异步调度任务有优先级完成的,也就是所谓的Future。为了保证较高的响应性,一般特别耗时的任务都会重开一个isolate来执行,执行完成之后通过isolate之间的通信返回结果到main isolate中。

Dart事件机制

dart中有两个任务队列,Micotask queue和event queue,isolate中的代码是按顺序执行的

  • 首先执行main函数中的代码。

  • 执行完main函数中代码后,会检查并执行Microtask queue中的任务,通常使用scheduleMicrotask向Microtask queue添加任务。

  • 最后执行Event queue队列中的代码,通常使用Future向队列中添加任务,或者使用async await方式添加。

  • 总结:Main ==> Microtask queue ==> Event queue

Future的.then方法会将其中的代码放入Microtask队列,在Future执行完毕后立即执行,因为Microtask队列优先级更高。

下面用一段代码来验证执行顺序

void main() {
print('main 1');
new Future(() => print('future 1'));
scheduleMicrotask(() => print('micro 1'));
new Future(() => print('future 2'));
scheduleMicrotask(() => print('micro 2'));
print('main 2');
} //打印
main 1
main 2
micro 1
micro 2
future 1
future 2
Exited

Stream和Future

Stream和Future都是dart中用来处理异步事件的,Future表示稍后处理一个事件。区别在于Future只能处理单个异步事件,stream是处理一系列异步事件流。Stream详细在前几篇博文中可以找到,这里不再赘述。

需要补充的是,await可以等待当前异步操作完成,await for就是等待当前异步事件流(stream)完成,并可以通过yield返回每一个异步事件的结果。

其实await并不会阻塞main函数中的代码,它具体的实现是,当dart执行到有await的地方时,将整个Future函数返回为一个Future对象,放入Event队列中稍后异步执行,而await后面的代码才会跟着一起执行完毕。这一切都是在main中的代码都执行完毕之后完成的。

await for 示例

awaitFor() async {
print('awaitFor begin');
await for (var item in Stream.fromIterable([1, 2, 3])) {
print(item);
}
print('awaitFor end');
} //打印
awaitFor begin
1
2
3
awaitFor end
Exited

StatefulWidget的生命周期

initState

当此对象插入树中时调用。

框架将为其创建的每个 [State] 对象调用此方法一次。

此时State对象还没有和context绑定,通常拿来做一些初始化操作。比如事件监听,channel初始化

didChangeDependencies

当此 [State] 对象的依赖项发生更改时调用。

例如,如果上一次调用 [build] 引用了后来更改的 [InheritedWidget],则框架将调用此方法以通知此对象有关更改的信息。

此方法也会在 [initState] 之后立即调用。从此方法调用 [BuildContext.dependOnInheritedWidgetOfExactType] 是安全的。

在initState之后理解调用,此时已经和context绑定,可以拿来初始化一些和基于context的内容,当有依赖改变时,也会调用此方法通知更改信息

didUpdateWidget

每当小组件配置更改时调用。

如果父小组件重新生成并请求更新树中的此位置以显示具有相同 [runtimeType] 和 [Widget.key] 的新小部件,则框架将更新此 [State] 对象的 [widget] 属性以引用新小部件,然后使用以前的小部件作为参数调用此方法。

框架总是在调用[didUpdateWidget]之后调用[build],这意味着在[didUpdateWidget]中对[setState]的任何调用都是多余的。

如果 [State] 的 [build] 方法依赖于本身可以更改状态的对象,例如 [ChangeNotifier] 或 [Stream],或者可以订阅以接收通知的其他对象,请确保在 [initState]、[didUpdateWidget] 和 [dispose] 中正确订阅和取消订阅

当父组件有改变时,此方法会调用,并且通过旧的小组件生成新的小组件,在调用此方法后会立即调用build方法,如果有stream或者ChangeNotifier,确保在didUpdateWidget方法中取消订阅

build

描述此小组件所表示的用户界面部分。

该框架在许多不同的情况下调用此方法。例如:

  • 调用 [initState] 之后。

  • 在调用 [didUpdateWidget] 之后。

  • 在接到对 [setState] 的调用后。

  • 在此 [State] 对象的依赖项发生更改(例如,先前的 [build] 更改所引用的 [InheritedWidget] 之后。

  • 调用 [停用],然后将 [State] 对象重新插入到树中的其他位置后。

构造界面方法,在一些其他时候会调用,比如didChangeDependencies,didUpdateWidget,setState等

deactivate

每当框架从树中删除此 [State] 对象时,它都会调用此方法。在某些情况下,框架会将 [State] 对象重新插入到树的另一部分(例如,如果包含此 [State] 对象的子树由于使用了 [GlobalKey] 而从树中的一个位置移植到另一个位置)。

如果发生这种情况,框架将调用[激活],以使[State]对象有机会重新获取它在[停用]中释放的任何资源。然后,它还将调用 [build],以使 [State] 对象有机会适应其在树中的新位置。

当移出Widget tree时调用,如果框架将State对象再次插入Widget tree时,调用build方法

dispose

从树中永久删除此对象时调用。

当此 [State] 对象永远不会再次生成时,框架将调用此方法。在框架调用 [dispose] 之后,[State] 对象被视为未装载,并且 [mounted] 属性为 false。此时调用 [setState] 是错误的。生命周期的此阶段是终端阶段:无法重新挂载已释放的 [State] 对象。

对象被销毁时调用,比如路由中的pop操作,此时可用来释放资源,例如AnimationController,StreamController等,如果此对象由于某些延时操作导致在销毁后调用setState,会抛出异常,建议用if(mounted)判断是否还在当前页面

Key

Flutter中有LocalKey和GolbalKey两种形式的key,key是用来指明widget身份的唯一标识符

LocalKey

  • ValueKey

value类型为文本,当有widget内容是恒定且不同的,可以用ValueKey来指定,不会产生混淆

  • ObjectKey

如果说WIdget拥有更复杂的数据结构,比如一个用户信息的地址簿应用。任何单个字段(如名字或生日)可能与另一个条目相同,但是每一个数据组合是唯一的,此时就更适合使用ObjectKey

  • UniqueKey

如果集合中拥有多个相同值的Widget,或者想确保每个Widget和Widget都是不同的就可以使用UniqueKey

GlobalKeys

允许 Widget 在应用中的任何位置更改父级而不会丢失 State ,或者可以使用它们在 Widget 树 的完全不同的部分中访问有关另一个 Widget 的信息。

代码示例

import 'package:flutter/material.dart';

class PageA extends StatefulWidget {
const PageA({Key? key}) : super(key: key); @override
State<PageA> createState() => _PageAState();
} class _PageAState extends State<PageA> {
//创建一个_PageBState类型的GlobalKey
final GlobalKey<_PageBState> akey = GlobalKey();
pagea() {}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageB(key: akey),
floatingActionButton: TextButton(
onPressed: () {
//可以通过此key的currentState调用_PageBState的pageb方法
akey.currentState!.pageb();
},
child: Text('text'),
),
);
}
} class PageB extends StatefulWidget {
const PageB({Key? key}) : super(key: key); @override
State<PageB> createState() => _PageBState();
} class _PageBState extends State<PageB> {
//同理创建一个_PageAState类型的GlobalKey
final GlobalKey<_PageAState> bkey = GlobalKey();
pageb() {}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: TextButton(
onPressed: () {
//一样可以通过此key的currentState调用_PageAState的pagea方法
bkey.currentState!.pagea();
},
child: Text('text'),
),
);
}
}

什么是Navigator? MaterialApp做了什么

Navigator是在Flutter中负责管理维护页面堆栈的导航器。MaterialApp在需要的时候,会自动为我们创建Navigator。Navigator.of(context),会使用context来向上遍历Element树,找到MaterialApp提供的_NavigatorState再调用其push/pop方法完成导航操作。

全局Error捕获和处理

参考

Flutter 框架可以捕获运行期间的错误,包括构建期间、布局期间和绘制期间。

所有 Flutter 的错误均会被回调方法 FlutterError.onError 捕获。默认情况下,会调用 FlutterError.dumpErrorToConsole 方法,正如方法名表示的那样,将错误转储到当前的设备日志中。当从 IDE 运行应用时,检查器重写了该方法,错误也被发送到 IDE 的控制台,可以在控制台中检查出错的对象。

当构建期间发生错误时,回调函数 ErrorWidget.builder 会被调用,来生成一个新的 widget,用来代替构建失败的 widget。默认情况,debug 模式下会显示一个红色背景的错误页面, release 模式下会展示一个灰色背景的空白页面。

如果在调用堆栈上没有 Flutter 回调的情况下发生错误(这里可以理解为FlutterError.onError仅仅可以捕获主线程的错误,而其他异步线程的错误则需要Zone来捕获),它们由发生区域的 Zone 处理。 Zone 在默认情况下仅会打印错误,而不会执行其他任何操作。

这些回调方法都可以被重写,通常在 void main() 方法中重写。

下面来看看如何处理。

捕获Flutter错误

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; void main() {
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details);
if (kReleaseMode) {
//处理线上错误,如统计上传
}
};
runApp(MyApp());
}

上面我们重写了FlutterError.onError,这样就可以捕获到错误,第一行代码就是将error展示到控制台,这样我开发时就会在控制台很方便的看到错误。下面代码就是在线上环境下,对错误进一步处理,比如统计上传。

自定义error widget

上面我们知道,构建时发生错误会默认展示一个错误页面,但是这个页面很不友好,我们可以自定义一个错误页面。定义一个自定义的 error widget,以当 builder 构建 widget 失败时显示,请使用 MaterialApp.builder。


class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key); @override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
Widget error = Text('rendering error');
if (child is Scaffold || child is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error;
return error;
},
);
}
}

无法捕获的错误

假设一个 onPressed 回调调用了异步方法,例如 MethodChannel.invokeMethod (或者其他 plugin 的方法)

如果 invokeMethod 抛出了错误,它不会传递至 FlutterError.onError,而是直接进入 runApp 的 Zone。

如果你想捕获这样的错误,请使用 runZonedGuarded。代码如下

void main() {
runZonedGuarded(() {
runApp(MyApp());
}, (Object object, StackTrace stackTrace) {
//处理错误
});
}

请注意,如果你的应用在 runApp 中调用了 WidgetsFlutterBinding.ensureInitialized() 方法来进行一些初始化操作(例如 Firebase.initializeApp()),则必须在 runZonedGuarded 中调用 WidgetsFlutterBinding.ensureInitialized():

void main() {
runZonedGuarded(() {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}, (Object object, StackTrace stackTrace) {
//处理错误
});
}

Flutter的线程管理模型

默认情况下,Flutter会创建一个主isolate,并且dart代码会默认在这个isolate中执行,必要时可以通过isolate.spawn或者solate.spawnUri来创建新的isolate(注:Flutter中不支持isolate.spawnUri),新建的isolate由Flutter统一管理。

事实上,Flutter并不会管理线程,线程的创建和管理是通过比Flutter引擎更底层的Embeder层负责的,Embeder层是将引擎移植在平台的中间层代码,Flutter Engine层架构如下图

Embeder层提供四个Task Runner,分别是platform task runner,UI task runner,GPU task runner,I/O task runner,Flutter Engine并不关心task runner运行在哪个线程,只关心线程在整个生命周期内保持稳定。

Flutter查漏补缺1的更多相关文章

  1. 《CSS权威指南》基础复习+查漏补缺

    前几天被朋友问到几个CSS问题,讲道理么,接触CSS是从大一开始的,也算有3年半了,总是觉得自己对css算是熟悉的了.然而还是被几个问题弄的"一脸懵逼"... 然后又是刚入职新公司 ...

  2. js基础查漏补缺(更新)

    js基础查漏补缺: 1. NaN != NaN: 复制数组可以用slice: 数组的sort.reverse等方法都会改变自身: Map是一组键值对的结构,Set是key的集合: Array.Map. ...

  3. Entity Framework 查漏补缺 (一)

    明确EF建立的数据库和对象之间的关系 EF也是一种ORM技术框架, 将对象模型和关系型数据库的数据结构对应起来,开发人员不在利用sql去操作数据相关结构和数据.以下是EF建立的数据库和对象之间关系 关 ...

  4. 2019Java查漏补缺(一)

    看到一个总结的知识: 感觉很全面的知识梳理,自己在github上总结了计算机网络笔记就很累了,猜想思维导图的方式一定花费了作者很大的精力,特共享出来.原文:java基础思维导图 自己学习的查漏补缺如下 ...

  5. 20165223 week1测试查漏补缺

    week1查漏补缺 经过第一周的学习后,在蓝墨云班课上做了一套31道题的小测试,下面是对测试题中遇到的错误的分析和总结: 一.背记题 不属于Java后继技术的是? Ptyhon Java后继技术有? ...

  6. 今天開始慢下脚步,開始ios技术知识的查漏补缺。

    从2014.6.30 開始工作算起. 如今已经是第416天了.不止不觉.时间过的真快. 通过对之前工作的总结.发现,你的知识面.会决定你面对问题时的态度.过程和结果. 简单来讲.知识面拓展了,你才干有 ...

  7. Mysql查漏补缺笔记

    目录 查漏补缺笔记2019/05/19 文件格式后缀 丢失修改,脏读,不可重复读 超键,候选键,主键 构S(Stmcture)/完整性I(Integrity)/数据操纵M(Malippulation) ...

  8. 【spring源码分析】IOC容器初始化——查漏补缺(四)

    前言:在前几篇查漏补缺中,其实我们已经涉及到bean生命周期了,本篇内容进行详细分析. 首先看bean实例化过程: 分析: bean实例化开始后 注入对象属性后(前面IOC初始化十几篇文章). 检查激 ...

  9. Django 查漏补缺

    Django 查漏补缺 Django  内容回顾: 一. Http 请求本质: 网络传输,运用socket Django程序: socket 服务端 a. 服务端监听IP和端口 b. 浏览器发送请求 ...

随机推荐

  1. Solution -「SPOJ-VCIRCLES」Area of Circles

    \(\mathcal{Description}\)   Link.   求平面上 \(n\) 个圆的并的面积.   \(n\le50\),可能被圆覆盖的横纵坐标区域在 \([-10^4,10^4]\) ...

  2. 关于SpringCloud中,使用 Hystrix的问题

    springCloud升级后.导致 HtystrixDashboard 默认的servlet请求路径修改了 将业务的微服务使用 HtystrixDashboard 仪表盘第一次监控时出现 Unable ...

  3. web开发 小方法2-字体设置

    font-size 字体大小 直接给  (任意px) 就可以 font-family:"微软雅黑";   这个里面可以给多个用空格区分 按照先后优先级使用 当没有第一个字体的时候会 ...

  4. scanf坑我的那些年

    scanf函数作为用户输入指令给计算机的一种输入方法,它的使用有如下几被坑点: scanf用法:#include<stdio.h>;scanf("格式控制符",地址表列 ...

  5. [数据生成器]UVA10054 The Necklace

    应吴老师之邀,写了个数据生成器. 目前这个数据生成器可以保证生成的数据都是合法的,且效率也还不错.只是在建立普通连通图的时候zyy偷懒了,直接把所有点串起来从而保证图的连通.如果有大神有更好的方法请不 ...

  6. Renix软件如何建立OSPF邻居——网络测试仪实操

    OSPF可以通过OSPF向导的方式方便的创建OSPF邻居, 也可以通过纯手工的方式创建OSPF邻居, 本文介绍的是纯手工的方式创建. 在工作中, 推荐使用OSPF向导的方式来创建, 会比较简单和高效. ...

  7. 【C# .Net GC】开篇

    前言 自从.NET Core 3.0开始对根据自己具体的应用场景去配置GC ,让GC 发挥最好的作用..NET 5 改动更大,而且.NET 5整体性能比.net core 3.1高20%,并且在GC这 ...

  8. 【C#反射】Assembly

    Assembly属性的应用 //获取当前执行代码的程序集 Assembly assem = Assembly.GetExecutingAssembly(); Console.WriteLine($&q ...

  9. Hive常用函数大全-字符串函数

    1.字符串长度函数:length(X)(返回字符串X的长度) select length('qwerty') from table --6 2.字符串反转函数:reverse(X)(返回字符串X反转的 ...

  10. kubebuilder operator的运行逻辑

    kubebuilder 的运行逻辑 概述 下面是kubebuilder 的架构图.可以看到最外层是通过名为Manager的组件驱动的,Manager中包含了多个组件,其中Cache中保存了gvk和in ...