Android Complex Demo 只显示标题 — 根因分析与修复

日期

2026-07-07

现象

Android 上加载 complex 组件时白屏/只显示标题区域,其余所有内容不可见。

根因(三层问题叠加)

1. Taitank 不支持百分比字符串 → 所有节点 width=0

JS normalizeStyleForNative() 保留 "100%" 字符串以兼容 Yoga(OHOS),但 Taitank(Android)只能处理数值。"100%" 到达 TaitankLayoutNode::Parser() → IsNumber()=false → SetWidth(NAN) → 所有子节点 layout width=0。

  • Yoga:YG_SET_NUMBER_PERCENT_AUTO_DECL 宏 → YGNodeStyleSetWidthPercent()
  • Taitank:CheckValueType() 仅 WARNING,值被静默丢弃

2. ScrollView 多子节点 → NestedScrollView 异常

NativeBackendElement.appendChild() 中的动态 wrapper 机制调用 HippyBridge.moveNode() 跨 parent 移动子节点,但:

  • C++ MoveRenderNode 通过 JNI 调用 Java moveNode(int, byte[]) → NativeRenderProvider → RenderManager.moveNode(rootId, pid, list)(同 parent 重排序重载)
  • 跨 parent 移动在 Java RenderNode tree 中不生效
  • NestedScrollView.addView() 抛出 IllegalStateException: ScrollView can host only one direct child

3. triggerRender 同步调用 → batch 分裂 → CreateDomNodes SKIP

这是最关键的问题。模板初始化期间,triggerRender → render() 同步调用 flushAndBuild → flushQueue。此时只有部分元素已完成 appendChild,其余元素尚未创建。结果:

Batch 1 (1 node):  wrapper(id=3)→ SKIP (parent id=2 不存在)
Batch 2 (3 nodes): children → ALL SKIP
Batch 3 (290 nodes): 2 created (id=1,2), 288 SKIP

C++ CreateDomNodes 使用 needSortByIndex=true 排序。同一 batch 内 parent 不存在的节点 depth=-1 排在最前 → "PARENT NOT FOUND" → SKIP。且 createNodeWithId 是一次性的——SKIP 后不会再重试。

最终修复方案

修复 1:模板层包装(index.hxml)— React 模式

为 complex 组件的两个 <scroll-view> 添加 wrapper <view>,确保 NestedScrollView 只有一个直接子节点:

<scroll-view>
  <view style="width:100%">          ← wrapper (外层)
    <view class="header">...</view>
    <scroll-view scroll-x>            ← tab bar
      <view style="flex-direction:row">  ← wrapper (内层)
        <view class="tab">全部</view>
        ...
      </view>
    </scroll-view>
    <view class="content">...</view>
    <view class="footer">...</view>
  </view>
</scroll-view>

React hippy-react 始终为 ScrollView 包裹 wrapper View——这是已验证的模式。

修复 2:JS 层防止重复包装(NativeBackendElement.ts)

在动态 wrapper 代码中增加检查:如果已有子节点是 <view>(模板级 wrapper),不再创建额外 wrapper:

const existingIsView = existing.tagName === 'view';
if (!isWrapped && !existingIsView) {
  // create wrapper...
}

修复 3:JS 层 recreate-on-flush — 解决 batch 分裂(NativeBackendElement.ts)

这是解决白屏的核心修复。triggerRender 同步调用会导致部分元素在 parent 不存在时被 SKIP。修复方案:

  • appendChild 中不设置 _isMounted=true(C++ create 可能被 SKIP)
  • flush() 中:如果 _justCreated=true,每次 flush 都重新发出 createNodeWithId
  • flush() 末尾设置 _isMounted=true + 清除 _justCreated
// flush() 中:
if (this._justCreated) {
  // 每次 flush 都重发 create,直到 parent 存在、C++ create 成功
  HippyBridge.createNodeWithId(...);
}
// ... 然后
HippyBridge.updateNode(...);
this._isMounted = true;
this._justCreated = false;

这样即使第一个 batch 的 create 被 SKIP,后续 batch 会重试,直到 parent 存在。

修复 4:JS 层根元素像素值(NativeBackendElement.ts)

appendChild ROOT-DIM-FIX:将 "100%" → getWindowWidth()/getWindowHeight() 像素值。 修正根元素检测条件:增加 parentIsOutsideJsTree(parent 不在 elementMap 中 = root-level)。

修复 5:诊断日志

C++: CheckValueType DLOG→LOG, SET_STYLE_VALUE_DIM 宏, DomNode::SetLayoutSize/DoLayout, RootNode::DoAndFlushLayout Java: NativeRenderer createNode/updateLayout/endBatch/moveNode, RenderManager batch

探索过的无效方案

方案 问题
setContext() pre-create wrapper + 立即 flush C++ batch 排序导致 wrapper (depth=正确) 排在 children (depth=-1) 之后 → children 先处理 → SKIP
render() 延迟到 microtask 无法阻止同步 triggerRender 调用, batch 仍分裂
JS wrapper + moveNode (原有方案) moveNode 在 Java RenderNode tree 不生效
仅模板 wrapper (无 recreate-on-flush) batch 分裂导致 288/290 节点 SKIP → 白屏

参考

  • React hippy-react/src/components/scroll-view.tsx:始终在 render() 中包裹 <View> wrapper
  • Vue 3 hippy-vue-next:仅 warn,不处理多子节点
  • 上游 Hippy Android 原生层:NestedScrollView / HorizontalScrollView 要求恰好一个直接子节点,不做自动包装

涉及文件

文件 变更
demos/ascf-ai-demo/ascf/ascf_src/skills/apidemos/components/complex/index.hxml 为两个 ScrollView 添加 wrapper View
driver/js/packages/hippy-ascf/src/native-backend/NativeBackendElement.ts ROOT-DIM-FIX 像素值, existingIsView 检查, recreate-on-flush
dom/src/dom/taitank_layout_node.cc 诊断日志 + SET_STYLE_VALUE_DIM 宏 + GetRootStyleWidth/Height
dom/src/dom/dom_node.cc SetLayoutSize/DoLayout/TransferLayoutOutputsRecursive 日志
dom/src/dom/root_node.cc DoAndFlushLayout 日志
dom/include/dom/taitank_layout_node.h GetRootStyleWidth/GetRootStyleHeight 声明
renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderer.java createNode/updateNode/updateLayout/endBatch/moveNode 日志
renderer/native/android/src/main/java/com/tencent/mtt/hippy/uimanager/RenderManager.java batch 日志