首页   

Flutter 基础知识点小集

SunshineBrother  ·  · 3 年前
阅读 13

Flutter 基础知识点小集

Dart

1、级联符号(..)表示什么意思?

Dart 当中的 「..」意思是 「级联操作符」,为了方便配置而使用。「..」和「.」不同的是 调用「..」后返回的相当于是 this,而「.」返回的则是该方法返回的值 。

2、Dart 的作用域

Dart 没有 「public」「private」等关键字,默认就是公开的,私有变量使用 下划线 _开头。

3、Dart 是不是单线程模型?是如何运行的?

异步 IO + 事件循环

1、I/O 模型

我们先来看看阻塞IO是什么样的:

String text = io.read(buffer); //阻塞等待
复制代码

注: IO 模型是操作系统层面的,这一小节的代码都是伪代码,只是为了方便理解。

当相应线程调用了read之后,它就会一直在那里等着结果返回,什么也不干,这是阻塞式的IO

这里普及两个概念:阻塞式调用和非阻塞式调用

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

  • 阻塞式调用: 调用结果返回之前,当前线程会被挂起,调用线程只有在得到调用结果之后才会继续执行。
  • 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。

但我们的应用程序经常是要同时处理好几个IO的,即便一个简单的手机App,同时发生的IO可能就有:用户手势(输入),若干网络请求(输入输出),渲染结果到屏幕(输出);更不用说是服务端程序,成百上千个并发请求都是家常便饭

有人说,这种情况可以使用多线程啊。这确实是个思路,但受制于CPU的实际并发数,每个线程只能同时处理单个IO,性能限制还是很大,而且还要处理不同线程之间的同步问题,程序的复杂度大大增加。

如果进行IO的时候不用阻塞,那情况就不一样了:

while(true){
  for(io in io_array){
      status = io.read(buffer);// 不管有没有数据都立即返回
      if(status == OK){
       
      }
  }
}
复制代码

有了非阻塞IO,通过轮询的方式,我们就可以对多个IO进行同时处理了,但这样也有一个明显的缺点:在大部分情况下,IO都是没有内容的(CPU的速度远高于IO速度),这样就会导致CPU大部分时间在空转,计算资源依然没有很好得到利用。

为了进一步解决这个问题,人们设计了IO多路转接(IO multiplexing),可以对多个IO监听和设置等待时间:

while(true){
    //如果其中一路IO有数据返回,则立即返回;如果一直没有,最多等待不超过timeout时间
    status = select(io_array, timeout); 
    if(status  == OK){
      for(io in io_array){
          io.read() //立即返回,数据都准备好了
      }
    }
}
复制代码

有了IO多路转接,CPU资源利用效率又有了一个提升。

在上面的代码中,线程依然是可能会阻塞在 select 上或者产生一些空转的,有没有一个更加完美的方案呢?

答案就是异步IO了:

io.async_read((data) => {
  // dosomething
});
复制代码

解决了Dart单线程进行IO也不会卡的疑问,但主线程如何和大量异步消息打交道呢?接下来我们继续讨论Dart的事件循环机制(Event Loop)。

2、事件循环(Event Loop)

Event Loop 完整版的流程图

从上图可知,Dart事件循环机制由一个消息循环(event looper)和两个消息队列构成,其中,两个消息队列是指事件队列(event queue)和微任务队列(Microtask queue)。该机制运行原理为:

  • 首先,Dart程序从main函数开始运行,待main函数执行完毕后,event looper开始工作;
  • 然后,event looper优先遍历执行Microtask队列所有事件,直到Microtask队列为空;
  • 接着,event looper才遍历执行Event队列中的所有事件,直到Event队列为空;
  • 最后,视情况退出循环。

微任务

微任务顾名思义,表示一个短时间内就会完成的异步任务。从上面的流程图可以看到,微任务队列在事件循环中的优先级是最高的,只要队列中还有任务,就可以一直霸占着事件循环。

微任务是由scheduleMicroTask建立的

scheduleMicrotask(() => print('This is a microtask'));
复制代码

不过,一般的异步任务通常也很少必须要在事件队列前完成,所以也不需要太高的优先级,因此我们通常很少会直接用到微任务队列,就连 Flutter 内部,也只有 7 处用到了而已(比如,手势识别、文本输入、滚动视图、保存页面效果等需要高优执行任务的场景)。

4、Dart 是如何实现多任务并行的?

尽管 Dart 是基于单线程模型的,但为了进一步利用多核 CPU,将 CPU 密集型运算进行隔离,Dart 也提供了多线程机制,即 Isolate。在 Isolate 中,资源隔离做得非常好,每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。

5DC3DDE0-787C-4E7A-B737-BF7EECB41755.png

假如不同的Isolate需要通信(单向/双向),就只能通过向对方的事件循环队列里写入任务,并且它们之间的通讯方式是通过port(端口)实现的,其中,Port又分为receivePort(接收端口)和sendPort(发送端口),它们是成对出现的。Isolate之间通信过程:

  • 首先,当前Isolate创建一个ReceivePort对象,并获得对应的SendPort对象;
 var receivePort = ReceivePort();
 var sendPort = receivePort.sendPort;
复制代码
  • 其次,创建一个新的Isolate,并实现新Isolate要执行的异步任务,同时,将当前Isolate的SendPort对象传递给新的Isolate,以便新Isolate使用这个SendPort对象向原来的Isolate发送事件;
