【Bug已解决】[Mobile] unimodule.json makes Expo exclude onnxruntime-react-native from autolinking - NativeModules.Onnxruntime is null 解决方案

一、现象长什么样

在 React Native + Expo 项目里集成 onnxruntime-react-native(ORT 的 RN 原生模块),iOS/Android 上运行时直接崩或拿不到原生模块:

import { NativeModules } from 'react-native';
console.log(NativeModules.Onnxruntime);   // null !
// 后续 session.create 调用因 NativeModules 为 null 而崩溃

最小信号:

原生模块没被链接 -> NativeModules.Onnxruntime === null
Expo 自动链接(autolinking)名单里没有 onnxruntime-react-native
报错/崩溃在模块加载阶段,不是推理阶段

注意:这是 Expo 自动链接配置问题,不是 ORT 推理 bug。Expo 的 autolinking 依据某些配置文件决定哪些原生模块参与链接,onnxruntime-react-native 被漏掉了。

二、背景

React Native 0.60+ 用 autolinking(自动链接):你 npm install 一个带原生代码的库,构建时 RN 的 autolinking 工具会扫描 node_modules 里各包的配置文件,自动把它们的 iOS pod / Android gradle 依赖加进工程,不用手动 react-native link

Expo 在此基础上有一套自己的模块配置(expo.modulesunimodule.json / expo-module.config 等),用来决定哪些库被 Expo 的预构建(prebuild)和原生宿主包含。

问题就出在:onnxruntime-react-native 这个包没有被正确登记进 Expo 的 autolinking 配置,于是:

  • iOS 上,pod install 时它的 pod 没被加进 Podfile,原生模块不编译进 app;
  • Android 上,它的 react-native-config / settings.gradle 条目缺失,不编进 apk;
  • 运行时 NativeModules.Onnxruntime 自然是 null,因为原生端根本没这个模块。

unimodule.json 在这里是 Expo 旧版模块清单的格式(新版用 expo-module.config.js)。如果这份清单里没有列 onnxruntime-react-native,Expo 的 autolinking 就会把它排除。

三、根因

根因是 Expo 的 autolinking 配置(unimodule.json / 模块清单)没有包含 onnxruntime-react-native,导致预构建/链接阶段把它排除,原生模块不进 app,运行时 NativeModules.Onnxruntime 为 null

  1. 清单漏登记onnxruntime-react-native 包本身可能没在 expo-module.config.js 里声明自己是 Expo 兼容模块,或宿主项目的 unimodule.json 没列它,autolinking 扫不到。
  2. autolinking 排除:Expo prebuild 依据清单生成原生工程,缺登记 -> pod/gradle 不生成 -> 原生模块缺席。
  3. 运行时 null:JS 侧 NativeModules.Onnxruntime 依赖原生端导出该模块,原生端没编进去,自然 null,任何调用崩。
  4. 不是 ORT 推理错:模块根本没加载,连 session 都建不了。

所以这不是数值错,而是 Expo 原生模块链接配置缺口,导致 ORT RN 模块没编进 app

四、最小可运行复现

下面用 JS 伪代码模拟“autolinking 清单过滤掉未登记模块,导致 NativeModules 为 null”:

// Expo autolinking 依据清单决定链接哪些模块
const EXPO_MODULE_MANIFEST = {
  modules: [
    "expo-camera",
    "expo-file-system",
    // onnxruntime-react-native 没在这里 -> 被排除
  ],
};

// autolinking 生成原生依赖时只取清单内的
function autolink(packages) {
  return packages.filter((p) => EXPO_MODULE_MANIFEST.modules.includes(p));
}

const installed = ["onnxruntime-react-native", "expo-camera"];
const linked = autolink(installed);
console.log("被链接的模块:", linked);  // 不含 onnxruntime-react-native

// 运行时原生端没编入 -> NativeModules 为 null
const NativeModules = {};
console.log("NativeModules.Onnxruntime:", NativeModules.Onnxruntime ?? null);
// -> null,因为没被链接

跑这个逻辑:onnxruntime-react-native 不在清单,autolink 过滤后不含它,运行时 NativeModules.Onnxruntimenull。这复现了“清单漏登记 -> 模块不链接 -> NativeModules 为 null”的机制。

五、解决方案(第一层:最小直接修复)

最小修复:onnxruntime-react-native 登记进 Expo 的 autolinking 配置,让原生模块被链接进 app。几种做法:

// 1) 在 onnxruntime-react-native 包里确保 expo-module.config.js 声明兼容
//    onnxruntime-react-native/expo-module.config.js:
{
  "platforms": ["ios", "android"],
  "ios": { "modules": ["Onnxruntime"] }
}

