Skip to main content

OTA 第二次进入「远程 bundle 不可用」分析

📋 已归档 · 排查笔记(非最终方案)
索引docs/fixes/README.md
面试叙事:stories/01-ota-reentry-registry-lifecycle.md · interview/ota-reentry-case-study.md

Date: 2026-07-09
Status: Investigation (superseded by interview case study)
Scope: OTA re-entry、FeatureHost 错误展示、bundleLoader


先下结论

服务端 v0.0.6、本地 v0.0.6 一致,基本可以排除「bundle 没下载 / 被删 / 版本对不上」。

这条文案是通用错误壳,真实失败多半发生在 split bundle 加载 / 注册 阶段,而不是 manifest 或磁盘缓存阶段。


这条错误从哪来

UI 文案来自 formatRemoteBundleError

export function formatRemoteBundleError(featureId: string, cause?: string): string {
const base = `远程 bundle 不可用(服务端 ota_${featureId} 可能已删除或未 upload)。请重新 upload 后再试。`;
// ...
return base;
}

第二次进入时在 catch所有加载错误都会被包成同一句:

setError({
message: formatRemoteBundleError(featureId, raw),
...versions,
});

所以页面上看不到真正的 raw 原因(例如 split bundle entry failednot registered 等)。

版本号来自 resolveOtaVersionContext:读 manifest 的 remote + 沙盒 metadata 的 local。即使 load 失败,只要 metadata 还在,就会显示 v0.0.6 / v0.0.6。


失败发生在哪一阶段

第一次能进、第二次失败 → 高概率卡在 B 阶段(load / register),不是 A 阶段(缓存 / 下载)


第二次进入的可能原因(按概率)

1. Split segment 二次加载 + 入口 __r 失败(最可能)

Native registerSegmentWithId 对同一 segment 不会 re-eval。第二次进入时:

  • clearFeatureRegistration 清掉了 JS registry
  • Native segment 仍在
  • 只跑 executeSplitBundleEntry__r(entry) 可能失败

抛出:split bundle entry failednot registered → 被 UI 包成「远程 bundle 不可用」。


2. DEV re-entry 的 full eval 失败(v0.0.6 风险更高)

第二次进入会走 reevaluateSplitBundleFromDisk(full eval 整个 bundle):

if (__DEV__ && otaReentry) {
const reevaluated = await reevaluateSplitBundleFromDisk(path, segmentId);
if (reevaluated && isFeatureLoadedFromOta(feature.id)) {
// 成功则 return
}
}
// 否则 fallback 到上面第 1 条路径

v0.0.6 的 bundle ~282KB,含 React Navigation 等依赖。Full eval 可能:

  • Requiring unknown module "…"
  • React 重复 / hooks 冲突
  • eval 抛错但被 catch 吞掉,只打 [bundleLoader] split bundle re-eval failed: …

然后 fallback 到第 1 条路径,仍然失败。


3. Re-eval 成功但未 registerFeature

reevaluateSplitBundleFromDisk 在 eval 没 throw 时就返回 true,即使末尾 __r(entry) 没跑到或 registerFeature 没执行。

此时 isFeatureLoadedFromOta 为 false → 继续走 native load + entry → 仍可能失败。


4. Metro 残留状态(你的复现路径)

Metro → OTA 第一次 OK → 第二次 OTA 失败:

  • Metro 可能污染 module 表 / Fast Refresh 状态
  • 第一次 OTA 标记了 wasOtaFeatureLoadedThisSession → 第二次强制 re-eval
  • 与第 2、3 条叠加

5. 真·缓存问题(相对不太可能)

若本地文件被删、isCachedBundleUsable 失败、metadata 与文件不一致,会在 ensureFeatureCached 阶段就 bundlePath = null

但你第一次已成功、版本仍显示 v0.0.6,说明 metadata 和文件大概率还在。


6. 主包 vs split module id 不一致(Metro --reset-cache

若 Metro 用了 --reset-cache,主包 module id 变了,而 OTA split 是旧主包 build 的:

  • 第一次有时能「碰巧」加载
  • 第二次 re-eval / re-entry 更容易炸

需重新 build:bundles + upload,且 Metro 不要 reset-cache 后混用旧 split。


7. UI 误导(一定存在)

formatRemoteBundleError 无论真实原因都返回同一句,所以看起来像 upload 问题,实际是 load/register 问题。


怎么快速确认是哪一种

在 Xcode / Metro 日志里搜这些关键字:

日志含义
[SplitBundleLoader] load ... otaReentry=true第二次进入,走了 re-entry 路径
[bundleLoader] split bundle re-eval failed:full eval 失败(看后面具体原因)
split bundle entry __r(...) failed入口 module 加载失败
not registeredbundle 加载了但 registerFeature 没执行
Requiring unknown modulemodule 表 / segment 映射问题

也可在报错页面临时加一行展示 raw 错误(当前被 formatRemoteBundleError 吃掉了)。


小结

判断说明
版本一致缓存/metadata 基本正常,不是「没 upload」
第二次才失败典型 segment 不 re-eval + registry 被清re-eval fallback 失败
v0.0.6bundle 更大,re-eval 路径风险比 0.0.1 高
当前 UI掩盖真实错误,需要看 console 才能定位

后续开发方向

  1. 改进错误展示:版本一致时露出 raw 原因,区分「缓存问题」与「load/register 问题」
  2. 加固第二次 re-entry:避免对大 bundle 做 harmful full eval;保留 segment + entry 路径
  3. 必要时 native 层 force re-eval segment(Release 场景)