// 调用Isolate.spawn创建一个新的Isolate
// 这是一个异步操作,因此使用await等待执行完毕
var anotherIsolate = await Isolate.spawn(otherIsolateInit, receivePort.sendPort);

// 新Isolate要执行的异步任务
// 即调用当前Isolate的sendPort向其receivePort发送消息
void otherIsolateInit(SendPort sendPort) async {
  value = "Other Thread!";
  sendPort.send("BB");
}
复制代码
  • 然后,调用当前Isolate#receivePort的listen方法监听新的Isolate传递过来的数据。Isolate之间什么数据类型都可以传递,不必做任何标记
receivePort.listen((date) {
    print("Isolate 1 接受消息:data = $date");
});
复制代码
  • 最后,消息传递完毕,关闭新创建的Isolate。
anotherIsolate?.kill(priority: Isolate.immediate);
anotherIsolate =null;
复制代码

5、说一下Dart异步编程中的 Future关键字?

Future 对象封装了Dart 的异步操作,它有未完成(uncompleted)和已完成(completed)两种状态。

在Dart中,所有涉及到IO的函数都封装成Future对象返回,在你调用一个异步函数的时候,在结果或者错误返回之前,你得到的是一个uncompleted状态的Future。

一个Future对象会有以下两种状态

  • pending:表示Future对象的计算过程仍在执行中,这个时候还没有可以用的result。
  • completed:表示Future对象已经计算结束了,可能会有两种情况,一种是正确的结果,一种是失败的结果。

6、说一下Dart异步编程中的 Stream数据流?

在Dart中,Stream 和 Future 一样,都是用来处理异步编程的工具。它们的区别在于,Stream 可以接收多个异步结果,而Future 只有一个。 1896199631-edb6678ead3c48af_fix732.png

  • 这个大机器就是StreamController,它是创建流的方式之一。
  • StreamController有一个入口,叫做sink
  • sink可以使用add方法放东西进来,放进去以后就不再关心了。
  • 当有东西从sink进来以后,我们的机器就开始工作
  • StreamController有一个出口,叫做stream
  • 机器处理完毕后就会把产品从出口丢出来,但是我们并不知道什么时候会出来,所以我们需要使用listen方法一直监听这个出口
  • 而且当多个物品被放进来了之后,它不会打乱顺序,而是先入先出

Stream的种类

  • "Single-subscription" streams 单订阅流:单个订阅流在流的整个生命周期内仅允许有一个listener
  • "broadcast" streams 多订阅流:广播流允许任意数量的收听者,且无论是否有收听者,他都能产生事件。所以中途进来的收听者将不会收到之前的消息。

7、await

static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
      {bool eagerError = false, void cleanUp(T successValue)?}){}
复制代码

wait静态方法可以等待多个Future执行完成,并通过List获取所有Future的结果。如果其中一个Future对象发生异常,会导致最终结果为failed

await for是不断获取stream流中的数据,然后执行循环体中的操作。它一般用在直到stream什么时候完成,并且必须等待传递完成之后才能使用,不然就会一直阻塞。

8、async 和 await

想象一个这样的场景:

    1. 先调用登录接口;
    1. 根据登录接口返回的token获取用户信息;
    1. 最后把用户信息缓存到本机。
Future<String> login(String name,String password){
  //登录
}
Future<User> fetchUserInfo(String token){
  //获取用户信息
}
Future saveUserInfo(User user){
  // 缓存用户信息
}
复制代码

用Future大概可以这样写:

login('name','password')
.then((token) => fetchUserInfo(token))
 .then((user) => saveUserInfo(user));
复制代码

换成async 和await则可以这样:

void doLogin() async {
  String token = await login('name','password'); //await 必须在 async 函数体内
  User user = await fetchUserInfo(token);
  await saveUserInfo(user);
}
复制代码

声明了async的函数,返回值是必须是Future对象。即便你在async函数里面直接返回T类型数据,编译器会自动帮你包装成Future<T>类型的对象,如果是void函数,则返回Future<void>对象。在遇到await的时候,又会把Futrue类型拆包,又会原来的数据类型暴露出来,请注意,await所在的函数必须添加async关键词

await的代码发生异常,捕获方式跟同步调用函数一样:

void doLogin() async {
  try {
    var token = await login('name','password');
    var user = await fetchUserInfo(token);
    await saveUserInfo(user);
  } catch (err) {
    print('Caught error: $err');
  }
}
复制代码

得益于async 和await 这对语法糖,你可以用同步编程的思维来处理异步编程,大大简化了异步代码的处理

9、函数

Dart是一门面向对象的语言,所以函数也是对象,并且函数的类型是Function

函数的参数可以分成两类: 必须参数和可选参数

可选参数可以分为 命名可选参数 和 位置可选参数

定义方式:

命名可选参数: {param1, param2, ...}
位置可选参数: [param1, param2, ...]
复制代码

1、可选参数

可选的命名参数,即使不传递这些参数也是可以的。 在定义函数时,使用{param1,parqm2,...}指定命名参数。

void userSettings({int age,String name}){
  if(age != null) {
    print('my age is ${age}');
  } 
  if(name != null) {
    print('my name is ${name}');
  } 
}
复制代码

上面函数中,我们可以传递age、name这两个参数,绘制其一,或者一个都不传递

2、必传参数

