Flutter面试

目录

const 和final的区别

  1. 用final修饰的变量,必须在定义时将其初始化,其值在初始化后不可改变 运行期
  2. const 用来定义常量,为编译期就确定了值
  3. const 关键字即可修饰变量也可用来修饰 常量构造函数,它要求该类的所有成员都必须是final的。

编译模式

  1. Debug: JIT-即时编译
    • flutter_assets 里面多了isolate_snapshot_data,kernel_blob.bin,vm_shapshot_data
    • lib 下只有 libflutter.so
    • isolate_snapshot_data:⽤于加速 isolate 启动,业务⽆关代码
    • kernel_blob.bin:业务代码产物
    • vm_snapshot_data:⽤于加速 Dart VM 启动的产物,业务⽆关代码
    • 为了实现 hot reload,debug 模式下,dart 代码会⽣成⾄少3个⽂件,这三个⽂件就是每次编译动态⽣成,运⾏的时候可以替换以便实现 hot reload。⽽ release 模式下这3个⽂件会并⼊ app 库。Flutter 库在 debug 和 release 模式下⼤⼩也是不⼀样的
  2. Release: AOT-提前编译
    • assets ⾥多个 flutter_assets存放资源的
    • lib ⾥多了 libapp.so 和 libflutter.so

状态管理

  1. provider,业务和UI层,InheretedWidget实现
    • InheretedWidget 允许它下面的任何 Widget 访问它的属性,这意味着可以有一个变量 xx.of(context)
    • Notifier负责实现业务逻辑,且在数据更新时发出通知。
    • Consumer负责实现界面逻辑,并在数据更新时更新自身,以及用户交互时调用Notifier方法。
    • 组件通信方式依赖公共Notifier父节点,数据同步与组件树结构强耦合,项目维护成本高
    • 组件不可插拔,组件获取数据依赖Notifier父节点,与组件树结构强耦合
    • 依赖BuildContext,全局管理比较麻烦
  2. RiverPod
    • 比Provider更少的样板」:RiverPod 在减少 Provider 模版方面做得很好,允许开发者只注册一个顶级存储而不必单独提供每个提供。(可能有人一想到把所有东西都集中在一个地方而畏缩——别担心,你可以确定你的 pod。)
    • 不依赖 BuildContext:这也是一个很好的选择,原因前面已经提到。有时你只是无法在需要的地方获得 BuildContext。
    • 「编译安全」:到目前为止这是状态管理的最佳创新,只要代码能编译就是安全的,我们不再需要知道为什么不能在树中找到我们的 Provider,这是一项巨大的创新,可以为你节省很多的时间。
  3. GetX

三棵树

  1. Widget 存放渲染内容、它只是一个配置数据结构,创建是非常轻量的,在页面刷新的过程中随时会重建
  2. Element 分离 Widget 和真正的渲染对象的中间层, WidgetTree 用来描述对应的Element 属性,同时持有Widget和RenderObject,存放上下文信息,通过它来遍历视图树,支撑UI结构。
  3. RenderObject 用于应用界面的布局和绘制,负责真正的渲染,保存了元素的大小,布局等信息,实例化一个 RenderObject 是非常耗能的
  4. 当应用启动时 Flutter 会遍历并创建所有的 Widget 形成 Widget Tree,通过调用 Widget 上的 createElement() 方法创建每个 Element 对象,形成 Element Tree。最后调用 Element 的 createRenderObject() 方法创建每个渲染对象,形成一个 Render Tree。

为什么设计三棵树,有什么好处 1. 如果每一点细微的操作就去完全重绘一遍UI,将带来极大的性能开销。flutter的三棵树型模式设计可以有效地带来性能提升。 2. widget的重建开销非常小,所以可以随意的重建,因为它不会导致页面重绘,并且它也不一定会常常变化。 而renderObject如果频繁创建和销毁成本就很高了,对性能的影响比较大,因此它会缓存所有页面元素,只是当这些元素有变化时才去重绘页面。 3. 而判断页面有无变化就依靠element了,每次widget变化时element会比较前后两个widget,只有当某一个位置的Widget和新Widget不一致,才会重新创建Element和widget

setState 流程

  1. 调用element.markNeedsBuild(),标记为dirty的
  2. 被标记为dirty的renderObject就需要重建
  3. 硬件层的垂直同步信号每一帧的时候,重新实现View树的测量、布局、重绘,也就将dirty的数据处理一边

流畅度优化(分帧上屏)

  1. 复杂的列表卡顿是因为build时间太长
  2. 将Item分帧渲染 ,先用个sizewidget占位,下一帧的时候再去渲染实际的widget
  3. 问题:可能无法确定item的高度,导致列表跳动

