2025Flutter(安卓)面试题详解

3,540 阅读18分钟

Flutter

  1. 介绍下Flutter的架构

image.png 由上图可知,Flutter框架自上而下分为Framework、Engine、Embedder三层。

  • Framework(框架)使用Dart语言编写,包含了基础组件布局动画手势等功能。提供了丰富的 Widget(组件) ,用于构建用户界面。负责处理渲染布局事件处理等逻辑。
  • Engine(引擎) 用 C++ 实现的底层引擎。提供图形渲染 Skia 引擎文本排版事件处理插件架构等核心功能。负责将 Framework 层构建的界面转换为实际的像素显示。
  • Embedder(嵌入)属于特定平台的嵌入层。负责将 Flutter 引擎嵌入到不同的平台(如 Android、iOS、Web 等)中,并处理与平台相关的交互,如与原生系统的通信、窗口管理等。
  1. Flutter 和其他跨平台方案的本质区别

image.png

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响应。

  1. 介绍下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 的优化。

  1. setState做了哪些工作?

setState()过程主要工作是记录所有的脏元素,会引起build函数执行,更新widget树、更新Element树和RenderObject树,最后重新渲染。

源码中通过 _element!.markNeedsBuild() 实现,该方法将关联的 Element 加入 _dirtyElements列表(待重建元素队列),setState() 是异步操作,不会立即更新 UI。它向 Flutter 框架提交一个重建请求,等待下一帧绘制周期执行。在下一帧事件循环中,框架遍历 _dirtyElements 列表,逐个调用 Element.rebuild() 方法,重新构建对应的 Widget 树。重建过程通过 build() 方法生成新的 Widget 树,并与旧树进行差异化比较(Diff 算法)​,仅更新发生变化的 UI 部分。

  1. Dart的线程模型是怎样运行的?

Dart 在单线程中是以消息循环机制来运行的,包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。 微任务队列 的优先级高于事件队列。在每一次事件循环中,Dart总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。

微任务队列插入任务?

Future.microtask()
scheduleMicrotask()
Stream中的执行异步的模式就是scheduleMicrotask。因为microtask的优先级又高于event。所以,如果 microtask 太多就可能会对触摸、绘制等外部事件造成阻塞卡顿

向事件队列插入任务? Future就是将任务插入到事件队列

Future和Stream有什么区别?

Future中的任务会加入下一轮事件循环,而Stream中的任务则是加入微任务队列。

Future 用于表示单个运算的结果,而 Stream 则表示多个结果的序列。

  1. 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(支持常见的数据类型如数字、字符串、布尔、列表、字典等)。

例子:当你需要发送一些自定义的、可能比较大的数据块,或者对编解码有特殊要求时,可以考虑使用它。

  1. 状态管理框架对比

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 对象(如 RxIntRxString),底层通过 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

  1. 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、对图片进行压缩处理,避免加载过大的图片

  1. 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)控制父容器是否拦截。

  1. 简述一下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 对象。

  1. 简述一下自定义View流程

onMeasure:可能多次触发,在measure的过程中注意MeasureSpec,specMode、specSize

onLayout:在ViewGroup中,只触发一次,决定子View的位置

onDraw:进行实际的绘制操作,包括绘制图形、文本等,绘制内容,通过Canvas.drawxxx(),paint

onTouchEvent:处理点击事件

  1. 针对 RecyclerView 做过哪些优化

布局优化:减少Item布局层级嵌套,减少测量和绘制时间;若所有Item尺寸一致,调用recyclerView.setHasFixedSize(true),避免重复触发全局布局计算。

数据绑定与更新优化:在onCreateViewHolder中通过ViewBinding/DataBinding初始化视图,避免重复调用findViewById(),使用DiffUtilAsyncListDiffer计算新旧数据集差异,局部刷新而非全局刷新。禁止在onBindViewHolder中执行网络请求、复杂计算或同步I/O操作,仅做数据绑定,耗时操作。

滑动性能优化:监听滚动状态(OnScrollListener),滑动时暂停图片加载(如Glide的pauseRequests()),停止后恢复(图片加载控制);自定义LayoutManager并重写calculateExtraLayoutSpace(),预加载屏幕外区域Item(如提前加载800px),启用setItemPrefetchEnabled(true),利用预取减少滚动时的卡顿(预加载机制);实现分页逻辑。

缓存机制优化:通过setItemViewCacheSize()增加缓存数量(如20个),提升快速滚动的流畅度,多列表场景共享RecycledViewPool,复用相同类型的ViewHolder。

  1. App启动流程优化

减少主线程阻塞:异步初始化:将非关键任务(如第三方 SDK、日志库)移至 IntentService 或协程 Dispatchers.IO;对推送、统计等 SDK 使用 Handler.postDelayed() 延后 500ms 执行。

Dex 与资源精简:启用 R8 压缩移除未使用代码,主 Dex 保留核心类(通过 multiDexKeepProguard 规则);压缩图片资源,替换 PNG 为 WebP,优先加载首屏必要资源。

渲染加速策略:使用 ConstraintLayout 减少嵌套,ViewStub 延迟加载非首屏模块;设置启动页主题背景为闪屏图,避免默认白屏/黑屏(防白屏,视觉上提速30%)

数据预取与缓存:用户登录后预取首页数据到内存/本地,冷启动直接读取缓存

网络请求优化:合并 API 请求

  1. 常用的设计模式和使用场景

单例模式:保证全局只有一个实例,如网络请求、sp存储的工具类、弹窗的工具类

建造者模式:用于需要设置比较多的属性可以用直接链式,如AlerDialog

工厂模式:用于业务的实体类创建,易于扩展,如BitMapFactory

责任链模式:OKhttp的拦截器封装

观察者模式:Rxjava的运用

  1. 简述 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协议的。

  1. Kotlin 中的协程,它与线程有什么区别?有哪些优点?

特点: 1,在单个进程内,多个协程串行执行,只挂起不阻塞 2,协程最终的执行还是在各个线程之中。 优点:1,由于不阻塞线程,异步任务是编译器主动交到线程池中执行。因此,在异步任务执行上,切换和消耗的资源都较少。 2,由于协程是跨多个线程,并且能够保持串行执行;因此,在处理多并发的情况上,能够比锁更轻量级。通过状态量实现

  1. 安卓中如何实现加载大图

BitmapRegionDecoder:用来加载大图并显示其中的一个区域。使用这种方法,可以避免一次性加载整张大图,从而降低内存占用

BitmapFactory.Options:使用inSampleSize 属性来缩小图片的尺寸,从而减少内存占用

Glide 和 Picasso 等第三方库:可以自动对加载的图片进行压缩和缩放,从而避免一次性加载整张大图

将图片分割成多个小图:将图片分割成多个小图,并在需要时分别加载并拼接成一张完整的大图,需要进行额外的处理和管理,比较复杂

  1. 安卓中的动画的分类及使用

逐帧动画:【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过渡动画等。

  1. 解决LiveData中数据倒灌的几种方式
  • 事件包装器:通过自定义数据包装类,标记事件是否已被消费,确保每个事件仅被处理一次。
  • 反射修改版本号:通过反射修改 LiveData 内部版本号(mVersion)和观察者版本号(mLastVersion),使新观察者忽略旧数据。
  • SingleLiveEvent:专为一次性事件设计的 LiveData 变体,通过原子标志位确保数据仅分发一次。
  • UnPeekLiveData:第三方库通过记录观察者订阅时的数据版本,确保仅分发新数据。
  1. Kotlin 的五个作用域函数(letalsoapplywithrun)的使用

image.png

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!"
}