有时候,我们在调用函数的时候,必须传入一些参数,这个时候就用到了@required来修饰。使用@required有利于静态代码分析器进行检查

void userSettings({@required int age,String name}){
  if(age != null) {
    print('my age is ${age}');
  }
  if(name != null) {
    print('my name is ${name}');
  }
}
复制代码

3、可选的位置参数

使用[]把目标标记为可选的位置参数

 void userSettings(int age, String name, [String interests]){
  if(age != null) {
    print('my age is ${age}');
  }
  if(name != null) {
    print('my name is ${name}');
  }
  if(interests != null){
    print('兴趣是${interests}');
  }
}
复制代码

4、默认参数

默认值是编译时常量,在函数的参数后面使用 = 为参数赋值

  • 参数可以有默认值, 在不传入的情况下, 使用默认值
  • 注意, 只有可选参数才可以有默认值, 必须参数不能有默认值
void userSettings(int age, String name, [String interests,String sex = "男"]){
  if(age != null) {
    print('my age is ${age}');
  }
  if(name != null) {
    print('my name is ${name}');
  }
  if(interests != null){
    print('兴趣是${interests}');
  }
  if(sex != null){
    print('性别${sex}');
  }
}
复制代码

10、变量和常量

1.1、变量

Dart语言中所有变量都是一个对象,每个对象都是一个类的实例。数字类型(numbers)、函数和 null 也是对象。所有对象都继承自 Object 类。

Dart中定义变量的方式有两种:

1、明确的指定变量的数据类型

String name;
int age;
double height;
复制代码

2、使用 var / dynamic / Object 声明变量

var message1;
message1 = "str";
message1 = 19; //这行代码会报错
dynamic message2;
message2 = 1.78; 
message2 = "message2";//这行代码不会报错
print("${message2.length}");
Object message3;
message3 =  10;
message3 = "message3";
print("${message3.lenngth}"); //这句代码会报错
复制代码

var与dynamic的区别

  • 1.var声明的变量一经赋值后,数据类型就已经确定,不可以接收其他的数据类型,所以message1 = 19这行代码报错
  • 2.dynamic声明的变量在第一次赋值后,可以继续接收其他的数据类型,所以message2 = "message2"这行代码不会报错

dynamic与Object的区别

  • 1.dynamic与Object声明的变量都可以再次接收其他类型的数据类型
  • 2.dynamic声明的变量可以使用变量运行时的属性跟方法,String类型的变量有length属性,所以可以使用,Object声明的变量只能使用Object本身的属性以及方法,Object类型本身不具备lehgth属性,所以print("${message3.lenngth}");这行代码会报错

1.2、final与const声明常量

const str = "sj";
final msg = "msg";
复制代码

常量总结: 1.const与final 都用于声明常量,而且已经赋值都不可以被修改 2.const声明常量时,必须赋值明确的值,final声明的常量可以在运行时再赋值

11、dynamic&Object

dynamic: [daɪˈnæmɪk]  动态的;动力的;动力学的;有活力的

在Dart里面,一切皆对象,而且这些对象的父类都是Object。 当没有明确类型的时候,编译的时候回根据值明确类型

var name1 = "abc";
Object name2 = "def";
dynamic name3 = "hij";
复制代码

以上写法都没有问题,但是Dart不建议我们这么做。在实际开发过程中,我么么应该尽量为变量确定一个类型,这样可以提高安全性,加快运行速度。

如果不指定类型,debug模式下类型会是动态的。

使用dynamic时,则告诉编译器,我们不用做类型检测,并且知道自己在做什么。如果我们调用了一个不存在的方法时,回执行noSuchMethod()方法。 在默认情况下(Object里实现)它会抛出NoSuchMethodError

dynamic,var,object三者的区别

void main()//dynamic,var,object三者的区别
{
  //dynamic
  dynamic x = 'hello';//编译时不会揣测数据类型,但是运行时会推断
  print(x.runtimeType);//String
  print(x);
  //但是这样的坏处就是会让dart的语法检查失效,所以有可能会造成混乱而不报错
  //所以不要直接使用dynamic
  x = 123;
  print(x.runtimeType);//int,说明类型是可变的
  print(x);
 
  //var
  var a = 'hello';
  print(a.runtimeType);
  print(a);
  //a = 123;//会报错
  a = '123';
  print(a);
 
  //Object
  Object w = 1;
  print(w.runtimeType);
  print(w);
  //不能调用Object不存在的方法
  
}
复制代码

dynamic与Object的最大的区别在于静态类型检查上

12、说一下 mixin机制

mixin 是Dart 2.1 加入的特性,以前版本通常使用abstract class代替。简单来说,mixin是为了解决继承方面的问题而引入的机制,Dart为了支持多重继承,引入了mixin关键字,它最大的特殊处在于:mixin定义的类不能有构造方法,这样可以避免继承多个类而产生的父类构造方法冲突。

mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件

因为mixins使用的条件,随着Dart版本一直在变,这里讲的是Dart2.1中使用mixins的条件:

  • mixins类只能继承自object mixins类不能有构造函数

  • 一个类可以mixins多个mixins

  • 可以mixins多个类,不破坏Flutter的单继承

13、Dart的内存分配与垃圾回收是怎么样的?

