Flutter面试题整理
Flutter面试题整理
Flutter有什么优势
跨平台开发
- 一次编写,多平台部署:Flutter 允许开发者使用 Dart 语言编写一次代码,就能同时在 iOS 和 Android 等多个主流平台上运行。这大大减少了开发成本和时间,避免了为不同平台分别开发的重复工作。
- 统一的开发体验:开发者无需在不同的开发语言(如 iOS 的 Objective - C/Swift 和 Android 的 Java/Kotlin)之间切换,使用 Dart 和 Flutter 框架就能完成全平台的开发。
高性能
- 接近原生的性能:Flutter 使用 Skia 图形引擎直接渲染 UI,绕过了原生系统的渲染机制,能够实现接近原生应用的流畅度和响应速度。在处理复杂的动画和高帧率的界面时,表现出色。
- 即时编译(JIT)和提前编译(AOT):在开发阶段,Flutter 采用 JIT 编译,支持热重载(Hot Reload)功能。开发者修改代码后,能在瞬间看到修改后的效果,无需重新启动应用,大大提高了开发效率。在发布阶段,使用 AOT 编译,将代码编译成原生机器码,进一步提升应用的运行性能。
美观的UI设计
- 丰富的组件库:Flutter 提供了大量精美的、可定制的 UI 组件,这些组件遵循 Material Design(安卓)和 Cupertino(iOS)设计规范,能够轻松创建出符合不同平台风格的界面。开发者可以根据需求对组件进行定制,实现独特的视觉效果。
- 强大的动画支持:Flutter 内置了丰富的动画 API,开发者可以轻松创建各种复杂的动画效果,如渐变、旋转、缩放等。这些动画效果可以增强用户体验,使应用更加生动有趣。
生态不断完善
- 活跃的社区支持:Flutter 拥有一个庞大且活跃的开发者社区,开发者可以在社区中分享经验、解决问题、获取最新的技术资讯。
Flutter实现跨平台的原理
Flutter实现跨平台主要依赖于其独特的架构设计,涵盖了框架层、引擎层和平台嵌入层。
- 框架层:Flutter框架提供了丰富的UI组件库,这些组件是跨平台统一的。例如,开发者使用Text、Button等组件时,无需针对不同平台进行特殊处理。当开发者编写UI代码时,使用的都是Flutter自身定义的抽象组件,这些组件不依赖于具体的原生平台。
- 引擎层:Flutter引擎使用C++编写,是Flutter实现跨平台的核心。它负责图形渲染、输入处理、Dart运行时等关键功能。在渲染方面,采用了Skia图形库,这是一个开源的2D图形库,被广泛应用于Chrome浏览器等项目中。Skia能够在不同的操作系统上提供一致的图形渲染效果,无论是在iOS还是Android系统,都能保证相同的UI表现。
- 平台嵌入层:该层负责将Flutter应用嵌入到不同的原生平台中。在iOS上,Flutter应用以Objective - C或Swift代码为基础进行嵌入;在Android上,则以Java或Kotlin代码为基础。平台嵌入层为Flutter应用提供了与原生系统交互的接口,例如访问设备的摄像头、文件系统等。
Flutter的特性
高性能:
- Flutter采用即时编译(JIT)和提前编译(AOT)两种编译模式。在开发阶段,JIT模式允许实现热重载功能,开发者修改代码后能快速看到效果,大大提高了开发效率。在发布阶段,AOT模式将Dart代码编译成原生机器码,避免了运行时的解释执行过程,使得应用的启动速度和运行性能都非常出色。
- 由于Flutter自带渲染引擎,直接在GPU上进行图形绘制,减少了与原生系统的通信开销,进一步提升了性能。
美观的UI:
- Flutter提供了丰富的、精美的UI组件,并且支持高度自定义。开发者可以根据需求对组件的样式、动画效果等进行定制,实现各种独特的用户界面。
内置了强大的动画系统,能够轻松实现流畅的过渡动画和交互效果,为用户带来出色的视觉体验。
响应式编程:
采用响应式编程模型,当数据发生变化时,UI会自动更新。开发者只需要关注数据的变化,而无需手动管理UI的更新过程。例如,使用StatefulWidget可以轻松实现状态管理,当状态改变时,Flutter会自动重建相关的UI部分。
丰富的插件生态:
Flutter拥有庞大的插件生态系统,开发者可以通过引入各种插件来快速实现一些复杂的功能,如支付、地图、推送通知等。这些插件在不同平台上具有一致的API,方便开发者进行跨平台开发。
高效的开发效率:
- 热重载功能使得开发者在修改代码后,无需重新启动应用就能看到修改后的效果,大大缩短了开发周期。
- Dart语言简单易学,具有静态类型检查和垃圾回收机制,提高了代码的可读性和可维护性。同时,Dart语言支持异步编程,能够高效处理网络请求等异步操作。
在Flutter中如何与原生通信
Flutter与原生通信基于消息通道(Message Channel),它是两者之间传递消息的桥梁。Flutter提供了三种不同类型的消息通道,分别适用于不同的通信场景。
基本消息通道(BasicMessageChannel)
- 适用场景:用于传递简单的消息,如字符串、数字、布尔值等基本数据类型,也可以传递序列化后的复杂数据。
- Flutter端和Android端发送消息使用messageChannel.send()方法,接收消息使用messageChannel.setMessageHandler方法
方法调用通道(MethodChannel)
- 适用场景:用于调用原生方法并获取返回值,适用于需要执行特定原生功能的场景,如调用相机、获取设备信息等。
- Flutter端使用methodChannel.invokeMethod方法调用原生方法,原生端使用setMethodCallHandler处理相关逻辑
事件通道(EventChannel)
- 适用场景:用于从原生端向Flutter端发送连续的事件流,如传感器数据、网络状态变化等。
- Flutter端使用 EventChannel.receiveBroadcastStream() 接收事件流。listen() 方法处理事件和错误。原生端使用setStreamHandler 设置事件监听器。onListen 启动事件推送逻辑。
请简述Flutter的渲染机制
🧩 1. 构建(Build)
- 作用:根据当前的状态构建 Widget 树。
- 过程:Flutter 会调用 build() 方法,生成一棵描述 UI 的 Widget 树。
- 特点:这是声明式 UI 的核心,Widget 是不可变的,仅用于描述界面。
🧱 2. 布局(Layout)
- 作用:确定每个元素的大小和位置。
- 过程:将 Widget 树转换为 RenderObject 树,并根据父子关系进行尺寸和位置的计算。
- 特点:涉及 performLayout(),从父节点向子节点传递约束条件。
🎨 3. 绘制(Paint)
- 作用:将元素绘制到屏幕上。
- 过程:RenderObject 将自身绘制到 Canvas 上,形成 Layer。
- 特点:可以进行视觉效果的定制,如颜色、阴影、边框等。
🧵 4. 合成(Compositing)
- 作用:将多个 Layer 合并为一个图像。
- 过程:Flutter 使用 Layer Tree 来管理不同的绘制层,并将其合成到一个场景中。
- 特点:优化性能,支持硬件加速和平台特性。
📺 5. 栅格化(Rasterization)
- 作用:将合成后的图像转换为像素并显示在屏幕上。
- 过程:由 Skia 图形引擎完成,将 Layer Tree 渲染为实际的像素图。
- 特点:这是最终呈现阶段,直接影响用户看到的界面。
Flutter 的三棵树是什么
Widget 树
- Widget 是 Flutter 中用于描述 UI 元素配置的不可变对象,它包含了 UI 的外观、属性等信息。Widget 树是由一系列 Widget 组成的层次结构,描述了应用界面的逻辑结构。
- 例如,在构建一个简单的按钮界面时,会创建一个 MaterialApp Widget 作为根 Widget,其内部包含 Scaffold Widget,Scaffold 中又包含 AppBar 和 Center Widget,Center 里包含 ElevatedButton Widget,这些 Widget 共同构成了 Widget 树。
- 由于 Widget 是不可变的,当 UI 状态发生变化时,Flutter 会创建新的 Widget 来替换旧的 Widget,以保证数据的一致性和可维护性。
Element 树
- Element 是 Widget 的实例化对象,它将 Widget 的配置信息与实际的渲染过程关联起来。Element 树是根据 Widget 树创建的,每个 Widget 对应一个或多个 Element。
- 当 Widget 树发生变化时,Flutter 会对比新旧 Widget 树,尽可能复用已有的 Element,只对发生变化的部分进行更新,这种机制称为“Widget 热替换”,可以提高性能。
- 例如,当按钮的文本发生变化时,Flutter 会创建一个新的 ElevatedButton Widget,但会复用对应的 Element,只更新 Element 中与文本相关的属性。
RenderObject 树
- RenderObject 负责实际的布局和绘制工作,它包含了元素的大小、位置、颜色等具体的渲染信息。RenderObject 树是根据 Element 树创建的,每个 Element 对应一个 RenderObject。
- RenderObject 树会根据布局算法计算每个元素的大小和位置,并将这些信息传递给底层的渲染引擎进行绘制。
- 例如,RenderBox 是一种常见的 RenderObject,它可以计算和管理元素的矩形边界,用于布局和绘制。
Flutter 的三棵树为什么比原生性能低
Flutter 性能不一定比原生低,在某些场景下性能表现良好,但在一些情况下可能存在性能相对较弱的情况,原因如下:
跨平台抽象层
- Flutter 为了实现跨平台的特性,引入了一层抽象层,这层抽象层需要处理不同平台的差异,可能会带来一定的性能开销。
- 例如,在处理一些底层硬件特性时,需要通过抽象层进行适配,这可能会比原生直接调用硬件接口的效率低。
渲染机制
- Flutter 使用自己的渲染引擎 Skia 进行绘制,虽然 Skia 是一个高性能的渲染引擎,但在某些复杂场景下,可能不如原生平台的渲染引擎针对特定硬件进行优化的效果好。
- 例如,在处理一些复杂的 3D 图形或视频渲染时,原生平台可能有更专业的硬件加速支持,而 Flutter 的通用渲染机制可能无法充分利用这些硬件优势。
内存管理
- Flutter 的内存管理机制与原生平台有所不同,在某些情况下可能会导致内存占用较高。
- 例如,Flutter 的垃圾回收机制可能会在特定时刻触发,导致短暂的性能卡顿,而原生平台的内存管理通常更加精细和高效。
请介绍flutter的启动流程
原生系统启动
- 当用户点击应用图标时,原生系统(如 Android 或 iOS)开始启动应用。
- Android 端:系统会创建一个新的 Android Activity,这是 Android 应用的基本组件,用于承载用户界面。Activity 会加载 Flutter 的引擎库,该库包含了 Flutter 运行所需的核心代码和资源。
- iOS 端:系统会创建一个 UIViewController,它是 iOS 应用中管理界面的核心类。同样,也会加载 Flutter 的引擎库,为后续 Flutter 代码的运行做好准备。
Flutter 引擎初始化
- Flutter 引擎是 Flutter 框架的核心,负责渲染、动画、输入处理等底层操作。在原生系统加载引擎库后,会进行引擎的初始化工作。
- 初始化 Dart 虚拟机(Dart VM),Dart 是 Flutter 开发使用的编程语言,Dart VM 负责执行 Dart 代码。
加载 Flutter 的运行时环境,包括各种内置库和工具,为后续 Dart 代码的执行提供支持。
加载 Dart 代码
- Flutter 应用的主要业务逻辑是用 Dart 语言编写的。在引擎初始化完成后,会加载并执行 Dart 代码的入口文件,一般是 main.dart 文件。
- 在 main.dart 中,通常会调用 runApp 函数,该函数是 Flutter 应用的启动入口。
构建 Widget 树
- runApp 函数接收一个 Widget 对象作为参数。Widget 是 Flutter 中用于构建用户界面的基本单元。
- Flutter 会根据传入的 Widget 对象开始构建 Widget 树。Widget 树是一个层级结构,每个 Widget 可以包含子 Widget,通过嵌套组合形成复杂的界面。
- 在构建过程中,Flutter 会调用 Widget 的 build 方法,该方法会返回一个新的 Widget 或 Widget 组合,用于描述界面的一部分。
生成 Element 树
- Widget 树只是描述了界面的结构,真正用于渲染的是 Element 树。Flutter 会根据 Widget 树生成对应的 Element 树。
- Element 是 Widget 的实例化对象,它包含了 Widget 的配置信息和状态信息。每个 Widget 都会对应一个或多个 Element,Element 树的结构与 Widget 树基本一致。
生成 RenderObject 树
- Element 树还不能直接进行渲染,需要进一步生成 RenderObject 树。RenderObject 负责具体的布局和绘制操作。
- Flutter 会根据 Element 树生成 RenderObject 树,每个 Element 会对应一个 RenderObject。RenderObject 会根据自身的属性和约束条件进行布局计算,确定每个组件的位置和大小,然后进行绘制操作,将界面渲染到屏幕上。
绘制到屏幕
- 经过前面几个阶段的准备,RenderObject 树已经完成了布局和绘制的计算。最后,Flutter 会将 RenderObject 树的绘制结果提交到屏幕上显示。
- 这个过程涉及到与原生系统的图形渲染接口进行交互,将绘制数据传递给系统的图形处理器(GPU),由 GPU 完成最终的渲染和显示。
请介绍flutter_boost的原理
含义
- FlutterBoost 是一个专为 Flutter 与原生混合开发场景设计的路由管理库,最初由 阿里闲鱼技术团队开源。它的目标是解决在大型原生 App 中渐进式接入 Flutter 时遇到的路由管理、页面跳转、生命周期同步等问题。
混合栈管理
- 双栈架构:FlutterBoost 采用了原生导航栈和 Flutter 导航栈的双栈架构。原生导航栈负责管理原生页面的入栈和出栈操作,而 Flutter 导航栈则管理 Flutter 页面。这种架构使得原生页面和 Flutter 页面可以独立进行导航操作,同时又能相互协作。
- 栈同步:为了保证两个栈的状态一致,FlutterBoost 会在页面切换时进行栈同步。当打开一个新的 Flutter 页面时,FlutterBoost 会在原生导航栈中压入一个包含 Flutter 引擎的容器页面,同时在 Flutter 导航栈中压入对应的 Flutter 页面。当关闭页面时,两个栈会同时进行出栈操作。
Flutter 引擎管理
- 单引擎多实例:FlutterBoost 使用单 Flutter 引擎多实例的方式来管理 Flutter 页面。一个 Flutter 引擎可以同时支持多个 Flutter 页面实例,这样可以减少资源消耗和启动时间。当需要打开一个新的 Flutter 页面时,FlutterBoost 会复用已有的 Flutter 引擎,只创建新的 Flutter 页面实例。
- 引擎预加载:为了提高 Flutter 页面的启动速度,FlutterBoost 支持引擎预加载。在应用启动时或者空闲时,提前初始化 Flutter 引擎,当需要打开 Flutter 页面时,直接使用预加载的引擎,避免了引擎初始化的时间开销。
通信机制
- 平台通道:FlutterBoost 利用 Flutter 提供的平台通道(Platform Channel)实现原生代码和 Flutter 代码之间的通信。平台通道分为三种类型:基本消息通道(BasicMessageChannel)、方法通道(MethodChannel)和事件通道(EventChannel)。
- 方法通道:主要用于原生和 Flutter 之间的方法调用。例如,当原生页面需要打开一个 Flutter 页面时,会通过方法通道调用 Flutter 端的相应方法;反之,Flutter 页面需要调用原生功能时,也可以通过方法通道调用原生端的方法。
- 事件通道:用于原生向 Flutter 发送事件流,例如原生端的网络状态变化、传感器数据等,可以通过事件通道实时传递给 Flutter 页面。
- 基本消息通道:用于传递简单的消息,如字符串、数字等。
路由管理
- 统一路由表:FlutterBoost 维护了一个统一的路由表,用于管理原生页面和 Flutter 页面的路由信息。在打开页面时,无论是原生页面还是 Flutter 页面,都可以通过路由表中的路由名称进行跳转。
- 路由拦截:支持路由拦截功能,可以在页面跳转前进行权限验证、参数处理等操作。例如,在打开一个需要登录的 Flutter 页面时,可以在路由拦截器中检查用户是否登录,如果未登录则跳转到登录页面。
生命周期管理
- 同步生命周期:FlutterBoost 会同步原生页面和 Flutter 页面的生命周期。当原生页面进入前台、后台或者销毁时,会通过平台通道通知 Flutter 页面,让 Flutter 页面相应地处理生命周期事件,如暂停、恢复、销毁等。这样可以保证 Flutter 页面在不同的生命周期状态下正确地处理资源和数据。
pubspec.yaml文件的作用是什么
依赖管理
- 它是管理项目依赖的核心文件。在Flutter开发中,我们常常需要使用第三方库来实现各种功能,比如网络请求、状态管理等。通过在pubspec.yaml文件的dependencies部分添加依赖项,就能方便地引入这些第三方库。
资源管理
- 该文件可用于管理项目中的资源,像图片、字体等。在assets部分列出资源路径,就能让Flutter知道项目需要使用哪些资源。
项目元数据
- pubspec.yaml文件还包含了项目的元数据,如项目名称、版本号、描述等。这些信息有助于其他开发者了解项目,也在发布项目到Pub仓库时起到重要作用。
1
2
3name: my_flutter_app
description: A new Flutter application.
version: 1.0.0+1
插件配置
- 如果项目使用了Flutter插件,pubspec.yaml文件也会对其进行配置。插件依赖同样在dependencies部分声明,Flutter会根据这些配置来正确集成插件功能。
1
2
3
4dependencies:
flutter:
sdk: flutter
camera: ^0.9.4+1
在Flutter中网络请求是用什么实现的
dart:io库
- dart:io 是 Dart 内置的库,可用于实现网络请求。它提供了 HttpClient 类,能进行 HTTP 请求。
- dart:io 是 Dart 基础库的一部分,无需额外依赖,适合对底层网络操作有需求的场景,但使用起来相对复杂,需要手动处理很多细节,如请求头、响应状态码等。
http 包
- http 是 Flutter 社区广泛使用的网络请求库,它对 dart:io 进行了封装,提供了更简洁易用的 API。
dio 包
- dio 是一个强大的 HTTP 客户端,支持拦截器、请求取消、文件上传下载等高级功能。
- dio 功能丰富,适合对网络请求有较高要求的场景,如需要对请求和响应进行拦截处理、上传下载大文件等。
请说明StatefulWidget和StatelessWidget的区别
可变性
- StatelessWidget:是不可变的,一旦创建,其属性(即final变量)就不能再改变。它没有内部状态的变化,组件的外观和行为完全由其构造函数传入的参数决定。例如,一个简单的文本组件,只要传入的文本内容和样式确定,它就不会自行改变外观。
- StatefulWidget:是可变的,它可以在生命周期内改变其状态。当状态发生变化时,组件会重新构建以反映这些变化。比如一个计数器组件,用户点击按钮时,计数器的值会增加,组件的外观也会相应更新。
状态管理
- StatelessWidget:没有自己的状态,它不管理任何可变的数据。如果需要更新组件,必须通过父组件重新创建该组件并传入新的参数。
- StatefulWidget:有自己的状态,状态存储在与其关联的State对象中。可以通过调用setState()方法来更新状态,调用setState()会触发build()方法重新构建组件,从而更新UI。
生命周期复杂度
- StatelessWidget:生命周期相对简单,只有一个build()方法,当组件需要构建时,Flutter会调用build()方法返回一个Widget树。
- StatefulWidget:生命周期较为复杂,除了build()方法外,还有initState()、didChangeDependencies()、didUpdateWidget()、deactivate()、dispose()等方法。这些方法在不同的阶段被调用,开发者可以在这些方法中执行不同的操作,如初始化状态、监听依赖变化、释放资源等。
Flutter中如何堆叠组件(Stack)以及如何定位(Positioned)
在Flutter里,Stack 组件可用于堆叠其他组件,Positioned 组件则能对 Stack 内的子组件进行定位。
堆叠组件(Stack)
- Stack 组件允许将多个子组件堆叠在一起,默认情况下,子组件会按照添加顺序依次堆叠,后添加的组件会覆盖先添加的组件。
定位组件(Positioned)
- Positioned 组件用于在 Stack 中对其子组件进行定位。它可以指定子组件相对于 Stack 边界的上、下、左、右偏移量。
注意事项
- Positioned 组件只能作为 Stack 的直接子组件使用。
- 当使用 Positioned 时,如果同时指定了水平(left 和 right)或垂直(top 和 bottom)方向的偏移量,还可以通过 width 和 height 属性来确定子组件的大小;若只指定了一个方向的偏移量,子组件的大小会根据内容自动调整。
Flutter和原生混合开发时,页面路由如何管理(安卓)
使用FlutterActivity和FlutterFragment
- FlutterActivity:当需要从原生页面跳转到Flutter页面时,可以通过启动一个新的FlutterActivity来实现。FlutterActivity是Flutter为Android平台提供的一个Activity子类,它可以加载并显示Flutter页面。
- FlutterFragment:若要在原生页面中嵌入Flutter页面,可以使用FlutterFragment。这样可以将Flutter页面作为一个组件嵌入到原生的布局中。
路由通信
- 原生到Flutter:可以通过在启动FlutterActivity时传递参数,Flutter端使用WidgetsBinding的addPostFrameCallback方法在页面初始化完成后获取这些参数,从而实现页面跳转。
- Flutter到原生:Flutter可以通过MethodChannel调用原生方法,原生代码接收到调用后进行相应的页面跳转。
为什么Flutter包内存比原生大
框架层面
- 集成完整框架:Flutter应用会将Flutter运行时和Dart SDK完整地打包进应用中。Flutter运行时包含了渲染引擎、事件处理、动画系统等核心功能,是应用运行的基础支撑。Dart SDK则提供了丰富的类库和工具,用于开发各种功能。相比之下,原生开发(如Android的Java/Kotlin和iOS的Objective - C/Swift),系统本身已经提供了很多基础框架和运行环境,应用只需要调用系统提供的功能,无需将大量框架代码打包进应用,所以包体积相对较小。
- 跨平台特性:Flutter的目标是实现一次开发,多平台部署。为了实现这一特性,它需要在代码中处理不同平台的差异,包含了适配多种平台的代码逻辑和资源。例如,在渲染方面,Flutter要确保在不同的屏幕分辨率、操作系统版本和硬件设备上都能有一致的显示效果,这就需要包含更多的代码和资源,从而增加了包的大小。而原生开发是针对特定平台进行开发的,开发者可以更精准地控制代码和资源的使用,避免了不必要的跨平台适配代码。
资源层面
- 自带字体和图标:Flutter默认会自带一些字体和图标库,以保证在不同平台上有一致的视觉效果。这些字体和图标资源会占据一定的空间。在原生开发中,开发者可以根据需要选择使用系统字体和图标,或者只引入必要的自定义字体和图标,从而减少资源占用。
- 资源冗余:在开发过程中,Flutter应用可能会包含一些未使用的资源。由于Flutter的构建系统在打包时可能无法完全准确地识别哪些资源是真正需要的,导致一些无用的图片、音频等资源也被打包进应用,增加了包的体积。而原生开发有更精细的资源管理机制,开发者可以更方便地清理和优化资源,减少不必要的资源冗余。
编译层面
- AOT编译:Flutter在发布应用时通常采用Ahead - Of - Time(AOT)编译,将Dart代码编译成机器码。虽然AOT编译可以提高应用的运行性能,但会生成相对较大的二进制文件。因为它需要将应用的所有代码和依赖项都编译成可执行的机器码,包含了大量的指令和数据。而原生开发在编译时可以根据平台的特点进行更优化的处理,生成的二进制文件相对较小。
- 代码膨胀:Flutter的一些特性,如泛型、异步编程等,在编译过程中可能会导致代码膨胀。例如,泛型在编译时会进行类型擦除和具体化处理,生成更多的代码来实现类型安全。异步编程中的Future和Stream等概念,也会引入额外的代码来处理异步操作。这些额外的代码会增加包的体积。而原生开发语言在处理类似功能时,可能有更简洁的实现方式,减少了代码膨胀的问题。
Flutter如何实现国际化
- Flutter 国际化主要通过 flutter_localizations 和 intl 包实现。我们使用 .arb 文件定义多语言资源,然后通过 flutter gen-l10n 工具生成 AppLocalizations 类。在 MaterialApp 中配置 localizationsDelegates 和 supportedLocales,即可支持多语言切换。也可以通过设置 locale 属性实现动态语言切换。
Flutter如何自定义组件
无状态组件(StatelessWidget)
- 创建一个类:继承自StatelessWidget。
- 重写build方法:在这个方法里构建组件的UI。
有状态组件(StatefulWidget)
- 创建一个继承自StatefulWidget的类:这个类主要用于定义组件的配置信息。
- 创建一个继承自State的类:这个类用于管理组件的状态和构建UI。
在Flutter中如何自定义画板,需要继承什么类
- 继承CustomPainter类:创建一个类继承自CustomPainter,并重写paint和shouldRepaint方法。
- paint方法:用于绘制具体的图形和内容。它接收两个参数,Canvas对象用于实际绘制操作,Size对象表示绘制区域的大小。
- shouldRepaint方法:用于判断是否需要重新绘制。当返回true时,会触发重绘;返回false则不会重绘。
- 使用CustomPaint组件:在界面中使用CustomPaint组件,并将自定义的CustomPainter实例传递给painter属性。
在Flutter中如何避免重绘
使用 const 构造函数
- 在创建不可变的 widget 时,使用 const 构造函数。Flutter 会对 const 实例进行优化,在 widget 树重建时,如果 const widget 的类型和参数没有变化,Flutter 不会重新创建和绘制这个 widget。
使用 const 集合
- 如果在 widget 中使用集合(如 List、Map 等),可以使用 const 集合。这样在 widget 重建时,只要集合的内容没有变化,就不会触发重绘。
使用 const 修饰自定义 widget
- 对于自定义的 widget,如果它是不可变的,也可以使用 const 修饰其构造函数。
使用 shouldRepaint 方法
- 对于 CustomPainter 类,重写 shouldRepaint 方法。该方法返回一个布尔值,用于决定是否需要重绘。如果返回 false,则不会触发重绘。
使用 AutomaticKeepAliveClientMixin
- 在有状态的 widget 中,如果需要保持子 widget 的状态,避免在切换时重绘,可以使用 AutomaticKeepAliveClientMixin。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class MyKeepAliveWidget extends StatefulWidget {
const MyKeepAliveWidget({Key? key}) : super(key: key);
@override
_MyKeepAliveWidgetState createState() => _MyKeepAliveWidgetState();
}
class _MyKeepAliveWidgetState extends State<MyKeepAliveWidget> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return Text('保持状态的 widget');
}
@override
bool get wantKeepAlive => true;
}
使用 ValueListenableBuilder 和 StreamBuilder
- ValueListenableBuilder:当需要监听 ValueNotifier 的值变化时,使用 ValueListenableBuilder。它只会在值发生变化时重建部分 widget,而不是整个 widget 树。
- StreamBuilder:当需要监听 Stream 的数据变化时,使用 StreamBuilder。它会根据 Stream 发出的数据更新部分 widget。
在Flutter中异步的实现方式有哪些
Future
- Future 代表一个异步操作的结果,它可以处于未完成、完成(成功或失败)状态。可以使用 async 和 await 关键字来处理 Future。
Stream
- Stream 是一系列异步事件的序列,类似于事件流。可以使用 StreamBuilder 或 async 和 await for 来处理 Stream。
1
2
3
4
5
6
7
8
9
10
11
12Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
await for (int value in countStream(5)) {
print(value);
}
} - countStream 函数是一个异步生成器函数,使用 async* 关键字标记,返回一个 Stream
。 - yield 关键字用于产生一个值,每次产生一个值后,函数会暂停,直到下一个值被请求。
- await for 用于遍历 Stream 中的每个值。
Isolate
- Isolate 是Flutter中的独立执行线程,每个 Isolate 都有自己的内存堆,避免了共享内存带来的并发问题。可以使用 compute 函数或 Isolate.spawn 来创建和管理 Isolate。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import 'dart:isolate';
// 耗时任务
int heavyTask(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return sum;
}
void main() async {
int result = await compute(heavyTask, 1000000);
print(result);
} - heavyTask 函数是一个耗时任务,计算从 0 到 n 的所有整数的和。
- compute 函数用于在一个新的 Isolate 中执行 heavyTask 函数,并返回结果。
在Flutter中调用setState后组件的生命周期是怎样的
调用setState
- 当在State类里调用setState方法时,会通知Flutter框架当前状态已改变,需要重新构建组件。
调用build方法
- setState被调用后,Flutter框架会调用State类的build方法。在build方法中,会根据新的状态创建新的Widget树。
- 注意,build方法会返回一个新的Widget实例,但这并不意味着会创建新的RenderObject和Element。Flutter框架会比较新旧Widget树,尽可能复用已有的RenderObject和Element。
组件更新
- Element更新:Flutter框架会将新的Widget与对应的Element关联起来。Element是Widget的实例化对象,它会根据新的Widget属性更新自身状态。
- RenderObject更新:如果Widget的属性发生了变化,Element会通知对应的RenderObject进行更新。RenderObject负责布局和绘制,它会根据新的属性重新计算布局和绘制内容。
布局和绘制
- 布局阶段:RenderObject会根据新的属性重新计算布局信息,确定自身的大小和位置。
- 绘制阶段:RenderObject会根据布局信息进行绘制,将内容显示在屏幕上。
如何在Flutter中调试内存分布
使用DevTools
- DevTools 是 Flutter 官方提供的一套强大的调试工具集,其中包含了专门用于内存分析的工具。
- 启动应用并连接到 DevTools
- 首先,在终端中运行 Flutter 应用:flutter run。
- 然后打开 DevTools,可以通过在终端中运行 flutter pub global run devtools 命令来启动 DevTools 服务器,之后在浏览器中访问 http://localhost:9100 打开 DevTools 界面。
- 在 DevTools 界面中,点击“Memory”选项卡,将 DevTools 连接到正在运行的 Flutter 应用。
- 进行内存快照
- 在 Memory 面板中,点击“Take snapshot”按钮,DevTools 会捕获应用当前的内存状态,生成一个内存快照。
- 快照包含了应用中所有对象的信息,如对象的类型、数量、占用的内存大小等。
- 分析内存快照
- 可以查看不同类型对象的内存占用情况,找出占用内存较大的对象类型。
- 通过“Dominators”视图,可以查看对象之间的引用关系,分析哪些对象持有其他对象的引用,从而找出可能存在的内存泄漏点。
- 还可以使用“Diff”功能,比较两个不同时间点的内存快照,找出在这期间新创建或销毁的对象。
使用Flutter Inspector
Flutter Inspector 也可以辅助调试内存分布。
- 启动 Flutter Inspector
- 在 Android Studio 或 Visual Studio Code 中运行 Flutter 应用后,点击工具栏中的“Flutter Inspector”按钮,打开 Flutter Inspector 面板。
- 查看 Widget 树和内存信息
- 在 Flutter Inspector 中,可以查看应用的 Widget 树结构,了解各个 Widget 的层级关系。
- 通过分析 Widget 树,可以找出一些可能导致内存占用过高的 Widget,例如包含大量子 Widget 的复杂容器 Widget。
简述 Flutter 中的三棵树,当有数据刷新时,这三棵树会如何变化
🌳 Flutter 的三棵树结构
Widget 树(配置树)
- 描述 UI 的结构和样式,是不可变的。
- 每次调用 build() 方法时都会重新创建新的 Widget 树。
- Widget 是轻量级的,仅用于描述 UI。
Element 树(桥接树) - 是 Widget 的实例化对象,连接 Widget 和 RenderObject。
- 负责管理生命周期、更新逻辑和状态。
- 每个 Widget 对应一个 Element,通过 createElement() 创建。
RenderObject 树(渲染树) - 负责实际的布局和绘制。
- 包含尺寸、位置、绘制逻辑等信息。
- 是性能关键部分,尽量避免频繁重建。
🔄 数据刷新时三棵树的变化
Widget 树:
- 会重新构建,生成新的 Widget 实例。
- 由于 Widget 是不可变的,每次刷新都重新创建。
Element 树: - Flutter 会比较新旧 Widget 树,决定是否复用 Element。
- 如果 Widget 类型相同,则复用 Element 并调用 update()。
- 如果类型不同,则销毁旧 Element,创建新 Element。
RenderObject 树: - 如果 Element 被复用,RenderObject 通常也会复用。
- 只有当布局或绘制信息发生变化时,才会更新 RenderObject。
- 避免不必要的重建以提升性能。
请介绍Flutter中的消息队列
在Flutter中,消息队列是其事件循环机制的重要组成部分,主要用于管理和调度异步任务,确保应用程序能够高效、有序地处理各种事件。Flutter中有两种主要的消息队列:微任务队列(Microtask Queue)和事件队列(Event Queue)。
微任务队列(Microtask Queue)
- 定义:微任务队列是一个先进先出(FIFO)的队列,用于存放需要尽快执行的微小任务。这些任务通常是一些非常轻量级的操作,并且需要在当前事件处理完成后立即执行。
- 执行时机:微任务队列的优先级高于事件队列。当一个事件处理完成后,Flutter会首先检查微任务队列,如果队列中有任务,会依次执行队列中的所有微任务,直到微任务队列为空,才会从事件队列中取出下一个任务执行。
- 使用场景:常用于需要在当前操作完成后立即执行的后续操作,比如一些状态更新后的回调。
1
2
3
4
5
6
7
8
9
10
11
12import 'dart:async';
void main() {
print('Start');
// 添加微任务
scheduleMicrotask(() {
print('Microtask executed');
});
print('End');
} - 在上述代码中,scheduleMicrotask 函数用于将一个微任务添加到微任务队列中。程序的输出顺序会是 Start、End、Microtask executed,这是因为微任务会在当前事件(这里是 main 函数的同步代码)执行完成后立即执行。
事件队列(Event Queue)
- 定义:事件队列也是一个先进先出(FIFO)的队列,用于存放各种异步事件,如定时器事件、I/O 事件、用户输入事件等。
- 执行时机:当微任务队列为空时,Flutter会从事件队列中取出一个任务并执行。事件队列的任务执行是按顺序进行的,每次只执行一个任务,执行完成后再检查微任务队列,若微任务队列不为空则先执行微任务,然后再从事件队列中取下一个任务。
- 使用场景:处理各种异步操作,如网络请求、文件读写、动画帧更新等。
1
2
3
4
5
6
7
8
9
10
11
12import 'dart:async';
void main() {
print('Start');
// 添加事件队列任务
Future.delayed(Duration(seconds: 1), () {
print('Event task executed');
});
print('End');
} - 在上述代码中,Future.delayed 函数会将一个任务添加到事件队列中,该任务会在 1 秒后执行。程序的输出顺序会是 Start、End,然后等待 1 秒后输出 Event task executed。
事件循环机制
Flutter的事件循环机制不断地从微任务队列和事件队列中取出任务并执行,其基本流程如下:
- 执行当前事件的同步代码。
- 检查微任务队列,如果队列中有任务,依次执行所有微任务,直到微任务队列为空。
- 从事件队列中取出一个任务并执行。
- 重复步骤 2 和 3,不断循环。
通过这种方式,Flutter能够高效地处理各种异步任务,保证应用程序的流畅运行。
请比较Flutter和传统跨端技术的区别
渲染机制
- Flutter:采用Skia图形引擎直接渲染,不依赖原生控件。它可以在不同平台上实现一致的渲染效果,能精准控制每一个像素,渲染性能高,动画流畅度好。例如在实现复杂的交互动画时,能快速响应并呈现出细腻的视觉效果。
- 传统跨端技术:通常是通过桥接层调用原生控件来实现界面渲染。这就导致不同平台的原生控件在样式和交互上可能存在差异,需要针对不同平台进行适配。而且由于涉及到跨层通信,在性能上可能会有一定的损耗,尤其是在处理复杂动画时,容易出现卡顿现象。
性能表现
- Flutter:由于直接使用Skia引擎渲染,避免了跨层通信的性能损耗,在启动速度、界面响应速度等方面表现出色。无论是在低端还是高端设备上,都能保持较好的性能。例如,打开一个复杂的Flutter应用,几乎可以瞬间完成加载。
- 传统跨端技术:受限于桥接层的性能瓶颈,在处理大量数据或复杂操作时,可能会出现性能下降的情况。尤其是在低端设备上,应用的启动时间和响应速度会明显变慢。
代码复用性
- Flutter:代码可以在iOS和Android平台上高度复用,一套代码可以同时适配两个平台,大大减少了开发和维护的工作量。例如,业务逻辑和界面代码可以在两个平台上共享,只需要进行少量的平台特定配置。
- 传统跨端技术:虽然也能实现一定程度的代码复用,但由于需要处理不同平台的差异,部分代码可能需要针对不同平台进行单独编写和优化,代码复用率相对较低。
请解释Flutter生命周期中deactivate、dispose阶段是什么
deactivate阶段
- 触发时机:当State对象从渲染树中暂时移除时,会触发deactivate阶段。这通常发生在页面导航时,例如从当前页面跳转到下一个页面,当前页面的State对象就会进入deactivate阶段。另外,当使用GlobalKey移动一个StatefulWidget时,也会触发该对象对应State的deactivate阶段。
- 作用:此阶段主要用于执行一些临时性的清理工作。在这个阶段,State对象仍然是“活跃”的,它可以重新插入到渲染树中。例如,当用户从下一个页面返回当前页面时,处于deactivate状态的State对象可以重新激活并插入到渲染树中。
dispose阶段
- 触发时机:当State对象被永久地从渲染树中移除,并且不会再被使用时,就会触发dispose阶段。一般在页面销毁时,如关闭当前页面,对应的State对象会进入dispose阶段。
- 作用:该阶段主要用于进行最终的资源清理工作,确保不会有资源泄漏。例如,需要释放所有订阅的事件流、取消所有定时器等。一旦进入dispose阶段,State对象就不能再重新插入到渲染树中,它的生命周期也就结束了。
请说明Flutter渲染和原生客户端的区别
渲染架构
- Flutter:采用自绘引擎Skia进行渲染。Skia是一个功能强大的2D图形库,Flutter直接使用Skia绘制UI,绕过了原生系统的渲染机制。这使得Flutter应用在不同平台上能保持高度一致的视觉效果,因为它不依赖于各平台的原生渲染组件。例如,在iOS和Android上显示的按钮样式可以做到完全相同。
- 原生客户端:依赖于各操作系统提供的原生渲染框架。在iOS上使用UIKit(针对Objective - C/Swift开发)或AppKit(针对macOS应用)进行渲染;在Android上使用Android SDK的View系统来构建和渲染UI。不同平台的渲染框架有各自的特点和规范,因此原生应用在不同平台上的视觉风格和交互方式会遵循对应平台的设计规范。
渲染性能
- Flutter:由于直接使用Skia进行自绘,减少了与原生系统的交互开销,在复杂UI场景下能提供更流畅的渲染性能。Flutter的渲染机制采用了分层渲染和GPU加速技术,能够高效地处理复杂的动画和高帧率要求的场景。例如,在实现一个复杂的图表动画时,Flutter可以更轻松地保持高帧率。
- 原生客户端:在简单UI场景下,原生渲染通常能提供较好的性能,因为原生系统对自身的渲染框架进行了深度优化。但在处理复杂UI和高帧率动画时,可能会遇到性能瓶颈,尤其是在低端设备上。例如,在一些老款安卓设备上,复杂的动画可能会出现卡顿现象。
请阐述Flutter的优缺点
优点
- 跨平台开发:Flutter 可以使用一套代码同时开发 iOS 和 Android 应用,甚至还支持 Web、桌面等平台。这大大减少了开发成本和时间,提高了开发效率。例如,一个创业公司想要快速推出一款移动应用,使用 Flutter 就可以避免分别为 iOS 和 Android 组建不同的开发团队,节省人力和时间成本。
- 高性能:Flutter 采用 Dart 语言,通过 AOT(Ahead - of - Time)编译,在移动设备上可以实现接近原生应用的性能。同时,Flutter 拥有自己的渲染引擎 Skia,能够直接与硬件层交互,避免了原生系统的一些性能损耗。像一些对性能要求较高的游戏类应用,使用 Flutter 开发也能有不错的表现。
- 丰富的组件库:Flutter 提供了大量的高质量、可定制的 UI 组件,开发者可以快速搭建出美观、流畅的用户界面。并且这些组件在不同平台上的显示效果基本一致,保证了应用的一致性体验。例如,开发者可以使用 Flutter 的 Material Design 组件快速构建出符合 Android 设计规范的界面,也可以使用 Cupertino 组件构建出具有 iOS 风格的界面。
- 热重载:Flutter 的热重载功能允许开发者在不重新启动应用的情况下,快速看到代码修改后的效果。这极大地提高了开发效率,开发者可以快速验证自己的想法,进行界面调整和功能测试。比如在开发一个界面时,开发者修改了某个按钮的颜色,只需要几秒钟就可以在模拟器或真机上看到修改后的效果。
- 社区支持:随着 Flutter 的不断发展,其社区越来越庞大,有丰富的开源项目和插件可供使用。开发者可以很容易地找到解决各种问题的方案,也可以将自己的项目贡献给社区,促进技术的交流和发展。
缺点
- 包体积较大:由于 Flutter 包含了自己的渲染引擎和 Dart 运行时,生成的应用安装包体积相对较大。这对于一些对包体积敏感的应用,尤其是在网络环境较差或者存储空间有限的设备上,可能会影响用户的下载和使用意愿。例如,一些小型工具类应用,用户可能更倾向于选择体积较小的应用。
- 学习成本:对于没有 Dart 语言基础和响应式编程经验的开发者来说,学习 Flutter 有一定的难度。Dart 语言有自己的语法和特性,响应式编程的思想也需要开发者进行一定的学习和适应。
- 原生交互复杂:虽然 Flutter 可以与原生代码进行交互,但在一些复杂的场景下,如调用特定的原生系统功能或与第三方原生 SDK 集成时,开发过程相对复杂。需要开发者具备一定的原生开发知识,并且要处理好 Flutter 与原生代码之间的通信和数据传递。
- 生态系统相对较小:尽管 Flutter 社区在不断发展,但与 Android 和 iOS 原生开发相比,其生态系统仍然相对较小。一些特定领域的插件和工具可能不够完善,开发者可能需要自己开发或者进行二次开发。
Flutter与安卓渲染的区别
渲染引擎
- Flutter:使用 Skia 渲染引擎,Skia 是一个开源的 2D 图形库,被广泛应用于 Chrome 浏览器等项目中。它可以直接在硬件层进行渲染,不依赖于操作系统的图形库,能够实现高效、一致的渲染效果。
- 安卓:安卓系统使用的是 Android 系统自带的渲染框架,如 OpenGL ES 等。这些框架依赖于安卓系统的底层图形库和硬件驱动,不同的安卓设备可能会因为硬件和系统版本的不同而出现渲染效果的差异。
渲染机制
- Flutter:采用基于 Widget 的响应式渲染机制。当数据发生变化时,Flutter 会根据新的数据重新构建 Widget 树,并通过渲染引擎将新的界面绘制到屏幕上。这种机制使得界面的更新更加高效和灵活。
- 安卓:安卓采用的是基于 View 的渲染机制。当界面需要更新时,需要调用 View 的 invalidate() 等方法来触发重绘。安卓的 View 系统是一个树形结构,更新某个 View 可能会导致其父 View 和子 View 的重绘,效率相对较低。
布局方式
- Flutter:Flutter 的布局是通过 Widget 来实现的,Widget 可以嵌套组合,形成复杂的布局。Flutter 提供了丰富的布局 Widget,如 Row、Column、Stack 等,开发者可以根据需要灵活选择。
- 安卓:安卓的布局是通过 XML 文件或者 Java/Kotlin 代码来定义的,使用的是 LinearLayout、RelativeLayout 等布局管理器。安卓的布局方式相对较为传统,在处理复杂布局时可能会比较繁琐。
跨平台一致性
- Flutter:由于使用了自己的渲染引擎,Flutter 可以在不同平台上实现高度一致的渲染效果。开发者只需要编写一套代码,就可以在 iOS 和 Android 等平台上获得相同的界面显示效果。
- 安卓:安卓应用主要是为安卓系统开发的,虽然可以通过一些适配措施在不同的安卓设备上保持一定的一致性,但由于安卓设备的碎片化问题,仍然可能会出现一些界面显示差异。
Flutter的Channel是如何实现的,其原理是什么
Flutter的Channel基于BinaryMessenger实现,BinaryMessenger是一个抽象接口,它定义了在Flutter和原生平台之间传递二进制消息的基本方法。其工作原理如下:
- 消息传递:当Flutter端或原生端调用Channel的发送方法时,消息会被编码成二进制数据,通过BinaryMessenger发送到对方。
- 通道标识:每个Channel都有一个唯一的通道名称,用于在Flutter和原生平台之间标识不同的通道。当消息发送时,会携带通道名称,以便对方能够正确地将消息路由到对应的Channel。
- 消息编解码:在消息发送前,需要将消息对象编码成二进制数据;在接收消息后,需要将二进制数据解码成消息对象。不同类型的Channel使用不同的编解码器,如StandardMessageCodec、JSONMessageCodec等。
- 异步通信:Flutter的Channel采用异步通信机制,当调用发送方法时,不会阻塞当前线程,而是返回一个Future对象,在消息处理完成后,通过回调函数返回结果。
综上所述,Flutter的Channel通过BinaryMessenger实现了Flutter和原生平台之间的消息传递,利用通道名称进行消息路由,使用编解码器进行消息编解码,采用异步通信机制提高了应用的性能和响应速度。