事件分发机制(竞技场)

  1. 命中测试 hitTest Flutter中的可视对象都继承自RenderObjectWidget,事件从Widget树的根部开始,根据输入的位置信息(包括x、y坐标和触摸区域大小)进行递归的命中测试,找到所有命中的Widget。
  2. 事件路由: dispatchEvent Flutter使用了一棵渲染对象树,这棵树的节点是RenderObject,当Flutter找到了所有的命中目标后,每个目标将检查命中的位置是否在它的范围内,并将命中事件沿父子链递归向上传递,直到根节点为止。
  3. 事件处理: 当事件到达目标节点时,目标节点负责调用它的事件处理程序来执行针对该事件的自定义处理程序,处理程序负责修改UI的状态以及调用StatefulWidget类中的build方法生成新的UI。Flutter会快速地将所生成的UI更新到屏幕上,反复重复上述三个过程。
  • 在Flutter中,事件总是从最底部的可视对象开始的。这与Android的事件分发机制有所不同,Android出于性能考虑使用了“自下而上”、“自上而下”的混合事件分配方法,这意味着UI组件可能会在根据其父级层次结构路径分配其所需事件之前处理事件。Flutter的事件分发机制是一种高效的命中测试、事件路由和事件处理的方式,可以使应用程序相应更加快速和流畅
  • 将事件最终判断为什么事件 、比如双击、滑动、长按、或者点击事件 , 最后又由谁去执行 ,形象比喻成一个事件竞技场决策
  1. 我们再runApp 的时候,会启动各种跟引擎相关的binding,如GestureBinding、RendererBinding 当事件从原生页面传递过来时,会执行GestureBinding 的 handlePointerEvent 方法,进行碰撞测试, 从根renderView 开始递归向子view遍历,将符合触摸坐标的控件全部添加到HitTestResult 中,最后会把GestureBinding 自己也添加进去

  2. 遍历HitTestResult 中记录的所有节点,执行handleEvent ,任何rederObject控件createRenderObject方法中,都创建了一个RenderPointerListener事件监听器,当遍历HitTestResult中的控件去执行对应的handleEvent时 ,实际上是执行到RenderPointerListener 的 handleEvent,这里在手指触摸上,也就是Down事件,如果控件注册了事件处理器GestureRecognizer(竞技场选手) ,这里会将这个down 事件初始化进GestureRecognizer 中,并生成GestureArenaEntry (拿到门票), 然后将GestureRecognizer(竞技场选手)加入到GestureArena(竞技场)中

  3. 最后会执行到GestureBinding的handleEvent 方法 ,这时基本有控件的事件处理器都已经加入了竞技场, 开始关闭竞技场 ,接下来的事件开始在竞技场中竞争 ,竞争到事件的控件事件处理器相应的方法开始回调, 一直到Up事件时,开始关闭竞技场,打扫战场 (双击事件会请求竞技场等待不要关闭 ,判断优胜者之后再关闭)

  4. GestureDetector:手势识别器,比如单击/双击/长按/拖动,比如双击如果 300 毫秒以内用户没有产生新的点击,那么 DoubleTapGestureRecognizer 就会宣布“失败“退出竞技

isolate

  1. 花湖阅读资源的解析就放在isolate内的,因为dart是单线程模型,类似js的事件循环机制,解析不使用isolate的话会导致UI卡顿,虽然可以异步,但是解析的逻辑一直在进行,导致整个线程都在做解析
  2. 使用了一个isolate负载均衡的库,类似是一个isolate的池,解决频繁创建和销毁isolate带来的开销
  3. 在isolate内是无法使用插件的,插件无法向原生发送消息,所以做了一次转发,从isolate发到主isolate,让main isolate发送给原生,等原生处理完拿到数据时,再发回给原来的isolate
  4. 自己做个isolate缓冲池的话,怎么设计

Deferred Components 延时加载,按需加载

  1. FutureBuilder

Binding