Dart VM 的内存分配策略比较简单,创建对象时只需要在堆上移动指针,内存增长始终是线性的,省去了查找可用内存的过程。在 Dart 中,并发是通过 Isolate 实现的。Isolate 是类似于线程但不共享内存,独立运行的 worker。这样的机制,就可以让 Dart 实现无锁的快速分配。Dart 的垃圾回收,则是采用了多生代算法。新生代在回收内存时采用“半空间”机制,触发垃圾回收时,Dart 会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存。回收过程中,Dart 只需要操作少量的“活跃”对象,没有引用的大量“死亡”对象则被忽略,这样的回收机制很适合 Flutter 框架中大量 Widget 销毁重建的场景。

14、构造方法

Dart 中的多构造方法,可以通过命名方法实现。

默认构造方法只能有一个,而通过 Model.empty() 方法可以创建一个空参数的类,其实方法名称随你喜欢,而变量初始化值时,只需要通过 this.name 在构造方法中指定即可

class ModelA {
  String name;
  String tag;
  
  //默认构造方法,赋值给name和tag
  ModelA(this.name, this.tag);

  //返回一个空的ModelA
  ModelA.empty();
  
  //返回一个设置了name的ModelA
  ModelA.forName(this.name);
}

复制代码

15、getter setter 重写

Dart 中所有的基础类型、类等都继承 Object ,默认值是 NULL, 自带 gettersetter ,而如果是 final 或者 const 的话,那么它只有一个 getter 方法,Object 都支持 getter、setter 重写:

  @override
  Size get preferredSize {
    return Size.fromHeight(kTabHeight + indicatorWeight);
  }
复制代码

Flutter

1、请简单介绍下Flutter框架,以及它的优缺点?

Flutter是Google推出的一套开源跨平台UI框架,可以快速地在Android、iOS和Web平台上构建高质量的原生用户界面。同时,Flutter还是Google新研发的Fuchsia操作系统的默认开发套件。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。Flutter采用现代响应式框架构建,其中心思想是使用组件来构建应用的UI。当组件的状态发生改变时,组件会重构它的描述,Flutter会对比之前的描述,以确定底层渲染树从当前状态转换到下一个状态所需要的最小更改。

优点

  • 热重载(Hot Reload),利用Android Studio直接一个ctrl+s就可以保存并重载,模拟器立马就可以看见效果,相比原生冗长的编译过程强很多;
  • 一切皆为Widget的理念,对于Flutter来说,手机应用里的所有东西都是Widget,通过可组合的空间集合、丰富的动画库以及分层课扩展的架构实现了富有感染力的灵活界面设计;
  • 借助可移植的GPU加速的渲染引擎以及高性能本地代码运行时以达到跨平台设备的高质量用户体验。简单来说就是:最终结果就是利用Flutter构建的应用在运行效率上会和原生应用差不多。

缺点

  • 不支持热更新;
  • 三方库有限,需要自己造轮子;
  • Dart语言编写,增加了学习难度,并且学习了Dart之后无其他用处,相比JS和Java来说。

2、Widget生命周期

flutter生命周期其实就是Widget的生命周期,生命周期的回调函数体现在了State上面。 主要可以分成两方面讨论

  • 1、StatelessWidget
  • 2、StatefulWidget

1、StatelessWidget

StatelessWidget比较简单,主要有构造方法build方法

class MyStatelessWidget extends StatelessWidget {
  final String title;
  MyStatelessWidget({this.title}){
    print('构造函数被调用了!');
  }
  @override
  Widget build(BuildContext context) {
    print('build方法被调用了!');
    return Container();
  }
}
复制代码

2、StatefulWidget

StatefulWidget就有点复杂了,写一个StatefulWidget的时候,系统默认给我们创建了一个WidgetState,所以StatefulWidget的生命周期包括了两部分

  • 1、Widget的生命周期
  • 2、State的生命周期

大致可以看成三个阶段

  • 初始化(插入渲染树)
    • 1、Widget的构造方法
    • 2、Widget的CreateState
    • 3、State的构造方法
    • 4、State的initState方法
  • 状态改变(在渲染树中存在)
    • 1、didChangeDependencies方法 (改变依赖关系):依赖的InheritedWidget发生变化之后, 方法也会调用!
    • 2、State的build:当调用setState方法。 会重新调用build进行渲染!
    • 3、didUpdateWidget:判断是否要更新widget树
  • 销毁(从渲染树种移除)
    • 1、deactivate:在dispose之前,会调用这个函数
    • 2、当Widget销毁的时候, 调用State的dispose

3、说下Widgets、RenderObjects 和 Elements的关系

7d342f274112437d87aa2f72da579f8e~tplv-k3u1fbpfcp-watermark.image.png

Flutter 是 UI 框架,Flutter 内一切皆 Widget ,每个 Widget 状态都代表了一帧,Widget 是不可变的。 那么 Widget 是怎么工作的呢?

在 Flutter 中大部分时候我们写的是 Widget ,但是 Widget 的角色反而更像是“配置文件” ,真正触发工作的其实是 RenderObject

  • Widget 是配置文件:仅用于存储渲染所需要的信息
  • Element 是桥梁和仓库。
  • RenderObject 是解析后的绘制和布局。

Widget 和我们以前的布局概念不一样,因为 Widget 是不可变的(immutable),且只有一帧,且不是真正工作的对象,每次画面变化,都会导致一些 Widget 重新 build 。

4、Widget与Element

ac1350989a804846969569321c52c760~tplv-k3u1fbpfcp-watermark.image.png