// 2) 宿主项目显式包含(expo 配置)
//    app.json / app.config.js:
{
  "expo": {
    "plugins": [
      // 某些包需要在这里声明插件以触发链接
    ]
  }
}

// 3) 重新生成原生工程
//    npx expo prebuild --clean
//    npx pod-install ios

这一步让 pod install / gradle 把 ORT RN 原生模块编进 app,运行时 NativeModules.Onnxruntime 不再是 null

六、解决方案(第二层:结构性改进)

把“哪些 RN 原生模块必须被 Expo autolinking 包含”收口成唯一的配置对象 OrtExpoAutolinkPolicy,项目配置与 CI 读它:

from dataclasses import dataclass, field
from typing import Tuple


@dataclass(frozen=True)
class OrtExpoAutolinkPolicy:
    """onnxruntime-react-native 在 Expo autolinking 的单一事实来源。"""
    # 必须被 autolinking 包含的 RN 原生模块
    required_modules: Tuple[str, ...] = ("onnxruntime-react-native",)
    # 各平台必须链接
    platforms: Tuple[str, ...] = ("ios", "android")
    # 包内必须声明的 expo 模块配置
    package_must_declare: Tuple[str, ...] = ("expo-module.config.js",)
    # prebuild 后必须验证原生模块存在
    verify_after_prebuild: bool = True

    def is_required(self, module: str) -> bool:
        return module in self.required_modules

    def describe(self) -> str:
        return "onnxruntime-react-native 必须进 Expo autolinking,iOS/Android 都链接"


POLICY = OrtExpoAutolinkPolicy()


def check_manifest(manifest_modules: list, policy: OrtExpoAutolinkPolicy = POLICY) -> list:
    missing = [m for m in policy.required_modules if m not in manifest_modules]
    if missing and policy.verify_after_prebuild:
        raise RuntimeError(f"Expo autolinking 缺失模块: {missing}")
    return missing

所有 Expo 项目配置与 CI 读同一份 POLICY,ORT RN 模块不会被漏链接。

七、解决方案(第三层:断言 / CI 守护)

把“onnxruntime-react-native 被 Expo 链接、NativeModules 非空”做成断言。下面用 pytest 风格守护(复用第四节逻辑):

import pytest


def test_module_required(policy):
    assert "onnxruntime-react-native" in policy.required_modules
    assert policy.is_required("onnxruntime-react-native") is True


def test_platforms_covered(policy):
    assert "ios" in policy.platforms
    assert "android" in policy.platforms


def test_manifest_must_include(policy):
    with pytest.raises(RuntimeError):
        check_manifest(["expo-camera"])   # 缺 onnxruntime-react-native


def test_complete_manifest_passes(policy):
    assert check_manifest(["onnxruntime-react-native", "expo-camera"]) == []

这四组断言锁住:(1) 模块必含;(2) iOS/Android 都覆盖;(3) 清单缺模块即失败;(4) 齐备通过。CI 跑通即代表 autolinking 配置不被漏。

八、排查清单

遇到 Expo 上 NativeModules.Onnxruntime 为 null:

  1. 看 autolinking 清单unimodule.json / expo-module.config.js 里有没有 onnxruntime-react-native。
  2. 看 pod/ gradle 是否生成Podfile.lock / settings.gradle 里有没有 ORT RN 原生依赖。
  3. 查包是否声明 expo 兼容onnxruntime-react-native 里有没有 expo-module.config.js
  4. 临时修复:把它加进清单 + expo prebuild --clean + pod install
  5. 统一策略对象:用 OrtExpoAutolinkPolicy 固化。
  6. CI 守护:断言模块在清单、平台覆盖、缺即失败。
  7. 不要手动 link 旧式:用 autolinking 正确声明,避免 RN 新版不认。

九、小结

[Mobile] unimodule.json makes Expo exclude onnxruntime-react-native from autolinking - NativeModules.Onnxruntime is null 的根因是:Expo 的 autolinking 配置(unimodule.json / 模块清单)没有包含 onnxruntime-react-native,预构建/链接阶段把它排除,原生模块不编进 iOS/Android app,运行时 NativeModules.Onnxruntimenull,任何调用崩溃。

最小修复是把该包登记进 Expo autolinking(包内声明 expo-module.config.js、宿主清单包含、expo prebuild --clean + pod install);结构性改进是用唯一的 OrtExpoAutolinkPolicy 固化必含模块;CI 用四组断言守护“模块必含、平台覆盖、缺即失败、齐备通过”。记住:RN 原生模块要进 Expo 的 autolinking 清单才会被链接,漏登记运行时就是 null。

配图

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