在Flutter中,Binding是一个非常重要的概念。Binding通常指的是将Flutter的逻辑代码与底层平台进行绑定的代码,它提供了访问底层API的接口,从而使Flutter可以与平台进行通信。以下是Flutter中的几种常见的Binding:

  1. WidgetsBinding WidgetsBinding是Flutter提供的最重要的Binding之一。它是一个Singleton对象,提供了与Flutter小部件树绑定的功能。WidgetsBinding用于将UI事件传递到小部件,并调用每个小部件的build方法重新构建小部件树。

  2. SchedulerBinding SchedulerBinding是Flutter框架的计划器。通过SchedulerBinding,Flutter可以处理即将到来的任务,例如重建页面、执行动画等。SchedulerBinding包含一个队列,用于存储待处理的任务。当处理任务时,SchedulerBinding使用Timer.run方法将任务推送到微任务队列中。

  3. PaintingBinding PaintingBinding是Flutter绘画引擎的Binding。它负责处理绘画相关的任务,例如呈现布局和绘制新的一帧等。PaintingBinding使用SchedulerBinding来安排自己的工作,以便在适当的时间完成工作。

  4. PlatformBinding PlatformBinding是Flutter与平台通信的Binding。它负责创建与平台相关的对象,并在需要时与平台进行通信。通过PlatformBinding,Flutter可以与底层的平台API进行交互,例如访问文件系统、网络通信等。

  5. ServicesBinding 提供了Flutter与底层服务交互的接口。在Flutter应用程序中,有时需要与底层服务进行通信,例如获取电池电量、监听剪贴板内容、启动定位服务等。这些服务的访问通常需要使用平台特定的API,因此不能直接从Flutter代码中进行访问。通过ServicesBinding,Flutter可以将这些请求发送到服务管理器中,并接收来自服务管理器的响应。

  6. WindowBinding 为Flutter应用程序提供窗口管理相关的接口,例如设置窗口大小、调整窗口位置、处理屏幕旋转事件等等。WindowBinding还负责管理Flutter应用程序的显示上下文,以确保在Flutter应用程序与底层平台之间进行交互时窗口管理的一致性。

  7. GestureBinding 负责处理与手势相关的事件。在Flutter应用程序中,手势可以是非常重要的交互方式,例如通过轻扫或点击来进行导航、菜单选择等。GestureBinding通过在Flutter中捕获和处理事件,使应用程序可以识别和响应不同的手势。

问题

  1. iOS 上首次卡顿的问题,2.5版本已经解决,低版本的官方给的解决方案用设备跑一个渲染文件出来,但是问题依旧存在
  2. 鸿蒙设备上列表会掉帧严重,没有鸿蒙的手机,用户反馈的
  3. 小米手机的无法使用系统字体
  4. 一加手机上切到后台,再切回来的时候可能会黑屏,切到系统后台任务再次切回来就正常了
  5. iOS字重

APM统计

FP:为首次渲染的时间点。 FCP:为首次有内容渲染的时间点。 FMP:为首次有效绘制时间,启动页面加载与页面呈现首屏之间的时间。 统计方式:

  1. 页面push时,开始监听FCP和FMP

  2. 非首页则需要跳过转场动画,动画期间不做FCP、FMP检查

  3. 通过判断RenderObject首个子节点是否是有效屏(文字-RenderParagraph; 图片-RenderImage; 视频-TextureBox),来计算fcp

  4. 通过计算当前RenderObject树中内容节点占用的Rect大小是否达到有效值,来计算fmpTime时间

  5. 页面pop时,进行上报数据

    fp = 当前第一帧渲染时间 - didpush事件回调时间 fcp = 检测到当前渲染树子节点是有效子节点(文字,图片,视频) - didpush事件回调时间 fmp = 检测到当前渲染树中内容节点占用的Rect大小是都大于临界值时间 - didpush事件回调时间

FPS:滑动帧率 。60Hz设备举例说明其计算:[单帧 FPS] = 1000 / Math.max(单帧时间, 16.7 * Math.ceil(单帧时间 / 16.7)) 统计方式:

  1. 通过SchedulerBinding.instance.addTimingsCallback获取当前滑动采样数据

  2. 页面push时开启当前FPS,页面pop时停止,通过sence创建丢帧表

  3. 获取当前滑动采样数据,并且更新丢帧表,计算uiFps

    final totalCount = drawnCount + skippedCount;
    final fps = defaultDisplayRefreshRate \* drawnCount / totalCount;
    
  4. 定时(默认10秒)上报数据

    丢帧表统计每次滑动过程中,每一个区间发生了多少次丢帧。key 表示丢了几帧,value 表示发生的次数。

    final int skippedCount = frameTiming.totalSpan.inMilliseconds ~/ 16.6;
    

    例如{ 0:12, 1:5, 2:10},表示这期间: 共有12次绘制的间隔<16ms以内,没有发生丢帧。 有5次绘制回调的间隔在 (1~2) * 16ms之间。 有10次绘制回调的间隔在 (3~6) * 16ms*4之间。

channel数据: 统计方式:

  1. 通过hook channel通道数据handlePlatformMessage,send来获取通道数据
  2. 计算method耗时
  3. 定时上报数据

异常统计:

  1. 利用runZonedGuarded来监测全局异常
  2. 利用ErrorWidget.builder监测UI异常(白屏)