并不是所有的Widget都会被独立渲染!只有继承RenderObjectWidget的才会创建RenderObject对象!

每一个Widget都会 创建一个Element对象,会隐式的调用createElement()方法,将Element加入Element树中,Element主要有三种

  • 1、RenderElement主要创建RenderObject对象,继承RenderWidget的widget会创建RenderObject对象
    • 1、创建RenderElement
    • 2、Flutter会调用mount方法,调用createRanderObject方法
  • 2、StatelessElement继承ComponentElement,StatelessWidget会创建StatelessElement
    • 主要就是调用build方法,将自己(Element)传出去
  • 3、StatefulElement继承ComponentElement,StatefulWidget会创建StatefulElement
    • 1、调用createState方法,创建state
    • 2、将widget赋值给state
    • 3、调用state中的build方法,并且将自己(Element)传出去

Widget build(BuildContext context) {}里面的context就是widget的Element

5、main()和runApp()函数在flutter的作用分别是什么?有什么关系吗?

main函数是类似于java语言的程序运行入口函数   

runApp函数是渲染根widget树的函数

一般情况下runApp函数会在main函数里执行

6、什么是状态管理,你了解哪些状态管理框架?

状态管理这个概念刚开始看的时候不太懂什么意思,研究一下发现其实状态管理就是我们原生中的数据管理 ** Flutter是声明式的,这意味着Flutter是通过更新UI来反映当前app的状态。简单来说,在Flutter中,如果我们想更新我们的控件,最基本的方式应该是setState()了。

Flutter的状态可以分为全局状态和局部状态两种。常用的状态管理有ScopedModel、BLoC、Redux / FishRedux和Provider

7、Flutter 是如何与原生Android、iOS进行通信的?

Flutter 为开发者提供了一个轻量级的解决方案,即逻辑层的方法通道(Method Channel)机制。基于方法通道,我们可以将原生代码所拥有的能力,以接口形式暴露给 Dart,从而实现 Dart 代码与原生代码的交互,就像调用了一个普通的 Dart API 一样。

af9e94b324b046248d2df63753c7983a~tplv-k3u1fbpfcp-watermark.image.png

当在Flutter中调用原生方法时,调用信息通过平台通道传递到原生,原生收到调用信息后方可执行指定的操作,如需返回数据,则原生会将数据再通过平台通道传递给Flutter。值得注意的是消息传递是异步的,这确保了用户界面在消息传递时不会被挂起。 Flutter 通过 PlatformChannel 与原生进行交互,其中 PlatformChannel 分为三种:

  • BasicMessageChannel:用于传递字符串和半结构化的信息。
  • MethodChannel:用于传递方法调用。Flutter主动调用Native的方法,并获取相应的返回值。
  • EventChannel:用于数据流(event streams)的通信。

Android、iOS 和 Dart 平台间的常见数据类型转换

平台通道使用标准消息编/解码器对消息进行编解码,它可以高效的对消息进行二进制序列化与反序列化。由于Dart与原生平台之间数据类型有所差异,下面我们列出数据类型之间的映射关系。

bec677c61c064441b4530efec16d3159~tplv-k3u1fbpfcp-watermark.image.png

值传递

Flutter如何实现一次方法调用请求

首先,我们需要确定一个唯一的字符串标识符,来构造一个命名通道;然后,在这个通道之上,Flutter 通过指定方法名flutter_postData来发起一次方法调用请求。

可以看到,这和我们平时调用一个 Dart 对象的方法完全一样。因为方法调用过程是异步的,所以我们需要使用非阻塞(或者注册回调)来等待原生代码给予响应。

// 声明 MethodChannel
const platform = MethodChannel('flutter_postData');

// 处理按钮点击
onPressed: () async{
    List result;
    try{
         result = await platform.invokeMethod('flutter_postData',{"flutter":"我是flutter"});
    }catch(e){
          result = [];
    }
   print(result.toString());
},
复制代码

iOS端的方法调用响应如何实现

在 iOS 平台,方法调用的处理和响应是在 Flutter 应用的入口,也就是在 Applegate 中的 rootViewController(即 FlutterViewController)里实现的,因此我们需要打开 Flutter 的 iOS 宿主 App,找到 AppDelegate.m 文件,并添加相关逻辑

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    
    // 创建命名方法通道
    let methodChannel = FlutterMethodChannel.init(name: "flutter_postData", binaryMessenger: self.window.rootViewController as! FlutterBinaryMessenger)
    // 往方法通道注册方法调用处理回调
    methodChannel.setMethodCallHandler { (call, result) in
        if("flutter_postData" == call.method){
            //打印flutter传来的值
            print(call.arguments ?? {})
            //向flutter传递值
            DispatchQueue.main.async {
                result(["1","2","3"]);
            }
            
        }
    }
    
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
复制代码

android端的方法调用响应如何实现

在 Android 平台,方法调用的处理和响应是在 Flutter 应用的入口,也就是在 MainActivity 中的 FlutterView 里实现的,因此我们需要打开 Flutter 的 Android 宿主 App,找到 MainActivity.java 文件,并在其中添加相关的逻辑。 接下来,在onCreate里创建MethodChannel并设置一个MethodCallHandler。确保使用和Flutter客户端中使用的通道名称相同的名称。

import android.os.Bundle;

