Lynx 渲染原理(二):数据更新、Fiber/Radon 与 View 交互

2 阅读19分钟

Lynx 渲染原理(二):数据更新、Fiber/Radon 与 View 交互

先比较客户端 API,再串起数据变化、Fiber/Radon、布局、平台 View 和事件回传。

本系列共三篇:模板加载、数据更新与交互、性能优化。每篇独立编号,源码链接固定到核对版本;本文为静态代码与文档分析,不代表已完成真机性能验证。

目录

    1. initialData、updateData 与 loadTemplate
    1. 从运行时节点到像素管线:真正的交接边界
    1. 从模板节点到 Android View
    1. 交互事件的代码落点

1. initialData、updateData 与 loadTemplate

1.1 API 角色总览

loadTemplate = 加载/切换模板并启动一次渲染
initialData  = 这次加载的首屏初始数据
updateData   = 模板已加载后的数据更新

1.2 initialData

initialData 属于 LynxLoadMeta,在进入 Native 加载前会被处理并传入渲染侧;预解析 Bundle 路径还会执行 flush()、取得 Native pointer、标记数据已消费,然后作为参数传给 Native:LynxTemplateRender.java:1841LynxTemplateRender.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:922LynxTemplateRender.java:2283LynxTemplateRender.java:2306

当前 API 文档中的宿主更新入口是 updateMetaData(LynxUpdateMeta);文章和旧 API 中的 updateData 是同一类语义的旧/兼容入口。updateMetaData

1.4 更新逻辑(学习版)

先不要把 Fiber 和 Radon 当成两个线程。这里的“Fiber 开关”首先是 Bundle/PageConfig 中的架构选择,源码在解码编译选项时写入 enable_fiber_archlynx_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:227radon_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 这几个词应该这样记
先记住的含义
RL2ReactLynx 2,通常对应 React + 旧 Radon 更新路径
RL3ReactLynx 3,通常对应 React + Fiber 更新路径
TTML模板/DSL,本身不等于 Diff 或 NoDiff
RadonDiff传统 Radon 节点树的新旧比较
NoDiff不走传统 RadonDiff,但仍会计算变化并 Patch 节点

源码的运行模式标记也印证了这个组合关系:Fiber 关闭时 React/TTML 分别记为 reactlynx2/ttml_radondiff,Fiber 开启时分别记为 reactlynx3/ttml_nodiffevent_tracker.cc:34

学习时先按“更新数据 → 计算变化 → Patch 节点 → Resolve/Layout/Flush/Paint”理解即可;只有需要追源码或分析 Trace 时,再进一步区分 RadonDiff 和 Fiber。

1.5 客户端常用 API 怎么选

Android 侧建议先把 API 分成“加载模板、更新数据、重新开始、宿主环境控制”四类。不要把 reloadreloadTemplate 一概理解成“重新下载模板”。

场景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:650LynxLoadMeta 可以携带 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(...)监听 onPageStartonLoadSuccessonFirstScreenonPageUpdateonDataUpdatedonFlushFinish、错误和性能回调
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 → DidLoadTemplatetemplate_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:1195template_assembler.cc:1233。传统 Radon 还会打开整树 Diff、使用新的组件数据并刷新生命周期:radon_page.cc:390

因此它的实际效果更接近“同一份模板重新创建页面组件”,而不是“给当前组件打一个数据补丁”。这也是为什么 reloadTemplateupdateData 更重,组件内部状态、子组件数据和生命周期都可能重新开始。

1.6.5 其他 API 对内部链路的影响
  • sendGlobalEvent(...):只负责把事件送进页面;是否产生节点变化,取决于事件处理函数是否调用 setState/setData
  • updateViewport(...):主要让布局系统重新计算尺寸,不改变业务数据。
  • updateFontScale(...):更新字体缩放并触发受影响文本重新布局。
  • destroy():释放 LynxView、运行时和相关资源,之后不能继续把它当作有效页面使用。
  • addLynxViewClient(...):不改变渲染结果,只用来观察 onLoadSuccessonPageUpdateonDataUpdatedonFlushFinish、错误和性能时序。

