Lynx 渲染原理(二):数据更新、Fiber/Radon 与 View 交互
先比较客户端 API,再串起数据变化、Fiber/Radon、布局、平台 View 和事件回传。
本系列共三篇:模板加载、数据更新与交互、性能优化。每篇独立编号,源码链接固定到核对版本;本文为静态代码与文档分析,不代表已完成真机性能验证。
目录
-
- initialData、updateData 与 loadTemplate
-
- 从运行时节点到像素管线:真正的交接边界
-
- 从模板节点到 Android View
-
- 交互事件的代码落点
1. initialData、updateData 与 loadTemplate
1.1 API 角色总览
loadTemplate = 加载/切换模板并启动一次渲染
initialData = 这次加载的首屏初始数据
updateData = 模板已加载后的数据更新
1.2 initialData
initialData 属于 LynxLoadMeta,在进入 Native 加载前会被处理并传入渲染侧;预解析 Bundle 路径还会执行 flush()、取得 Native pointer、标记数据已消费,然后作为参数传给 Native:LynxTemplateRender.java:1841、LynxTemplateRender.java:4029。
首屏依赖的数据应该放在 initialData,不要等页面出来后再补一次 updateData。
1.3 updateData
源码明确说明:通常在 renderTemplate 之前调用 updateData 不会生效,除非启用 EnablePreUpdateData:LynxView.java:916。
运行时更新路径是:
LynxView.updateData(data)
↓
LynxTemplateRender.updateData(data, ...)
↓
prepareUpdateData(data)
↓
nativeUpdateDataByPreParsedData(...)
↓
渲染侧重新计算并刷新 UI
代码证据:LynxView.java:922、LynxTemplateRender.java:2283、LynxTemplateRender.java:2306。
当前 API 文档中的宿主更新入口是 updateMetaData(LynxUpdateMeta);文章和旧 API 中的 updateData 是同一类语义的旧/兼容入口。updateMetaData
1.4 更新逻辑(学习版)
先不要把 Fiber 和 Radon 当成两个线程。这里的“Fiber 开关”首先是 Bundle/PageConfig 中的架构选择,源码在解码编译选项时写入 enable_fiber_arch:lynx_binary_config_decoder.cc:129。它决定更新时走哪条节点处理路径。
可以先只记下面这张图:
数据变化
│
├─ Fiber 关闭:旧 Radon 路径
│ 重新得到一棵 Radon 节点树
│ → 旧树 vs 新树
│ → RadonMyersDiff
│ → 生成 UI 操作
│
└─ Fiber 开启:Fiber 路径
计算变化并打包 Changes
→ 主线程解析并 Patch Element Tree
→ 生成 UI 操作
两条路径最后都会走:Resolve → Layout → Flush → Paint
1.4.1 Fiber 关闭:传统 Radon Diff
这条路径可以理解为“先做一棵新树,再和旧树比较”。RadonPage::UpdatePage() 会更新数据、重新执行页面渲染,然后调用 RadonMyersDiff 对新旧 Radon 子节点做比较:radon_page.cc:227、radon_page.cc:375。
例如:
旧树:<text>10</text>
新树:<text>11</text>
Diff 结果:只更新 text 内容为 11
1.4.2 Fiber 开启:不是“完全没有 Diff”
Fiber 路径不再走上面的 Radon C++ 旧树/新树比较,但仍然要计算变化。当前 ReactLynx 文档把它拆成 Diff Changes → Pack Changes → Parse Changes → Patch Changes:后台侧计算并打包变化,主线程解析变化并更新 Element Tree,后面继续 Resolve、Layout、UI Operation、Paint。Render Time Analysis
源码里,Fiber 分支通过 UpdatePage 消息进入核心运行时:template_assembler.cc:651。因此“ NoDiff ”更准确的理解是:不走传统 RadonDiff,不是完全不判断变化,也不是不做 Patch。
1.4.3 这几个词应该这样记
| 词 | 先记住的含义 |
|---|---|
| RL2 | ReactLynx 2,通常对应 React + 旧 Radon 更新路径 |
| RL3 | ReactLynx 3,通常对应 React + Fiber 更新路径 |
| TTML | 模板/DSL,本身不等于 Diff 或 NoDiff |
| RadonDiff | 传统 Radon 节点树的新旧比较 |
| NoDiff | 不走传统 RadonDiff,但仍会计算变化并 Patch 节点 |
源码的运行模式标记也印证了这个组合关系:Fiber 关闭时 React/TTML 分别记为 reactlynx2/ttml_radondiff,Fiber 开启时分别记为 reactlynx3/ttml_nodiff:event_tracker.cc:34。
学习时先按“更新数据 → 计算变化 → Patch 节点 → Resolve/Layout/Flush/Paint”理解即可;只有需要追源码或分析 Trace 时,再进一步区分 RadonDiff 和 Fiber。
1.5 客户端常用 API 怎么选
Android 侧建议先把 API 分成“加载模板、更新数据、重新开始、宿主环境控制”四类。不要把 reload 和 reloadTemplate 一概理解成“重新下载模板”。
| 场景 | Android 入口 | 作用 | 是否重新下载/解析模板 |
|---|---|---|---|
| 首次加载或切换模板 | LynxView.loadTemplate(LynxLoadMeta) | 提供 URL、二进制或预解析 Bundle,并带上 initialData/globalProps,启动页面 | URL 可能触发 Provider/Fetcher;Bundle/二进制可直接加载 |
| 已加载页面的数据变化 | updateMetaData(LynxUpdateMeta) | 更新 updatedData,也可同时更新 updatedGlobalProps | 否,只走数据更新流水线 |
| 兼容旧代码的数据更新 | updateData(TemplateData/String/Map) | 只更新页面数据的便捷入口 | 否 |
| 清空数据后再给新数据 | resetData(TemplateData) | 设置 reset_page_data,清掉当前页面数据后更新 | 否 |
| 用同一模板重新开始 | reloadTemplate(data, globalProps) | 清理旧组件、触发 OnReload/页面重新加载生命周期,再用新数据渲染 | 否;源码明确不重新 Decode/PageConfig |
最容易记的调用关系是:
loadTemplate(meta) // 模板 + 首屏数据
↓
updateMetaData(updateMeta) // 页面已存在,只改数据/全局属性
↓
reloadTemplate(data, props) // 同一模板,清理旧页面状态后重新开始
loadTemplate 的公开注释称它是客户端加载 Lynx 模板的主入口:LynxView.java:650。LynxLoadMeta 可以携带 URL、二进制、ByteBuffer、TemplateBundle、initialData 和 globalProps:LynxLoadMeta.java:10。
updateMetaData 是当前推荐的组合更新入口,LynxUpdateMeta 同时容纳 updatedData 和 updatedGlobalProps:LynxUpdateMeta.java:7。Android 代码仍保留 updateData,源码中的 TODO 也说明后续会把旧的 updateData/updateGlobalProps 迁移到 updateMetaData:LynxView.java:942。
reloadTemplate 与普通 updateData 的关键区别在于:Native 侧将它标记为 reload_template,先调用旧页面/组件的 reload 清理,再派发新的页面更新;Fiber 和非 Fiber 还分别走不同的旧组件移除路径:template_assembler.cc:1145。它要求页面已经完成 loadTemplate,否则会报 early reload:template_assembler.cc:1160。
这里还有一个命名陷阱:
- Android 业务 API 通常写
lynxView.reloadTemplate(data, globalProps); - Web API 文档写
lynx.reload(),含义是 Reload 当前页面; - DevTool/调试用的
PageReloadHelper.reload(ignoreCache)可能重新拉取模板,不能拿它代表业务侧的reloadTemplate。
除了这三个主流程 API,实际接入中最常用的还有:
| API | 用途 |
|---|---|
updateViewport(...) | 宿主尺寸变化时通知 Lynx 重新布局,不是业务数据更新 |
updateFontScale(scale) | 宿主字体缩放变化 |
sendGlobalEventToLepus(...) / sendGlobalEvent(...) | 宿主向页面派发全局事件,不必伪装成数据更新 |
addLynxViewClient(...) | 监听 onPageStart、onLoadSuccess、onFirstScreen、onPageUpdate、onDataUpdated、onFlushFinish、错误和性能回调 |
destroy() | 页面不用时释放 LynxView;Android 官方文档提示不主动释放可能造成内存泄漏 |
renderTemplate(...) / renderTemplateBundle(...) | 旧的直接渲染入口;新代码优先使用 loadTemplate(LynxLoadMeta) |
loadLynxML(...)、renderSSR(...)、ssrHydrate(...) | LynxML 或 SSR 特殊场景,不属于普通数据更新主链路 |
因此业务侧的选择口诀是:换模板用 loadTemplate,改数据用 updateMetaData,清空数据但不重启页面语义用 resetData,同一模板重新走一遍页面生命周期用 reloadTemplate。
1.6 调用之后 Lynx 内部发生什么
可以把客户端 API 看成“给渲染引擎发不同类型的命令”。最终都会影响 Element Tree 和 Android View,但影响范围不同:
客户端 API
↓
LynxView
↓
LynxTemplateRender / JNI
↓
TemplateAssembler(TASM)
↓
节点树变化(Radon Diff 或 Fiber Changes/Patch)
↓
Resolve → Layout → UI Operation Flush → Paint
↓
Android View 属性、子 View、位置或可见性发生变化
1.6.1 loadTemplate:从“没有页面”建立首屏
一次标准加载大致是:
loadTemplate(meta)
↓
选择 URL / byte[] / ByteBuffer / TemplateBundle
↓
准备 LynxEngine,设置 globalProps
↓
Decode 模板与 PageConfig
↓
执行模板 VM(Lepus/JS 等)
↓
处理 initialData,建立页面初始数据
↓
首次 Render,创建节点树
↓
Resolve → Layout → Flush → Paint
↓
template_loaded = true,通知页面加载完成
Native 源码把加载阶段明确拆成 OnLoadTemplate → Decode → VMExecute → OnRenderTemplate → RenderTemplate → DidRenderTemplate → DidLoadTemplate:template_assembler.cc:985。其中首次渲染时,Fiber 和传统 Radon 会调用不同的首屏渲染实现:template_assembler.cc:583。
所以 loadTemplate 不只是“把二进制读进来”,它会建立运行时、执行模板逻辑、创建第一棵节点树,并最终创建或挂接 Android View。
1.6.2 updateData/updateMetaData:保留当前页面,只传播数据变化
调用后的主链路是:
updateMetaData(updatedData)
↓
TemplateData.flush()
↓
JNI nativeUpdateDataByPreParsedData
↓
TemplateAssembler::UpdateDataByPreParsedData
↓
ProcessTemplateData
↓
UpdateTemplate
├─ Fiber:发送 UpdatePage,计算/打包/解析/应用 Changes
└─ Radon:更新数据,重新 Render,RadonMyersDiff 新旧节点
↓
RequestResolve
↓
Layout / UI Operation / Paint
普通更新不会主动移除整页组件,也不会重新 Decode 模板;只有受影响的节点才会产生属性更新、文本更新、插入、删除或替换等 UI 操作。没有变化时,可能只走检查和 Resolve,不产生有效 Patch。
Android 的 LynxTemplateRender.updateData() 会先执行 prepareUpdateData(),再调用 nativeUpdateDataByPreParsedData():LynxTemplateRender.java:2283。Native 侧随后进入 UpdateDataByPreParsedData():template_assembler.cc:2336。
这就是我们前面沉淀的“数据更新逻辑”:数据变了,不是直接操作 Android View,而是先让 Lynx 重新计算节点变化,再把最小 UI 操作刷到 View。
1.6.3 resetData:先恢复页面数据基线,再更新
resetData(data) 和普通 updateData 走同一条 Native 更新入口,但会额外设置 reset_page_data:
resetData(data)
↓
reset_page_data = true
↓
清理/恢复当前页面数据
↓
按新数据重新计算节点变化
↓
Resolve → Layout → Flush → Paint
它的重点是“数据重置”,不是“重新加载模板”。因此模板代码、已加载的运行时和整体页面加载过程不会重新 Decode;如果只是需要清空列表、恢复默认数据,优先考虑这个 API。
1.6.4 reloadTemplate:保留模板资源,但重启页面语义
reloadTemplate 的内部标记是 reload_template = true,所以它不是普通数据更新:
reloadTemplate(data, globalProps)
↓
确认模板已经 loadTemplate
↓
更新 globalProps(如果传入)
↓
调用页面/组件 OnReload
↓
移除旧组件
├─ Fiber:发送 RemoveComponents
└─ Radon:RemoveOldComponentBeforeReload
↓
通知 JS App Reload
↓
用新数据执行 UpdateTemplate(reload_template=true)
↓
重新触发页面加载完成相关回调
源码注释明确写着这里“不需要重新 Decode 和设置 PageConfig”;但它会清理旧组件,并把这次更新当成一次新的模板加载语义处理:template_assembler.cc:1195、template_assembler.cc:1233。传统 Radon 还会打开整树 Diff、使用新的组件数据并刷新生命周期:radon_page.cc:390。
因此它的实际效果更接近“同一份模板重新创建页面组件”,而不是“给当前组件打一个数据补丁”。这也是为什么 reloadTemplate 比 updateData 更重,组件内部状态、子组件数据和生命周期都可能重新开始。
1.6.5 其他 API 对内部链路的影响
sendGlobalEvent(...):只负责把事件送进页面;是否产生节点变化,取决于事件处理函数是否调用setState/setData。updateViewport(...):主要让布局系统重新计算尺寸,不改变业务数据。updateFontScale(...):更新字体缩放并触发受影响文本重新布局。destroy():释放 LynxView、运行时和相关资源,之后不能继续把它当作有效页面使用。addLynxViewClient(...):不改变渲染结果,只用来观察onLoadSuccess、onPageUpdate、onDataUpdated、onFlushFinish、错误和性能时序。
从客户端视角可以把整个生命周期压缩成:
创建 LynxView
↓
loadTemplate(initialData) 建立页面和首屏节点
↓
updateMetaData(updatedData) 保留页面,局部传播变化
↓
resetData(data) 重置数据基线后更新
或
reloadTemplate(data, props) 清理组件,重新开始页面语义
↓
destroy() 释放页面
1.7 各 API 总对比
前面先分别看了每个 API 的内部动作,最后可以用下面这张表做横向对比:
| 对比项 | loadTemplate | updateData / updateMetaData | resetData | reloadTemplate / reload |
|---|---|---|---|---|
| 操作对象 | 模板资源、运行时、首屏数据 | 当前页面数据/全局属性 | 当前页面数据基线 | 当前页面组件和页面生命周期 |
| 是否需要模板输入 | 需要 URL、二进制或 Bundle | 不需要,使用已加载模板 | 不需要,使用已加载模板 | 不需要,使用已加载模板 |
| 是否可能下载模板 | URL 模式可能下载 | 否 | 否 | 业务 API 否;DevTool reload 可能下载 |
| 是否重新 Decode/PageConfig | 是(预解析 Bundle 可跳过部分 Decode) | 否 | 否 | 否 |
| 是否重新初始化运行时 | 建立或准备 LynxEngine | 否 | 否 | 否,复用当前模板运行时 |
| 数据处理 | initialData 作为首屏数据 | 按更新字段传播数据,可附带 globalProps | 先重置数据,再应用新数据 | 使用新数据作为一次重新加载的数据 |
| 节点树处理 | 创建第一棵节点树 | Fiber Changes/Patch 或 Radon 新旧树 Diff | 带 reset 标志的数据更新 | 移除旧组件后按 reload 语义重新渲染 |
| Android View 影响 | 创建/挂接首屏 View | 只更新受影响的 View,必要时增删 View | 按重置后的变化更新 View | 旧组件/View 可能被移除并重新建立 |
| 页面生命周期 | 首次加载生命周期 | 更新生命周期 | 数据更新生命周期 | OnReload、JS App Reload、重新加载完成语义 |
| 典型场景 | 首次打开、切换模板 | 接口返回数据、状态刷新 | 清空列表、恢复默认数据 | 切换账号/页面上下文、要求页面重新开始 |
| 操作重量 | 最大 | 最轻 | 中等 | 大于普通更新 |
最简记忆方式是:
loadTemplate 改“模板 + 运行时 + 首屏”
updateData 改“当前页面的数据”
resetData 改“当前页面的数据基线”
reloadTemplate 改“当前页面的组件生命周期”
它们最终都可能进入同一个后半段:
节点变化
→ Resolve
→ Layout
→ UI Operation Flush
→ Paint
区别不在于最后是否绘制,而在于绘制之前,哪一层状态被重置,以及节点变化是局部 Patch 还是整页重新开始。
也可以把四个主入口画成“状态复用范围”对比:
本节小结:
initialData跟着loadTemplate进入首屏;updateData只改当前页面,resetData重置数据基线,reloadTemplate复用模板但重新开始组件生命周期。四者最后都会回到 Resolve、Layout、Flush、Paint。
2. 从运行时节点到像素管线:真正的交接边界
前面的链路经常被简写成“Lepus 生成 VDOM,再转成 Native View”。这个说法能帮助入门,但会掩盖两个重要事实:
- Fiber 和传统 Radon 生成、比较节点的方式不同;
- 二者进入样式、布局和平台渲染前,都会汇合到 C++
Element Tree。
换句话说,Element 才是运行时结果进入 Lynx 像素管线的共同边界;Android View 还在更后面。
2.1 Fiber:运行时直接调用 C++ Element API
Fiber 首屏渲染时,RenderTemplateForFiber 向模板运行时发送 RenderPage;后续数据更新则发送 UpdatePage。模板运行时执行编译产物时,可以调用一组注册好的 Native Element API:
RenderPage / UpdatePage
↓
Lepus 执行模板编译产物
↓
__CreateElement / __CreatePage / __CreateText ...
__SetAttribute / __SetClasses / __SetInlineStyles ...
__AppendElement / __RemoveElement / __InsertElementBefore ...
↓
C++ Fiber Element Tree
↓
__FlushElementTree(root, options)
这些名称不是示意名。renderer_ng.cc 把 __CreateElement、__AppendElement、__SetAttribute、__FlushElementTree 等函数逐项注册到对应的 C++ RendererFunctions:renderer_ng.cc:333。
以创建和挂接节点为例:
FiberCreateElement根据 tag 调用ElementManager::CreateFiberNode,返回一个由 Lepus Value 持有的 C++Element引用:renderer_functions.cc:2699;FiberAppendElement取出父子Element引用,直接调用parent->InsertNode(child):renderer_functions.cc:3203;- 属性、class、inline style 和事件也通过同一组 binding 修改对应
Element,而不是先创建 Android View。
所以 Fiber 路径并不是“Lepus 产出一份完整 JSON VDOM,C++ 再整体反序列化”。从当前源码能直接证明的实现是:模板运行时通过绑定函数增量创建、连接和修改 C++ Element 对象。
__FlushElementTree 是这一段的提交点。它接收本次变化的根节点和 PipelineOptions;如果传入的是尚未挂到主树上的 TemplateElement,会先向上找到已挂载的非 template 祖先。统一像素管线开启时,它主要记录 resolve_requested 和本次 resolve 的目标节点;旧管线则直接进入 OnPatchFinish:renderer_functions.cc:5314、renderer_functions.cc:5559。
这带来三个容易忽略的边界:
FlushElementTree表示“节点变化已提交给后续管线”,不等于 Android 已经绘制;- 开启
emptyPatch跳过策略时,没有有效 Patch 可以不请求 Resolve; asyncFlush分支只异步处理子树属性并提前返回,不能套用普通同步链路的时序判断:renderer_functions.cc:5519。
2.2 Radon:先比较 Radon Tree,再把变化派发到 Element Tree
Fiber 关闭时,模板渲染会先形成 Radon 节点。首次派发一个 Radon 节点时,当前实现会延迟创建底层 Element,再把 id、class、attribute、style、event 等状态写进去并插入父节点:
Radon Node
↓ DispatchSelf
CreateElementIfNeeded
↓
CreateFiberElement / CreateFiberPage
↓
DispatchFirstTime 写入属性、样式、事件
↓
InsertElementIntoParent
↓
Element Tree
源码入口分别是 CreateElementIfNeeded、DispatchFirstTime 和 DispatchSelf:radon_node.cc:80、radon_node.cc:156、radon_node.cc:278。
更新时,RadonMyersDiff 比较新旧子节点序列:删除项从 Element Tree 移除,新增项执行 DispatchForDiff,可复用项交换已有 Element 后递归比较子节点:radon_base.cc:331。
这里有一个很容易被类名误导的细节:传统 Radon 路径当前也调用 CreateFiberElement 创建底层节点。它不代表 Radon Diff 消失了,而是说明“上层怎么计算变化”和“下层用什么 Element 承载变化”是两个维度。
| 阶段 | Fiber 路径 | Radon 路径 |
|---|---|---|
| 节点变化由谁计算 | 模板运行时/前端框架的 Fiber 更新逻辑 | C++ Radon 新旧树 Diff |
| 如何创建底层节点 | Lepus binding 直接创建 Element | Radon dispatch 时按需创建 Element |
| 父子关系如何变化 | Append/Insert/Remove binding 直接改 Element Tree | Diff 结果驱动 Element Tree 增删和复用 |
| 后续共同入口 | Element Tree 的 dirty/action 与 Resolve 请求 | Element Tree 的 dirty/action 与 Resolve 请求 |
2.3 PipelineScope:把一次节点变化收束成一条像素管线
首屏 LoadTemplateInternal 和客户端 UpdateDataByPreParsedData 都会建立 PipelineScope:template_assembler.cc:1077、template_assembler.cc:2348。PipelineScope 析构退出时调用 RunPixelPipeline(),因此前面累积的 Resolve、Layout、Flush 请求会在一次明确的管线上下文中按状态推进:pipeline_scope.cc:14。
统一像素管线的主状态可以压缩成:
RunPixelPipeline 并不是无条件跑完整棵树:
- 只有
resolve_requested为真才执行ElementManager::ResolveStyle; - Resolve 可以带
target_node,只从本次变化相关的根节点开始; ResolveStyle最终调用element->FlushActionsAsRoot(),并根据是否真的产生 Patch 设置layout_requested;- 只有
layout_requested为真才请求 Layout;否则直接走OnLayoutAfter(layout_triggered=false)收尾。
对应实现:template_assembler.cc:3779、element_manager.cc:1710。
这也解释了为什么一次 updateData 不一定引发重新布局:数据值虽然进入了运行时,但若最终没有节点 Patch,或变化只需要无需布局的更新,管线可以提前结束或只提交相应 UI 操作。
2.4 Starlight Layout:计算几何结果,不负责直接画 View
TASM 的 RequestLayout 经 LayoutScheduler 转给 TasmMediator,再由 layout_actor_ 调用布局侧的 DispatchLayoutUpdates:template_assembler.cc:3755、tasm_mediator.cc:904。
布局侧 LayoutContext 会先检查根节点是否 dirty、布局和 viewport 是否已就绪。真正计算时按以下顺序执行:
DispatchLayoutBeforeRecursively
↓
root_->CalculateLayout(...) Starlight 计算
↓
LayoutRecursively(...) 回写每个节点的布局结果
↓
UpdateRootSize
↓
OnLayoutAfter(options, ..., true)
源码在 LayoutContext::Layout 中直接给出了这个顺序:layout_context.cc:809。CalculateLayout 产出宽高和偏移等几何结果;它本身不创建 Android View,也不等于 Canvas 已经绘制。
线程方面只能做有边界的结论:请求通过 layout_actor_ 发送到布局执行体,RunPixelPipeline 的源码也明确兼容同步和异步 Layout。实际是否独立布局线程,仍受当前 ThreadStrategyForRendering、平台 runner 和页面配置影响,不能只看函数名断言固定线程:template_assembler.cc:3824。
2.5 UI Operation Flush:提交平台操作,不等于屏幕已经出帧
布局完成后,TemplateAssembler::OnLayoutAfter 把 Pipeline 推进到 kUIOpFlush,然后调用 PaintingContext::Flush():template_assembler.cc:3972。Android PaintingContext 中,前面的节点创建、属性和布局动作已经被编码为闭包或 compact batch;普通 Flush 会先整理 batch,再执行 UI Operation Queue:painting_context_android.cc:920、painting_context_android.cc:1440。
如果启用了 compact batch,Native 会通过 JNI 调用 Java flushUIOperationBatch,再由 Java PaintingContext/LynxUIOwner 落到具体 LynxUI:painting_context_android.cc:1497。
还要区分两种提交时机:
- 普通模式:
Flush()立即进入FlushImmediately(),执行队列; - VSync 对齐模式:
Flush()会先返回,FinishLayoutOperation转而请求平台 Layout,在后续对齐的时机提交:painting_context_android.cc:920、painting_context_android.cc:946。
因此几个“完成”信号不能混为一谈:
| 观察点 | 能证明什么 | 不能证明什么 |
|---|---|---|
FiberFlushElementTree | Fiber 节点变化已提交到后续管线 | Layout、View 更新或绘制已完成 |
| Resolve 结束 | dirty action 已解析,是否需要布局已确定 | 平台 View 已更新 |
OnLayoutAfter | Starlight 几何计算已结束或本次无需布局 | Canvas 已出帧 |
| UI Operation Queue Flush | 平台 UI 操作已提交/执行到对应队列 | 用户一定已经看到该帧 |
Android dispatchDraw/帧 Trace | 已进入平台实际绘制阶段 | 下一次合成和显示时刻完全等同于回调时刻 |
排查“数据已返回但页面没变化”时,可以按这五个边界逐层打点。这样能快速判断问题停在运行时节点生成、Element Resolve、Starlight Layout、UI Operation,还是 Android 最终绘制。
本节小结:Fiber 通过 Lepus binding 直接构造
Element Tree,Radon 通过 Diff 和 dispatch 修改同一棵底层树;之后由PipelineScope统一推进 Resolve、Starlight Layout 和 UI Operation Flush。任何一个 Flush 回调都不应直接等同于“用户已经看到这一帧”。
3. 从模板节点到 Android View
3.1 标签到 Behavior
节点创建时,LynxUIOwner 使用标签名查找 Behavior:
Behavior behavior = mBehaviorRegistry.get(tag);
ui = behavior.createUIWithParams(mContext, params);
代码证据:LynxUIOwner.java:1754。
如果没有注册对应 Behavior,BehaviorRegistry.get() 会抛出 No BehaviorController defined for class …;如果 Behavior 没有覆写 createUI,默认代表虚拟节点,不创建真实 UI。BehaviorRegistry.java:47、Behavior.java:80
3.2 Behavior 到 LynxUI 到 Android View
<explorer-input>
↓ tag 查 Behavior
LynxExplorerInput extends LynxUI<AppCompatEditText>
↓ initialize()
createView(Context)
↓
AppCompatEditText
对应的对象关系可以压缩成下面这张适配图:
LynxUI.initialize() 会调用 getOrCreateView();如果没有复用的 View,则调用子类覆写的 createView():LynxUI.java:217、LynxUI.java:255。
源码还支持异步创建 View,并通过 LynxThreadPool.postUIOperationTask 执行创建任务;最终会在 ensureCreateView() 中补齐 View:LynxUI.java:304、LynxUI.java:319、LynxUI.java:355。
3.3 属性、布局与更新
前端属性通过 @LynxProp 进入 LynxUI 的 setter,setter 再修改 Android View。布局完成后,onLayoutUpdated() 根据 Lynx 计算出的 bounds 更新 View 的可见性;measure() 设置 LayoutParams 并测量原生 View。LynxUI.java:847、LynxUI.java:857
因此后续数据更新不是把整棵页面重新转成 View,而是由渲染侧产生节点增删/属性变化,再由已有 LynxUI 更新已有 Android View;新增节点才需要创建新 View。
本节小结:Lynx 内部节点不会全部变成 Android View。只有注册了 Behavior、需要真实界面的节点,才会由 LynxUI 创建或更新对应 View;虚拟节点仍留在引擎内部。
4. 交互事件的代码落点
4.1 原生组件事件回传
原生组件可以监听 Android View 的回调并发送 Lynx 自定义事件:
LynxCustomEvent event = new LynxCustomEvent(getSign(), "input");
event.addDetail("value", text);
getLynxContext().getEventEmitter().sendCustomEvent(event);
事件类型和参数容器定义在 LynxCustomEvent.java:11。官方自定义元件文档提供了 TextWatcher → sendCustomEvent 的完整示例:Custom Element。
4.2 事件后的数据更新
交互后的数据更新仍回到第 6 节:普通业务事件进入 JS Runtime,调用 setData/React state;宿主侧事件也可以调用 updateData/updateMetaData,最后由渲染侧更新 View。当前 Lynx 还支持主线程事件处理,但它是额外的 Main Thread Script 能力,适合手势和动画,不等于所有业务逻辑都放到主线程。Event Handling
本节小结:Android 的点击、输入等先变成 Lynx 事件,再由 JS 或宿主决定是否更新数据;事件本身不等于页面已经更新,真正的 View 变化仍要走数据更新和 UI Flush。