import io.flutter.Log;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "flutter_postData";
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
                new MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, Result result) {
                        // TODO
                        if(call.method.equals("flutter_postData")){
                            //打印flutter传来的值
                            Log.e(call.arguments);
                            //向flutter传递值
                            result.success(new String[]{"1", "2","3"});
                        }
                    }
                });
    }
}

复制代码

Flutter视图中嵌套原生视图

使用方法

  • 1、首先,由作为客户端的 Flutter,通过向原生视图的 Flutter 封装类(在 iOS 和 Android 平台分别是 UIKitView 和 AndroidView)传入视图标识符,用于发起原生视图的创建请求;
  • 2、然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
  • 3、最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让 Flutter 发起的视图创建请求可以直接找到对应的视图创建工厂。

9a48ff6059494000af0291e3f0aa74bc~tplv-k3u1fbpfcp-watermark.image.png

8、简述下Flutter 的热重载

热重载是指,在不中断 App 正常运行的情况下,动态注入修改后的代码片段。

Flutter 的热重载功能可帮助您在无需重新启动应用程序的情况下快速添加功能以及修复错误。通过将更新的源代码文件注入到正在运行的 Dart 虚拟机(VM) 来实现热重载。在虚拟机使用新的字段和函数更新类之后, Flutter 框架会自动重新构建 widget 树,以便您可以快速查看更改的效果

Flutter编译方式

Flutter在Debug和Relase执行不同的编译模式

  • JIT:Just In Time . 动态解释,一边翻译一边执行,也称为即时编译,如JavaScript,Python等,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但是运行速度和性能则会受到影响,Flutter中的热重载正是基于此特性

  • AOT: Ahead of Time. 静态编译,是指程序在执行前全部被翻译为机器码,提前编译,如 C ,C++ ,OC等,发布时期使用AOT,就不需要像RN那样在跨平台JavaScript代码和原生Android、iOS代码间建立低效的方法调用映射关系。

Flutter 热重载

40ce4b4454b44892b09825865cdbb1bd~tplv-k3u1fbpfcp-watermark.image.png

总体来说,热重载的流程可以分为扫描工程改动、增量编译、推送更新、代码合并、Widget 重建 5 个步骤:

  • 1、工程改动。热重载模块会逐一扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的 Dart 代码。
  • 2、增量编译。热重载模块会将发生变化的 Dart 代码,通过编译转化为增量的 Dart Kernel 文件。
  • 3、推送更新。热重载模块将增量的 Dart Kernel 文件通过 HTTP 端口,发送给正在移动设备上运行的 Dart VM。
  • 4、代码合并。Dart VM 会将收到的增量 Dart Kernel 文件,与原有的 Dart Kernel 文件进行合并,然后重新加载新的 Dart Kernel 文件。
  • 5、Widget 重建。在确认 Dart VM 资源加载成功后,Flutter 会将其 UI 线程重置,通知 Flutter Framework 重建 Widget

可以看到,Flutter 提供的热重载在收到代码变更后,并不会让 App 重新启动执行,而只会触发 Widget 树的重新绘制,因此可以保持改动前的状态,这就大大节省了调试复杂交互界面的时间。

不支持热重载的场景

Flutter 提供的亚秒级热重载一直是开发者的调试利器。通过热重载,我们可以快速修改 UI、修复 Bug,无需重启应用即可看到改动效果,从而大大提升了 UI 调试效率。

不过,Flutter 的热重载也有一定的局限性。因为涉及到状态保存与恢复,所以并不是所有的代码改动都可以通过热重载来更新。

接下来,我就与你介绍几个不支持热重载的典型场景:

  • 代码出现编译错误;

  • Widget 状态无法兼容;

  • 全局变量和静态属性的更改;

  • main 方法里的更改;

  • initState 方法里的更改;

  • 枚举和泛类型更改。

说一下Hot Reload,Hot Restart,热更新三者的区别和原理。

  • Hot Reload:热重载是指,在不中断 App 正常运行的情况下,动态注入修改后的代码片段。
  • Hot Restart:系统的软件重启
  • 热更新:不需要用户感知,对app进行版本迭代

Hot Reload比Hot Restart快,Hot Reload会编译我们文件里新加的代码并发送给dart虚拟机,dart会更新widgets来改变UI,而Hot Restart会让dart 虚拟机重新编译应用。另一方面也是因为这样, Hot Reload会保留之前的state,而Hot Restart回你重置所有的state回到初始值。

9、说下Flutter 和其他跨平台方案的本质区别

React Native 之类的框架,只是通过 JavaScript 虚拟机扩展调用系统组件,由 Android 和 iOS 系统进行组件的渲染;

Flutter 则是自己完成了组件渲染的闭环。那么,Flutter 是怎么完成组件渲染的呢?这需要从图像显示的基本原理说起。在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。CPU 把计算好的、需要显示的内容交给 GPU,由 GPU 完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。操作系统在呈现图像时遵循了这种机制,而 Flutter 作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释 Flutter 的绘制原理。

Flutter 绘制原理可以看到,Flutter 关注如何尽可能快地在两个硬件时钟的 VSync 信号之间计算并合成视图数据,然后通过 Skia 交给 GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给 GPU 渲染。

10、Widget 唯一标识Key有哪几种?

在flutter中,每个widget都是被唯一标识的。这个唯一标识在buildrendering阶段由框架定义。该标识对应于可选的Key参数,如果省略,Flutter将会自动生成一个。