从客户端视角可以把整个生命周期压缩成:

创建 LynxView
   ↓
loadTemplate(initialData)     建立页面和首屏节点
   ↓
updateMetaData(updatedData)   保留页面,局部传播变化
   ↓
resetData(data)               重置数据基线后更新
   或
reloadTemplate(data, props)   清理组件,重新开始页面语义
   ↓
destroy()                     释放页面

1.7 各 API 总对比

前面先分别看了每个 API 的内部动作,最后可以用下面这张表做横向对比:

对比项loadTemplateupdateData / updateMetaDataresetDatareloadTemplate / 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”。这个说法能帮助入门,但会掩盖两个重要事实:

  1. Fiber 和传统 Radon 生成、比较节点的方式不同;
  2. 二者进入样式、布局和平台渲染前,都会汇合到 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++ RendererFunctionsrenderer_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 的目标节点;旧管线则直接进入 OnPatchFinishrenderer_functions.cc:5314renderer_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

源码入口分别是 CreateElementIfNeededDispatchFirstTimeDispatchSelfradon_node.cc:80radon_node.cc:156radon_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 直接创建 ElementRadon dispatch 时按需创建 Element
父子关系如何变化Append/Insert/Remove binding 直接改 Element TreeDiff 结果驱动 Element Tree 增删和复用
后续共同入口Element Tree 的 dirty/action 与 Resolve 请求Element Tree 的 dirty/action 与 Resolve 请求

2.3 PipelineScope:把一次节点变化收束成一条像素管线

首屏 LoadTemplateInternal 和客户端 UpdateDataByPreParsedData 都会建立 PipelineScopetemplate_assembler.cc:1077template_assembler.cc:2348PipelineScope 析构退出时调用 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:3779element_manager.cc:1710

这也解释了为什么一次 updateData 不一定引发重新布局:数据值虽然进入了运行时,但若最终没有节点 Patch,或变化只需要无需布局的更新,管线可以提前结束或只提交相应 UI 操作。

2.4 Starlight Layout:计算几何结果,不负责直接画 View

TASM 的 RequestLayoutLayoutScheduler 转给 TasmMediator,再由 layout_actor_ 调用布局侧的 DispatchLayoutUpdatestemplate_assembler.cc:3755tasm_mediator.cc:904

布局侧 LayoutContext 会先检查根节点是否 dirty、布局和 viewport 是否已就绪。真正计算时按以下顺序执行:

DispatchLayoutBeforeRecursively
        ↓
root_->CalculateLayout(...)      Starlight 计算
        ↓
LayoutRecursively(...)           回写每个节点的布局结果
        ↓
UpdateRootSize
        ↓
OnLayoutAfter(options, ..., true)

源码在 LayoutContext::Layout 中直接给出了这个顺序:layout_context.cc:809CalculateLayout 产出宽高和偏移等几何结果;它本身不创建 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:920painting_context_android.cc:1440

如果启用了 compact batch,Native 会通过 JNI 调用 Java flushUIOperationBatch,再由 Java PaintingContext/LynxUIOwner 落到具体 LynxUIpainting_context_android.cc:1497

还要区分两种提交时机:

因此几个“完成”信号不能混为一谈:

观察点能证明什么不能证明什么
FiberFlushElementTreeFiber 节点变化已提交到后续管线Layout、View 更新或绘制已完成
Resolve 结束dirty action 已解析,是否需要布局已确定平台 View 已更新
OnLayoutAfterStarlight 几何计算已结束或本次无需布局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:47Behavior.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:217LynxUI.java:255

源码还支持异步创建 View,并通过 LynxThreadPool.postUIOperationTask 执行创建任务;最终会在 ensureCreateView() 中补齐 View:LynxUI.java:304LynxUI.java:319LynxUI.java:355

3.3 属性、布局与更新

前端属性通过 @LynxProp 进入 LynxUI 的 setter,setter 再修改 Android View。布局完成后,onLayoutUpdated() 根据 Lynx 计算出的 bounds 更新 View 的可见性;measure() 设置 LayoutParams 并测量原生 View。LynxUI.java:847LynxUI.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。