Flutter
-
介绍下Flutter的架构
由上图可知,Flutter框架自上而下分为Framework、Engine、Embedder三层。
- Framework(框架)使用Dart语言编写,包含了基础组件、布局、动画、手势等功能。提供了丰富的 Widget(组件) ,用于构建用户界面。负责处理渲染、布局、事件处理等逻辑。
- Engine(引擎) 用 C++ 实现的底层引擎。提供图形渲染 Skia 引擎、文本排版、事件处理、插件架构等核心功能。负责将 Framework 层构建的界面转换为实际的像素显示。
- Embedder(嵌入)属于特定平台的嵌入层。负责将 Flutter 引擎嵌入到不同的平台(如 Android、iOS、Web 等)中,并处理与平台相关的交互,如与原生系统的通信、窗口管理等。
-
Flutter 和其他跨平台方案的本质区别
React Native 之类的框架,只是通过 JavaScript 虚拟机扩展调用系统组件,由 Android 和 iOS 系统进行组件的渲染;
Flutter 则是自己完成了组件渲染的闭环。Flutter只关心向 GPU提供视图数据,GPU的 VSync信号同步到 UI线程,UI线程使用 Dart来构建抽象的视图结构,这份数据结构在 GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU。
UI线程负责处理Dart代码、构建 Widget 树转换成 RenderObject 树再生成Layer Tree,GPU线程负责接收Layer Tree、进行合成 (Compositing)、调用图形库 (Skia) 与GPU交互并完成最终绘制。所有GPU操作都在此线程,避免阻塞UI响应。
-
介绍下Flutter的渲染机制(三棵树)
- Widget树是开发写的代码组件,比如 Container()、Text(),这些一层层嵌套的Widget就构成了一个 Widget 树,Widget 本身是很轻量,它相当于一个配置。它描述了组件的样子、数据,但它自己不直接参与真正的绘制工作。Widget 对象通常是不可变的,一旦创建,它的属性就不会再改了。如果我们想改,通常是创建一个新的 Widget 实例。
- Element树是 Widget 的实体,它持有对应的Widget和 RenderObject ,当 Widget 树中某个节点变化时,Element 会对比新旧 Widget ,决定是否复用旧的 RenderObject ,还是销毁重建。
- RenderObject树是真正干活的,它负责布局(layout)、绘制(paint)、点击测试(hit test)。比如 RenderFlex 对应 Row/Column ,它计算子控件的位置和大小。
它们怎么协同工作的?当 Widget 树变化时,Element 树会去比较新的 Widget 和旧的 Widget:
- 如果 Widget 的类型和 Key 没变,Element 就会被复用,它会拿到新的 Widget 配置去更新对应的 RenderObject。
- 如果类型或 Key 变了,通常旧的 Element 和它管理的 RenderObject 就会被销毁,然后创建新的。
- Element 树进而管理 RenderObject 树。当 Element 更新了 RenderObject 的配置后,RenderObject 就会进行重新布局和重绘。
所以 Flutter 的设计哲学是:频繁重建 Widget 树,但通过 Element 树控制实际渲染开销。这也是为什么 setState() 不会导致性能灾难——底层有 Element 和 RenderObject 的优化。
-
setState做了哪些工作?
setState()过程主要工作是记录所有的脏元素,会引起build函数执行,更新widget树、更新Element树和RenderObject树,最后重新渲染。
源码中通过 _element!.markNeedsBuild() 实现,该方法将关联的 Element 加入 _dirtyElements列表(待重建元素队列),setState() 是异步操作,不会立即更新 UI。它向 Flutter 框架提交一个重建请求,等待下一帧绘制周期执行。在下一帧事件循环中,框架遍历 _dirtyElements 列表,逐个调用 Element.rebuild() 方法,重新构建对应的 Widget 树。重建过程通过 build() 方法生成新的 Widget 树,并与旧树进行差异化比较(Diff 算法),仅更新发生变化的 UI 部分。
-
Dart的线程模型是怎样运行的?
Dart 在单线程中是以消息循环机制来运行的,包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。 微任务队列 的优先级高于事件队列。在每一次事件循环中,Dart总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。
微任务队列插入任务?
Future.microtask()
scheduleMicrotask()
Stream中的执行异步的模式就是scheduleMicrotask。因为microtask的优先级又高于event。所以,如果 microtask 太多就可能会对触摸、绘制等外部事件造成阻塞卡顿。
向事件队列插入任务? Future就是将任务插入到事件队列
Future和Stream有什么区别?
Future中的任务会加入下一轮事件循环,而Stream中的任务则是加入微任务队列。
Future 用于表示单个运算的结果,而 Stream 则表示多个结果的序列。
-
Flutter 是如何与原生Android、iOS进行通信的?
- MethodChannel: 用于传递方法调用
用途:用于 Flutter 调用原生代码中的方法,并可以异步地接收一个返回结果。反过来,原生代码也可以通过它调用 Flutter (Dart) 中的方法(虽然不那么常见,但可以实现)。
工作方式:你在 Flutter 端定义一个 MethodChannel,并给它一个唯一的名称。然后在原生端也用同样的名称创建一个 MethodChannel 并设置一个 MethodCallHandler。当 Flutter 端调用 invokeMethod 时,原生端的 Handler 就会收到这个调用,执行相应的原生代码,然后可以通过 result.success()、result.error() 或 result.notImplemented() 返回结果给 Flutter。
- EventChannel:用于原生代码向 Flutter 发送持续的数据流。
用途:用于原生代码向 Flutter 发送持续的数据流。
工作方式:Flutter 端创建一个 EventChannel 并监听它返回的 Stream。原生端则负责在这个 Channel 上发送事件(数据)。一旦有新的数据,Flutter 端的监听器就会收到。
例子:比如原生那边有传感器数据(像 GPS 位置更新、陀螺仪数据)、网络连接状态变化、或者监听广播事件等,就可以通过 EventChannel 持续地把这些信息传递给 Flutter。
- BasicMessageChannel:用于传递字符串和半结构化的信息
用途:用于传递结构化的数据,可以自定义编解码器 (codec)。它比 MethodChannel 更基础,可以双向发送消息。
工作方式:双方都创建一个 BasicMessageChannel,然后可以互相发送消息。你可以指定消息的编解码器,比如 StringCodec、JSONMessageCodec,或者标准的 StandardMessageCodec(支持常见的数据类型如数字、字符串、布尔、列表、字典等)。
例子:当你需要发送一些自定义的、可能比较大的数据块,或者对编解码有特殊要求时,可以考虑使用它。
-
状态管理框架对比
setState
Flutter 内置,适用于局部状态(如单个 Widget 的交互状态),简单直接
InheritedWidget
InheritedWidget 通过 Element 映射表 和 依赖订阅机制 实现高效数据共享,用于在 Widget 树中高效共享数据的组件,通过它可以在父 Widget 和子 Widget 之间传递数据,避免逐层传递的冗余操作。
依赖注册:
当子 Widget 调用 dependOnInheritedWidgetOfExactType 时,Flutter 会将该子 Widget 的 Element 添加到 InheritedElement 的 依赖列表 中。这建立了一个隐式的订阅关系。
更新通知流程:
父 Widget 通过 setState 触发 InheritedWidget 的重建,当updateShouldNotify返回 true 则通知所有依赖的子 Widget,InheritedElement 调用 notifyClients,遍历依赖列表并触发子 Widget 的 didChangeDependencies 或 build 方法。
Provider
核心原理:
Provider 库基于 InheritedWidget 实现,通过封装 ChangeNotifier 和 Consumer(其Element 被注册到 InheritedWidget 的依赖列表中) 简化了数据更新逻辑。ChangeNotifier提供订阅能力,当调用 notifyListeners() 时,遍历所有监听器并触发回调(如 setState),重建 InheritedWidget,并触发子 Widget 的 didChangeDependencies 或 build 方法。
GetX
核心原理:结合响应式编程与依赖注入,通过轻量化设计实现高效状态管理。
使用 .obs 将变量转换为 Rx 对象(如 RxInt、RxString),底层通过 StreamController 和 Stream 实现数据流监听,Obx 或 GetX Widget 自动订阅变化,仅局部刷新关联 UI,通过 Get.put 注册单例,Get.find 全局获取,依赖关系由 GetX 自动管理。
Bloc
核心原理:基于 Stream 数据流与事件驱动机制,实现业务逻辑与 UI 的分离。
用户交互被封装为 Event 对象,通过 Bloc.add(Event) 方法发送到 Bloc 内部的事件队列,Bloc 内部通过 StreamController<Event> 或 RxDart 的 Subject(如 PublishSubject)创建事件流,实时接收外部传入的事件,
用户交互触发 Event,Bloc 通过 mapEventToState 转换为 State,并输出 Stream,使用 StreamBuilder 监听状态流,局部更新 UI。
Riverpod
核心原理:Provider 的现代化升级,强调类型安全与自动依赖管理。
Redux
核心原理:基于单向数据流和纯函数,实现可预测的状态管理。
Android
-
Android 中的性能优化实践
- 启动优化: application中不要做大量耗时操作,如果必须的话,建议异步做耗时操作
- 布局优化:使用合理的控件选择,少嵌套。(合理使用include,merge,viewStub等使用)
- apk优化:(资源文件优化,代码优化,lint检查,.9.png,合理使用shape替代图片,webp等)
- 性能优化,网络优化,电量优化:避免轮询,尽量使用推送,应用处于后台时,禁用某些数据传输,限制访问频率,失败后不要无限重连,使用缓存。
- 内存优化:
-
内存泄露: 程序中某些对象不再被使用,但它们占用的内存没有被及时释放,导致内存逐渐被占用而无法被回收利用。内存使用量持续上升,不会下降,最终可能导致 OOM,解决方案: 1、单例持有 Activity 上下文(强引用),Activity 销毁后仍被单例引用(方案:使用Application,生命周期与App 一致)。2、内部类如Handler隐式持有外部类如Activity的强引用(方案:静态内部类 + 弱引用外部类)3、未关闭的资源(文件、数据库、Bitmap 等)
-
内存抖动: 循环或频繁调用的方法中创建大量临时对象,或者对象的创建和销毁过于频繁,会导致界面卡顿,影响应用程序的性能。解决方案: 优化代码,避免在循环或频繁调用的方法中创建不必要的临时对象;尽量复用对象,减少内存的分配和释放次数
-
OOM异常: 1、内存泄露导致可用内存逐渐减少,最终引发OOM、2、或者一次性申请了过大的内存,超过了系统限制。解决方案: 1、优化内存使用,避免内存泄露;2、合理分配内存,避免一次性申请过大的内存;3、对图片进行压缩处理,避免加载过大的图片
-
Android 中的事件分发机制
事件分发遵循Activity -> Window -> ViewGroup -> View的传递路径
dispatchTouchEvent:
是否继续分发?
true 事件由当前 View 处理,停止向下传递
false 事件回传给父 View 的onTouchEvent
super.dispatchTouchEvent(ev):继续调用子 View 的dispatchTouchEvent。
onInterceptTouchEvent:
是否拦截事件?
true 拦截事件,事件由当前 ViewGroup 处理
false 不拦截,事件继续传递给子 View
onTouchEvent:
处理具体的触摸事件
true 事件被消费,停止向上传递
false 事件未被消费,回传给父 View 的onTouchEvent。
如何解决事件冲突?
外部拦截法(父容器说了算):
父容器(如 ViewPager)根据条件决定是否拦截事件,重写父容器的 onInterceptTouchEvent(),判断滑动方向或距离,达到阈值则拦截事件。(解决 ViewPager 嵌套 RecyclerView 的滑动冲突)
内部拦截法(子 View 主动协商):
在子View的dispatchTouchEvent()中根据条件请求父容器不拦截。子View 通过requestDisallowInterceptTouchEvent(false)控制父容器是否拦截。
-
简述一下Handler的原理
Handler是Android 中用于处理消息和线程间通信的机制,主要包括以下几个步骤:
创建一个Handler对象,系统会自动将该Handler与创建它的线程的MessageQueue和Looper相关联,使用Handler发送一个Message(含有target) 对象添加到对应的MessageQueue中,Looper依次取出消息,并将其分发给对应的Handler进行处理。
Looper.loop()逐个从消息队列中取出消息并一一处理,我们所说的ANR其实是主线程正好卡在了某个消息或者事件上,导致后面的消息或者事件没有得到处理造成的,不是一个概念。
一个线程可以有多个Handler,而一个线程只能有一个Looper,这个在Looper类的Looper.prepare中可以看出Looper的唯一性。
子线程默认是没有关联 Looper 的。因此,在使用Handler时,需要先创建一个 Looper 对象,并使用该对象创建一个 Handler 对象。
-
简述一下自定义View流程
onMeasure:可能多次触发,在measure的过程中注意MeasureSpec,specMode、specSize
onLayout:在ViewGroup中,只触发一次,决定子View的位置
onDraw:进行实际的绘制操作,包括绘制图形、文本等,绘制内容,通过Canvas.drawxxx(),paint
onTouchEvent:处理点击事件
-
针对 RecyclerView 做过哪些优化
布局优化:减少Item布局层级嵌套,减少测量和绘制时间;若所有Item尺寸一致,调用recyclerView.setHasFixedSize(true),避免重复触发全局布局计算。
数据绑定与更新优化:在onCreateViewHolder中通过ViewBinding/DataBinding初始化视图,避免重复调用findViewById(),使用DiffUtil或AsyncListDiffer计算新旧数据集差异,局部刷新而非全局刷新。禁止在onBindViewHolder中执行网络请求、复杂计算或同步I/O操作,仅做数据绑定,耗时操作。
滑动性能优化:监听滚动状态(OnScrollListener),滑动时暂停图片加载(如Glide的pauseRequests()),停止后恢复(图片加载控制);自定义LayoutManager并重写calculateExtraLayoutSpace(),预加载屏幕外区域Item(如提前加载800px),启用setItemPrefetchEnabled(true),利用预取减少滚动时的卡顿(预加载机制);实现分页逻辑。
缓存机制优化:通过setItemViewCacheSize()增加缓存数量(如20个),提升快速滚动的流畅度,多列表场景共享RecycledViewPool,复用相同类型的ViewHolder。
-
App启动流程优化
减少主线程阻塞:异步初始化:将非关键任务(如第三方 SDK、日志库)移至 IntentService 或协程 Dispatchers.IO;对推送、统计等 SDK 使用 Handler.postDelayed() 延后 500ms 执行。
Dex 与资源精简:启用 R8 压缩移除未使用代码,主 Dex 保留核心类(通过 multiDexKeepProguard 规则);压缩图片资源,替换 PNG 为 WebP,优先加载首屏必要资源。
渲染加速策略:使用 ConstraintLayout 减少嵌套,ViewStub 延迟加载非首屏模块;设置启动页主题背景为闪屏图,避免默认白屏/黑屏(防白屏,视觉上提速30%)
数据预取与缓存:用户登录后预取首页数据到内存/本地,冷启动直接读取缓存
网络请求优化:合并 API 请求
-
常用的设计模式和使用场景
单例模式:保证全局只有一个实例,如网络请求、sp存储的工具类、弹窗的工具类
建造者模式:用于需要设置比较多的属性可以用直接链式,如AlerDialog
工厂模式:用于业务的实体类创建,易于扩展,如BitMapFactory
责任链模式:OKhttp的拦截器封装
观察者模式:Rxjava的运用
-
简述 Http、Https、UDP、Socket之间的区别?
http传输的数据都是未加密的,也就是明文传输的,https则是具有安全性的ssl加密传输协议。
http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。 最后一点在Android 9.0 如果用http进行传输,需要在application节点下设置 android:usesCleartextTraffic="true"
UDP是非面向连接的协议,发送数据时不管对方状态直接发送,无需建立连接,如同微信发送一个消息或者语音信息,对面在不在线无所谓.
Socket不属于协议范畴,别名套接字通过调用Socket,才能使用TCP/IP协议,Socket连接是长连接,理论上客户端和服务器端一旦建立连接将不会主动断开此连接。Socket连接属于请求-响应形式,服务端可主动将消息推送给客户端。
在每次进行连接和断开连接都需要经过复杂的三次握手和四次握手,从而保证了每个连接都是可靠的,所以TCP协议是可靠的,而HTTP就是TCP上层的协议,所有连接都是基于TCP协议的。
-
Kotlin 中的协程,它与线程有什么区别?有哪些优点?
特点: 1,在单个进程内,多个协程串行执行,只挂起不阻塞 2,协程最终的执行还是在各个线程之中。 优点:1,由于不阻塞线程,异步任务是编译器主动交到线程池中执行。因此,在异步任务执行上,切换和消耗的资源都较少。 2,由于协程是跨多个线程,并且能够保持串行执行;因此,在处理多并发的情况上,能够比锁更轻量级。通过状态量实现
-
安卓中如何实现加载大图
BitmapRegionDecoder:用来加载大图并显示其中的一个区域。使用这种方法,可以避免一次性加载整张大图,从而降低内存占用
BitmapFactory.Options:使用inSampleSize 属性来缩小图片的尺寸,从而减少内存占用
Glide 和 Picasso 等第三方库:可以自动对加载的图片进行压缩和缩放,从而避免一次性加载整张大图
将图片分割成多个小图:将图片分割成多个小图,并在需要时分别加载并拼接成一张完整的大图,需要进行额外的处理和管理,比较复杂
-
安卓中的动画的分类及使用
逐帧动画:【Frame Animation】,即顺序播放事先准备的图片,比较常用的方式,在res/drawable目录下新建动画XML文件
补间动画:【Tween Animation】,View的动画效果可以实现简单的平移、缩放、旋转。只能给View加,不能给对象加,并且不会改变对象的真实属性。可以在xml中定义,也可以在代码中定义
属性动画:【Property Animation】,补间动画增强版,支持对对象执行动画。补充补间动画的一些缺点,任意 Java 对象,不再局限于 视图View对象,可自定义各种动画效果,不再局限于4种基本变换:平移、旋转、缩放 & 透明度,分为ObjectAnimator和ValueAnimator。可以用xml实现也可以用代码实现
过渡动画:【Transition Animation】,实现Activity或View过渡动画效果。包括5.0之后的MD过渡动画等。
-
解决LiveData中数据倒灌的几种方式
事件包装器:通过自定义数据包装类,标记事件是否已被消费,确保每个事件仅被处理一次。反射修改版本号:通过反射修改 LiveData 内部版本号(mVersion)和观察者版本号(mLastVersion),使新观察者忽略旧数据。SingleLiveEvent:专为一次性事件设计的 LiveData 变体,通过原子标志位确保数据仅分发一次。UnPeekLiveData:第三方库通过记录观察者订阅时的数据版本,确保仅分发新数据。
-
Kotlin 的五个作用域函数(
let、also、apply、with、run)的使用
let:空安全处理与对象转换,通过 it 引用对象,返回 Lambda 的最后一行结果
// 空安全处理 & 对象转换
val name: String? = "Kotlin"
val length = name?.let {
println("Processing: $it") // 输出:Processing: Kotlin
it.length // 返回长度 → 6
} ?: 0
also:附加操作,通过 it 引用对象,返回对象本身
// 链式调用中插入日志
val list = mutableListOf(1, 2, 3)
.also { println("初始列表: $it") } // 输出:初始列表: [1, 2, 3]
.apply { add(4) }
apply:对象初始化与配置,通过 this 引用对象(可省略),返回对象本身
// 对象属性批量设置
val button = Button(context).apply {
text = "Submit" // 直接访问属性
textSize = 16f
setOnClickListener { ... }
} // 返回配置后的 Button 对象
run:对象操作 + 结果计算,通过 it 引用对象,返回 Lambda 结果
// 扩展函数:初始化并计算
val personInfo = Person("Alice", 25).run {
age += 1
"Name: $name, Age: $age" // 返回字符串 → "Name: Alice, Age: 26"
}
// 非扩展函数:临时作用域
val discount = run {
val basePrice = 100
basePrice * 0.8 // 返回计算结果 → 80
}
with:集中操作非空对象,通过 this 引用对象,返回 Lambda 结果
// 批量操作对象方法
val sb = StringBuilder()
val result = with(sb) {
append("Hello")
append(" World!")
toString() // 返回字符串 → "Hello World!"
}