在flutter中,主要有4种类型的KeyGlobalKey(确保生成的Key在整个应用中唯一,是很昂贵的,允许element在树周围移动或变更父节点而不会丢失状态)、LocalKeyUniqueKeyObjectKey

11、什么是Navigator? MaterialApp做了什么?

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

12、flutter run实际走了哪三个命令?分别用于什么操作?

  • flutter build apk:通过gradle来构建APK
  • adb install:安装APK
  • adb am start:启动应用

13、setState做了哪些工作?是如何更新UI的?

setState 其实是调用了 markNeedsBuild ,该方法内部标记此Element 为 Dirty ,然后在下一帧 WidgetsBinding.drawFrame 才会被绘制, setState 并不是立即生效的。

14、Flutter 动画

在Flutter中,学习动画相关的开发,其实就是围绕Animation、Curve、Controller、Tween等四个动画对象来展开的。

Animation

在Flutter中,Animation是实现动画的核心类,Animation的主要作用就是保存动画的插值和状态,它本身与视图渲染没有任何关系。Animation对象则是一个可以在一段时间内依次生成一个区间值的类,其输出值可以是线性的、曲线的,可以是一个步进函数或者任何其他曲线函数等,由Curve来决定

AnimationController

AnimationController,即动画控制器,Animation是一个抽象类,并不能用来直接创建对象并实现动画,它的主要用于控制动画的开始、结束、停止、反向等操作。AnimationController是Animation的一个子类,默认情况下,AnimationController会在给定的时间段内以线性的方式生成从0.0到1.0的数字

CurvedAnimation

CurvedAnimation是Animation的一个实现类,它的目的是为了给AnimationController增加动画曲线。通常,动画过程可以是匀速的、匀加速的或者先加速后减速等。Flutter通过Curve来描述动画过程,我们可以把匀速动画称为线性动画,把非匀速动画称为非线性动画。

CurvedAnimation可以将AnimationController和Curve结合起来,生成一个新的Animation对象。

Tween

默认情况下,AnimationController对象的取值范围是[0.0,1.0],如果需要给动画设置不同的范围或者类型的值时,可以使用Tween来定义并生成不同范围或类型的值

Tween的源码非常简单,传入两个值即可

15、Flutter 跨组件传递数据都有哪些方式

  • 1、InheritedWidget
  • 2、Notification
  • 3、事件总线EventBus

1、InheritedWidget

InheritedWidget是Flutter中非常重要的一个功能型Widget,它可以高效的将数据在Widget树中向下传递、共享,这在一些需要在Widget树中共享数据的场景中非常方便,如Flutter中,正是通过InheritedWidget来共享应用主题(Theme)和Locale(当前语言环境)信息的。

f29dfa233527435b9ad1d7ea7bdb1a3b~tplv-k3u1fbpfcp-watermark.image.png

2、Notification

Notification 是 Flutter 中进行跨层数据共享的另一个重要的机制。如果说 InheritedWidget 的数据流动方式是从父 Widget 到子 Widget 逐层传递,那 Notificaiton 则恰恰相反,数据流动方式是从子 Widget 向上传递至父 Widget。这样的数据传递机制适用于子 Widget 状态变更,发送通知上报的场景。

Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。

如果想要实现自定义通知,我们首先需要继承 Notification 类。Notification 类提供了 dispatch 方法,可以让我们沿着 context 对应的 Element 节点树向上逐层发送通知

3、事件总线EventBus

无论是 InheritedWidget 还是 Notificaiton,它们的使用场景都需要依靠 Widget 树,也就意味着只能在有父子关系的 Widget 之间进行数据共享。但是,组件间数据传递还有一种常见场景:这些组件间不存在父子关系。这时,事件总线 EventBus 就登场了。

事件总线是在 Flutter 中实现跨组件通信的机制。它遵循发布 / 订阅模式,允许订阅者订阅事件,当发布者触发事件时,订阅者和发布者之间可以通过事件进行交互。发布者和订阅者之间无需有父子关系,甚至非 Widget 对象也可以发布 / 订阅。这些特点与其他平台的事件总线机制是类似的。

接下来,我们通过一个跨页面通信的例子,来看一下事件总线的具体使用方法。需要注意的是,EventBus 是一个第三方插件,因此我们需要在 pubspec.yaml 文件中声明它:

dependencies:  
  event_bus: ^1.1.1
复制代码

16、Flutter 路由与导航

Flutter 路由管理中有两个非常重要的概念

  • Route:路由是应用程序页面的抽象,对应 Android 中 Activity 和 iOS 中的 ViewController,由 Navigator 管理。
  • Navigator:Navigator 是一个组件,管理和维护一个基于堆栈的历史记录,通过 push 和 pop 进行页面的跳转。

** 根据是否需要提前注册页面标识符,Flutter 中的路由管理可以分为两种方式:

  • 基本路由。无需提前注册,在页面切换时需要自己构造页面实例。
  • 命名路由。需要提前注册页面标识符,在页面切换时通过标识符直接打开新的路由。

17、Flutter中用户交互事件

手势操作在 Flutter 中分为两类:

  • 第一类是原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上触摸(或鼠标、手写笔)行为触发的位移行为;
  • 第二类则是手势识别(Gesture Detector),表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装。

手势竞争与冲突

GestureDetector 内部对每一个手势都建立了一个工厂类(Gesture Factory)。而工厂类的内部会使用手势识别类(GestureRecognizer),来确定当前处理的手势。

