Android八股文-- flutter篇

📅 2026/7/16 8:47:07
Android八股文-- flutter篇
flutter三棵树flutter三层树时分层职责拆分自上而下1、Widget树配置层描述ui长上面样纯不可变配置2、Element树实例层生命周期管理者Widget与渲染层的中间桥梁3、RenderObject树渲染层负责布局绘制事件真实屏幕渲染三棵树一一对应但生命周期重建逻辑完全分开一、Widget树配置树1、本质Widget是immutable不可变的配置描述类只存UI的参数颜色宽高文字子节点样式不持有任何渲染资源不做布局绘制。每次页面刷新状态变更会重建全新的Widget对象旧Widget直接丢弃两类Widget1、StatelessWidget无内部状态build只依赖入参2、StatefulWidget携带State状态存在State里Widget本身依然不可变刷新时重建Widget但State会复用特点1、轻量只有配置参数创建销毁成本极低2、不可变所有属性final更新UI只能新建Widget3、只负责描述不碰渲染布局画布4、重建极频繁setState动画页面滚动都会大量新建Widget二、Element树实例/协调树中间核心桥梁1、定位Widget只是配置模板Element是Widget在树上的活实例绑定生命周期串联Widget与RenderObject。每一个Widget对应一个ElementElement会挂载到Element树上长期存活不会随Widget重建轻易销毁。2、两种Element1、ComponentElement对应普通组合Widget不参与渲染只管理子ElementStatelessElement对应 StatelessWidgetStatefulElement对应 StatefulWidget持有 State管理 state 生命周期2、RenderObjectElement对哦应RenderBoxRenderObjectWidget如ContainerTextRowColumn持有RenderObject直接对接渲染层3、核心Element的复用逻辑Widget每次重建会生成新对象但Element不会直接销毁框架通过canUpdate(Widget oldWidget)判断新旧Widget runtimeType、key完全一致则复用当前Element调用update更新配置类型/key变了销毁旧Element新建Element挂载这就是Flutter高性能的根源Widget随便重建ElementRenderObject尽量复用不重复创建昂贵渲染对象4、Element独有生命周期Widget没有生命周期State的生命周期本质是StatefulElement驱动createState() - initState - didChangeDependencies - build - didUpdateWidget - disposeContext本质就是ElementBuildContext Element三、RenderObject树渲染树真正绘制布局1、职责唯一负责布局约束、大小测量、位置摆放、画布绘制、点击事件、层合成直接对接Skia图形引擎输出像素到屏幕。对应Android的View/ViewGroup常用子类RenderBox二维盒子模型绝大多数组件底层都是RenderBox2、两大核心流程两阶段渲染阶段1布局Layoutmeasurelayout1、父节点向下传递约束Constraints最大/最小宽高2、子节点根据约束计算自身大小performLayout()3、子向上上报自身尺寸父确定子控件坐标位置阶段2绘制Paint布局完成后遍历RenderObject树调用paint(PaintingContext context, Offset offset)在Canvas执行绘制文字图片矩形渐变3、独有能力拥有真实渲染缓存图层偏移尺寸处理点击手势滚动事件开销最大创建成本高框架极力复用不会频繁销毁重建页面更新完成流程1、调用setstate()标记当前StatefulElement为脏元素markDirty()2、下一帧信号到来遍历所有脏Element执行build()生成全新的Widget子树3、Elememnt对比新旧Widget可复用执行element.update(newWidget),更新配置不可复用删除旧Element销毁对应RenderObject创建新ElementRenderObject4、所有更新后的RenderObject重新走Layout测量Paint绘制5、Skia将图层合成渲染屏幕原生和flutter如何解耦原生和Flutter耦合都来自于双向通信通道MethodChannel/BasicMessageChannel页面互相持有引用强依赖上下文硬编码路由SDK生命周期强绑定解耦核心目标两端只依赖抽象接口不依赖对方具体实现通道通信封装隔离路由解耦资源、生命周期隔离整体分层解耦思路1、通信层解耦封装Channel屏蔽原生、Flutter底层API统一抽象通信接口2、路由页面解耦跨端路由中间件消除两端硬编码跳转3、业务能力解耦4、生命周期与上下文解耦5、依赖注入分层业务层不直接持有通道、Flutter引擎实例flutter一帧如何渲染结合渲染管线动画前置Flutter渲染分为三大阶段整体跑在UI线程主线程渲染后端是Skia2D图形库无原生平台View合成一帧标准目标耗时小于等于16ms完成管线顺序1、动画阶段Animate驱动所有动画更新状态2、构建阶段Build重建Widget树diff更新Element更新RenderObject配置3、布局阶段LayoutRenderObject约束测量、计算位置尺寸4、绘制阶段Paint遍历Render树生成绘制指令5、合成光栅化Composite Ratesize图层合并Skia光栅化像素提交屏幕动画是一帧最前置的环节动画会主动标记脏帧驱动完整渲染管线循环一、屏幕刷新信号源头VSync手机硬件每 16ms 发出一次 VSync 垂直同步信号Flutter 监听 VSync 作为一帧的起点 所有动画、渲染都由 VSync 驱动保证和屏幕刷新率对齐避免画面撕裂。二、分阶段拆解渲染管线结合动画阶段1Aminate动画更新管线第一步动画核心区接触时机VSync信号抵达最先执行1、遍历当前页面所有活跃Ticker所有AnimationController2、Ticker根据两次VSync时间差更新动画进度值value 线性动画value匀速变化曲线动画CurvedAnimation套用缓动函数修正value3、动画更新后会调用markNeedsBuild()/markNeedsPaint()标记脏区域如果动画改变控件尺寸、文字、子树-》标记Element为脏需要走完整BuildLayoutPaint如果仅位移、透明度、旋转-》只标记RenderObject为脏跳过build、layout直接重绘合成性能极高4、所有动画同步更新后才进入下一阶段构建Widget树阶段2Build构建阶段Widget/Element Diff1、遍历所有被标记的dirty的StatefulElement执行state.build()2、生成全新的Widget子树Widget不可变每次重建新实例3、Element执行diff复用逻辑对比新旧Widget runtimeTypekey匹配复用调用element.update ,同步配置到RenderObject不匹配销毁旧Element旧RenderObject新建挂载4、此阶段只生成配置无任何测量绘制操作动画场景如果动画触发 build每帧都会执行一次 build频繁创建 Widget 对象容易产生内存抖动、GC 卡顿 优化方案动画尽量抽离到外层 Transform避免 build 内部动态插值。阶段3Layout布局阶段RenderObject两阶段测量仅上一阶段有布局变更时才执行无变更直接跳过约束向下传递父 RenderObject 把宽高约束 Constraints 传给子节点子节点 performLayout ()子根据约束计算自身 size尺寸向上回传子上报尺寸父确定子控件坐标 Offset 整套是深度优先遍历整棵 RenderObject 树。动画场景宽高padding行列数量变化的动画会触发每帧Layout多层嵌套布局时开销指数上升长列表布局动画极易卡顿阶段4Paint绘制阶段生成绘制图层Layer遍历 RenderObject 树执行paint()方法控件把绘制指令矩形、文字、图片、路径写入PictureLayer遇到需要独立缓存的控件Opacity、Clip、Transform、ShaderMask会生成独立 Layer 图层缓存绘制结果优势图层缓存后后续仅变换图层矩阵不用重复执行 paint代价过多 Layer 会增大 GPU 内存占用图层爆炸输出一棵图层树 Layer Tree而非直接像素。阶段5CompositeRatersize合成光栅化GPU线程执行UI线程构建完Layer树后把图层数据提交给Raster光栅线程不阻塞主线程合成按照图层顺序叠加执行 Transform 矩阵、透明度混合、裁剪Skia 光栅化将矢量图层转换成位图像素提交像素缓冲区给 GPU交换缓冲区上屏用户看到画面动画在这里的性能优势Transform/Opacity 动画仅修改图层变换矩阵不需要重新走 Build/Layout/Paint光栅线程仅做矩阵运算GPU 加速60/120 帧丝滑。动画卡顿常见根源1、动画插值在build中每帧触发完整buildlayout2、布局嵌套深3、paint内创建大量临时对象4、动画触发图层爆炸光栅线程阻塞5、Ticker过多6、主线程存在IO数据库复杂计算动画渲染优化方法1、动画分层避免触发Build/Layout用SlideTransition/ScaleTransition/AnimatedOpacity仅操作图层变换 动态文字 / 尺寸动画单独隔离不要全局大面积布局变动。2、缓存静态绘制内容3、减少同时活跃Ticker4、简化Layout树5、paint复用对象Paint、Path、Shader 提取为成员变量不在 paint 内 new消除内存抖动。6、非必要不嵌套Opacity/Transform避免图层爆炸。StatefulWidget 完整生命周期、原理生命周期全部由Element驱动所有回调不是Widget主动调用是StatefulElement在挂载更新销毁时主动执行State对应方法完整生命周期执行顺序阶段1首次挂载1、createStateElement初始化时仅执行一次创建State实例不能使用Context此时Element还未挂载到Element树2、initState初始化唯一入口只执行一次可初始化变量、AnimationControllerScrollController注册监听等触发State创建完成Element挂载在树上3、didChangeDependencies()触发时机1、首次initState后build之前2、上层InheritedWidgetThemeMediaQueryProvider数据发生变化场景需要监听主题屏幕尺寸全局用户信息变化时在这里处理这里可以正常读取context共享数据4、build构建UI每一帧脏标记都会执行仅返回Widget组合阶段2更新阶段父组件重建传参变化setstate动画执行顺序1、didUpdateWidget(oldWidget)触发场景父页面重建传入了新的StatefulWidget实例但是runtimeTypekey一致Element复用2、build()参数更新、setState、Inherited变化都会重新执行build标记当前Element为dirty脏元素下一帧VSync到来执行build重建Widget子树不会新建State只会复用原有State阶段3页面移除销毁执行顺序1、deactivate()触发当前Element暂时从树上移除可能恢复如果页面缓存/页面回退Element重新挂载时会再次走build不会走initState2、dispose()永久销毁只执行一次资源释放唯一位置setstate原理State → 持有 StatefulElementBuildContext StatefulElement 挂载在 Element 树连接 Widget树 RenderObject树源码拆解1、setState方法签名void setState(VoidCallback fn) { // 校验1不能在 dispose 后调用 assert(_debugLifecycleState ! _StateLifecycle.disposed, dispose后禁止调用setState); // 校验2fn不能返回Future禁止async setState assert(!fn is Future, setState回调不能async); // 校验3不能在 build / didUpdateWidget / didChangeDependencies 同步调用 assert(_debugBuilding false, 构建阶段禁止同步setState); // 执行状态修改回调修改State内部变量 fn(); // 核心逻辑标记当前Element为脏节点等待下一帧重建 _element.markNeedsBuild(); }关键点先执行传入的fn回调同步修改状态变量真正触发刷新的不是 fn是_element.markNeedsBuild()一系列断言限制非法调用防止递归死循环、内存异常。核心markNeedsBuild流程_element是StatefulElement实例markNeedsBuild定义在父类Element步骤一判断是否脏节点void markNeedsBuild() { // 如果已经标记脏直接返回防止重复加入脏列表 去重优化 if (_dirty) return; // 标记当前Element为脏 _dirty true; // 将当前Element加入全局脏元素列表 owner!.scheduleBuildFor(this); }步骤二owner.scheduleBuilderFor(this)owner是BuildOwner全局唯一构建管理器整颗Element树共用作用把脏Element存入_dirtyElements列表请求下一帧执行重建步骤三绑定VSync等待下一帧渲染管线BuildOwner会向引擎请求一次新帧请求VSync信号当前帧不会立刻刷新所有setState统一收集等到下一次VSync信号到来批量处理多次连续调用setState不会多次渲染同一帧内全部合并只执行一次buildVSync到来后批量执行Element重建一帧Build阶段VSync信号抵达进入渲染管线第二阶段BuildBuildOwner遍历_dirtyElements:取出脏StatefulElement执行element.rebuild()rebuild源码void rebuild() { // 清空脏标记 _dirty false; // 执行state.build生成全新Widget子树 Widget newWidget state.build(this); // 对比新旧Widget更新Element持有的widget引用 _updateWidget(newWidget); }_updateWidget内部做Element Diffvoid _updateWidget(Widget newWidget) { final Widget oldWidget widget; _widget newWidget; // Element持有新Widget // canUpdate 判断runtimeType key 完全一致 if (oldWidget.canUpdate(newWidget)) { // 可复用仅更新配置不销毁Element/RenderObject update(newWidget); } else { // 不可复用销毁整棵子树重新创建ElementRenderObject _inflateWidget(newWidget); } }复用场景类型、key 不变仅参数改变 → 复用 Element、State、RenderObject仅更新配置不复用类型切换 /key 修改 → 销毁旧子树新建整套对象开销更大。完整一帧时序调用setState到界面刷新全链路业务代码执行setState((){ _count; })同步执行回调修改 State 内部变量调用markNeedsBuild()标记 Element 脏加入 BuildOwner 脏列表当前帧剩余逻辑继续走完不会打断当前渲染硬件发出下一次 VSync 垂直同步信号渲染管线启动先执行所有动画 Ticker 更新进入 Build 阶段BuildOwner 遍历所有脏 Element执行 rebuild → build 生成新 WidgetElement Diff 复用 / 重建子树同步更新下层 RenderObject 配置进入 Layout 布局、Paint 绘制、光栅合成最终刷新屏幕。