而所有手势的工厂类都会被交给 RawGestureDetector 类,以完成监测手势的大量工作:使用 Listener 监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。

像这样的手势识别发生在多个存在父子关系的视图时,手势竞技场会一并检查父视图和子视图的手势,并且通常最终会确认由子视图来响应事件。而这也是合乎常理的:从视觉效果上看,子视图的视图层级位于父视图之上,相当于对其进行了遮挡,因此从事件处理上看,子视图自然是事件响应的第一责任人。

在下面的示例中,我定义了两个嵌套的 Container 容器,分别加入了点击识别事件:

GestureDetector(
  onTap: () => print('Parent tapped'),// 父视图的点击回调
  child: Container(
    color: Colors.pinkAccent,
    child: Center(
      child: GestureDetector(
        onTap: () => print('Child tapped'),// 子视图的点击回调
        child: Container(
          color: Colors.blueAccent,
          width: 200.0,
          height: 200.0,
        ),
      ),
    ),
  ),
);
复制代码

运行这段代码,然后在蓝色区域进行点击,可以发现:尽管父容器也监听了点击事件,但 Flutter 只响应了子容器的点击事件。

为了让父容器也能接收到手势,我们需要同时使用 RawGestureDetector 和 GestureFactory,来改变竞技场决定由谁来响应用户事件的结果。 在此之前,我们还需要自定义一个手势识别器,让这个识别器在竞技场被 PK 失败时,能够再把自己重新添加回来,以便接下来还能继续去响应用户事件。

在下面的代码中,我定义了一个继承自点击手势识别器 TapGestureRecognizer 的类,并重写了其 rejectGesture 方法,手动地把自己又复活了:

class MultipleTapGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    acceptGesture(pointer);
  }
}
复制代码

接下来,我们需要将手势识别器和其工厂类传递给 RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。事实上,RawGestureDetector 的初始化函数所做的配置工作,就是定义不同手势识别器和其工厂类的映射关系。

这里,由于我们只需要处理点击事件,所以只配置一个识别器即可。工厂类的初始化采用 GestureRecognizerFactoryWithHandlers 函数完成,这个函数提供了手势识别对象创建,以及对应的初始化入口。

在下面的代码中,我们完成了自定义手势识别器的创建,并设置了点击事件回调方法。需要注意的是,由于我们只需要在父容器监听子容器的点击事件,所以只需要将父容器用 RawGestureDetector 包装起来就可以了,而子容器保持不变:

RawGestureDetector(// 自己构造父 Widget 的手势识别映射关系
  gestures: {
    // 建立多手势识别器与手势识别工厂类的映射关系,从而返回可以响应该手势的 recognizer
    MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
        MultipleTapGestureRecognizer>(
          () => MultipleTapGestureRecognizer(),
          (MultipleTapGestureRecognizer instance) {
        instance.onTap = () => print('parent tapped ');// 点击回调
      },
    )
  },
  child: Container(
    color: Colors.pinkAccent,
    child: Center(
      child: GestureDetector(// 子视图可以继续使用 GestureDetector
        onTap: () => print('Child tapped'),
        child: Container(
              color: Colors.blueAccent,
              width: 200.0,
              height: 200.0
            )
      ),
    ),
  ),
);

复制代码

18、Flutter 的编译模式

Flutter 支持 3 种运行模式,包括 Debug、Release 和 Profile。在编译时,这三种模式是完全独立的。首先,我们先来看看这 3 种模式的具体含义吧。

  • Debug 模式对应 Dart 的 JIT 模式,可以在真机和模拟器上同时运行。该模式会打开所有的断言(assert),以及所有的调试信息、服务扩展和调试辅助(比如 Observatory)。此外,该模式为快速开发和运行做了优化,支持亚秒级有状态的 Hot reload(热重载),但并没有优化代码执行速度、二进制包大小和部署。flutter run --debug 命令,就是以这种模式运行的。
  • Release 模式对应 Dart 的 AOT 模式,只能在真机上运行,不能在模拟器上运行,其编译目标为最终的线上发布,给最终的用户使用。该模式会关闭所有的断言,以及尽可能多的调试信息、服务扩展和调试辅助。此外,该模式优化了应用快速启动、代码快速执行,以及二级制包大小,因此编译时间较长。flutter run --release 命令,就是以这种模式运行的。
  • Profile 模式,基本与 Release 模式一致,只是多了对 Profile 模式的服务扩展的支持,包括支持跟踪,以及一些为了最低限度支持所需要的依赖(比如,可以连接 Observatory 到进程)。该模式用于分析真实设备实际运行性能。flutter run --profile 命令,就是以这种模式运行的。

Profile 与 Release 在编译过程上几乎无差异

推荐文章
MedMed医学传播时讯  ·  盛世泰科完成首个中美双报新药项目 开启海外征程  ·  1 年前  
英伦圈  ·  脱发爆痘姨妈痛, 还有救吗? ...  ·  1 年前  
瑞信全球证券研究  ·  年轻消费群体可能影响环境变化的速度  ·  2 年前  
玩转手机摄影  ·  你们要的「超市人像」拍摄攻略来了!  ·  3 年前  
万小刀  ·  10万+公司申请注销,这一家惊险突围!  ·  4 年前  
© 2022 51好读
删除内容请联系邮箱 2879853325@qq.com