
Expo Updates 本地开发指南源码接入、原生构建配置与 BSPatch 增量更新原理【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo本文基于 packages/expo-updates/DEVELOPMENT.md 展开面向需要在本地修改、调试 expo-updates 源码或希望深入理解其原生构建开关与二进制增量更新机制的开发者。你将掌握如何把本地仓库的 expo-updates 接入测试应用、iOS/Android 两侧的配置与调试技巧、如何在 Debug 构建中强制启用 expo-updates以及 BSPatch/BZip2 增量补丁在移动端的落地方式。一、为什么需要一份「开发模式」指南expo-updates 是 Expo 的开源 OTA 更新模块README它让应用能够从远端服务器拉取并加载新版本的 JS Bundle 与资源。日常使用者只需通过expo-updates的公开 API 与app.json配置即可完成接入但当你需要修改模块自身源码、调试更新加载流程、或者为 expo-updates 贡献代码时就会面临一组独有的问题如何让测试应用使用本地工作副本而不是 npm 上发布的版本为什么默认情况下 Debug 构建不启用 expo-updates如何在 Debug 模式下打断点、单步调试更新流程增量补丁BSPatch BZip2在 iOS 与 Android 上是如何被编译和链接的本文档就是 Expo 官方为回答这些问题而写的内部开发手册配合仓库源码可以还原完整的底层行为。二、环境准备选择一个合适的宿主应用原文档明确建议虽然可以在 Expo Go 的上下文中开发 expo-updates但通常更简单的方式是使用一个裸应用bare app。原因是 Expo Go 的更新配置是动态的、可运行多个 scope 的更新这一点在 UpdatesConfiguration.kt 的类注释中有说明而在裸应用中配置是编译进构建产物的行为更可控、更贴近真实发布环境。如果你运行expo init并选择任意 bare 模板模板中已经预装了最新版本的 expo-updates 并完成了基础配置可以直接在此基础上替换为本地源码。三、把本地 expo-updates 接入测试应用将本地源码接入测试应用有三种方式按推荐程度与适用场景排列1.yarn link最简单但有局限如果你在 JS 中没有导入 expo-updates 的任何方法yarn link是最直接的方案。但原文档特别警告Metro在写作本文档时不支持符号链接symlink。因此一旦你的应用需要通过 JS 调用 Updates 模块的方法例如Updates.checkForUpdateAsync()yarn link就无法满足需求——Metro 解析不到符号链接指向的真实文件。2. 使用file:协议替换依赖在package.json中把 expo-updates 依赖替换为本地路径{ dependencies: { expo-updates: file:/path/to/expo/expo/packages/expo-updates } }注意每次修改 expo-updates 源码后都需要重新运行yarn --force让 yarn 把最新源码复制进node_modules。3. 手动拷贝免等待如果不想每次改动都等 yarn 执行可以手动把 expo-updates 复制到node_modulescp -r /path/to/expo/expo/packages/expo-updates node_modules/expo-updates这是开发迭代最快的方式代价是需要自己维护拷贝的一致性。原文档也欢迎社区补充其他接入方式Feel free to add other options here!可见官方将此视为一个开放话题。四、原生配置URL 与 Runtime Versionexpo-updates 的原生配置在两端各有载体iOSExpo.plistAndroidAndroidManifest.xml多数选项文档强调两个必须正确设置的配置项配置项iOSExpo.plist keyAndroidmeta-data key更新服务 URLEXUpdatesURLexpo.modules.updates.EXPO_UPDATE_URLRuntime VersionEXUpdatesRuntimeVersionexpo.modules.updates.EXPO_RUNTIME_VERSION从源码看这两项不仅是配置还是配置合法性的核心判据。在 UpdatesConfiguration.kt 中getUpdatesConfigurationValidationResult会依次校验enabled是否被显式关闭INVALID_NOT_ENABLED更新 URL 是否存在INVALID_MISSING_URLRuntime Version 是否为空INVALID_MISSING_RUNTIME_VERSION。只有三者通过才返回VALID。Android 侧读取expo.modules.updates.EXPO_RUNTIME_VERSION时还会做string:前缀剥离见 UpdatesConfiguration.kt并支持一个特殊哨兵值file:fingerprint——它会打开 assets 中的fingerprint文件、把内容当作 runtime version 读取。除这两个必选项外从 UpdatesConfiguration.kt 的字段可以看出完整配置面例如launchWaitMsexpo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS默认 0启动等待更新的毫秒数checkOnLaunchexpo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH默认ALWAYS取值对应 JS 侧的UpdatesCheckAutomaticallyValueUpdates.types.ts——ON_LOAD/ON_ERROR_RECOVERY/WIFI_ONLY/NEVER非法值会被降级并记录Log.e警告requestHeaders、codeSigningCertificate、enableBsdiffPatchSupport默认true等。也就是说原文档强调的「URL Runtime Version」只是最小可用集源码中还隐藏着完整的可调参数表开发调试时同样可以在Expo.plist/AndroidManifest.xml中覆盖它们。忽略内嵌更新Ignore Embedded Update当你用 expo-updates 测试自己开发的更新服务器时通常会遇到一个问题每次新构建都会生成一个带有新创建时间creation time的内嵌 bundle导致应用拒绝加载之前发布过的远端更新因为选择策略要求远端更新的创建时间必须比内嵌 bundle 更新。解决办法是让 expo-updates 忽略内嵌更新、强制走远端iOSEXUpdatesHasEmbeddedUpdate设为falseAndroidexpo.modules.updates.HAS_EMBEDDED_UPDATE设为false源码层面Android 的读取逻辑位于 UpdatesConfiguration.ktgetOriginalHasEmbeddedUpdate优先读取 overrideMap其次读 AndroidManifest 的 meta-data兜底默认true。同时getHasEmbeddedUpdate还处理了「禁用了防砖anti-bricking措施且存在运行时 override」时强制返回false的路径。iOS 侧的对应实现可参考 EmbeddedAppLoader.swift 中对originalHasEmbeddedUpdate的判断——当该值为false时直接返回空不加载内嵌清单。另外在 Android 上HAS_EMBEDDED_UPDATE会被写入构建元数据BuildData.kt用于后续构建数据一致性校验相关迁移逻辑有单测覆盖BuildDataTest.kt。附加请求头Additional Headers如果需要给 manifest 请求附加自定义请求头有两种渠道iOS在Expo.plist中以EXUpdatesRequestHeaders为 key 存放一个字典Android目前无法在AndroidManifest.xml中配置必须调用UpdatesController.overrideConfiguration(Context context, MapString, Object configuration)位于 UpdatesController.kt在MainApplication.java中、UpdatesController.initialize()之前调用// MainApplication.java UpdatesController.overrideConfiguration( this, Map.of( requestHeaders, Map.of(expo-channel-name, main) ) );这个方法会先做配置校验只有getUpdatesConfigurationValidationResult返回VALID才会真正应用 override否则只记录警告logger.warn(Failed to overrideConfiguration: invalid configuration: ...)并在已初始化后调用会直接抛AssertionError——因此调用时机必须在initialize()之前。iOS 侧也有对应的静态方法AppController.overrideConfiguration(configuration:)AppController.swift无法从 Expo.plist 加载配置时会输出警告。值得一提的是安全性Android 的 override 请求头并不是无条件接受的。isValidRequestHeadersOverrideUpdatesConfiguration.kt会校验——禁止覆盖Host头防止恶意请求重写且所有 override 的 header key 必须已经存在于内嵌配置的原始 header 集合中否则回退到内嵌配置。五、构建Release 构建才默认启用默认行为是裸应用中 expo-updates 只在 release 构建里启用debug 构建从本地 Metro server 加载。这是为了保证开发迭代速度——每次改 JS 都能热更新而不必与远端更新逻辑纠缠。iOS在 Xcode 中构建 Release按照以下菜单路径操作Xcode → Product → Scheme → Edit Scheme → 把 Run 配置从 Debug 改为 Release然后点击主窗口的 Run 按钮即可。Android通过 Gradle 构建 Release在项目根目录执行react-native run-android --variant Release构建开关背后是expo-updates-gradle-plugin。在 ExpoUpdatesPlugin.kt 中可以看到插件会读取环境变量EX_UPDATES_NATIVE_DEBUG 1或 Gradle 属性EX_UPDATES_NATIVE_DEBUG true并在启用时禁用所有react.debuggableVariants——这正是「原生调试模式下 app 从 bundle 加载而非 Metro」的实现关键之一。六、在 Debug 构建中启用 expo-updates有时你希望在 debug 构建中也启用 expo-updates——典型场景是想在更新加载流程中打断点、单步调试。步骤分为两步第一步忽略内嵌更新先按照上文「忽略内嵌更新」一节配置好EXUpdatesHasEmbeddedUpdate/expo.modules.updates.HAS_EMBEDDED_UPDATE为false。这样 debug 构建就能强制从远端拉取更新而不是被内嵌 bundle 挡住。第二步设置环境变量export EX_UPDATES_NATIVE_DEBUG1这个变量的作用范围是原生构建阶段两端各有实现Androidandroid/build.gradle 中的getBoolStringFromPropOrEnv(EX_UPDATES_NATIVE_DEBUG, false)会依次检查gradle.properties属性与系统环境变量环境变量优先级更高并把结果写入BuildConfig.EX_UPDATES_NATIVE_DEBUGbuild.gradle。运行时 UpdatesPackage.kt 中getDelayLoadAppHandler会判断!useDeveloperSupport || isUsingNativeDebug来决定是否延迟加载 App——即原生调试模式下即使useDeveloperSupport为 true 也走 expo-updates 的启动流程。iOSEXUpdates.podspec 中环境变量EX_UPDATES_NATIVE_DEBUG优先于Podfile.properties.json中的updatesNativeDebug当值为1时向 Debug 配置注入-DEX_UPDATES_NATIVE_DEBUG1C 标志与-DEX_UPDATES_NATIVE_DEBUGSwift 标志见 podspec。Swift 侧由UpdatesUtils.isNativeDebuggingEnabled()UpdatesUtils.swift通过#if EX_UPDATES_NATIVE_DEBUG编译期判断返回。iOS 额外的两个步骤在 iOS 上还需要额外处理两件事1. 强制打包 JS bundle。修改 Xcode 工程文件把SKIP_BUNDLING替换为FORCE_BUNDLING使应用在 debug 和 release 两种构建中都把 JS 打进 Appsed -i s/SKIP_BUNDLING/FORCE_BUNDLING/g; ios/project name.xcodeproj/project.pbxproj实际上当EX_UPDATES_NATIVE_DEBUG1时podspec 的脚本阶段会自动设置export FORCE_BUNDLING1EXUpdates.podspec这解释了为什么原生调试模式必须在构建时生效——bundle 是否内嵌是由构建脚本决定的。2. 重新安装 CocoaPodsnpx pod-install在项目顶层目录执行。完成以上步骤后你就能构建出一个行为如同 release 构建、但没有内嵌更新的 debug 版应用。这正是调试更新流程的理想环境可以安心打断点同时由远端服务器驱动更新加载。这套配置同样被官方 e2e 测试采用——e2e/README.md 的 Updates API 测试项目设置中同样导出了EX_UPDATES_NATIVE_DEBUG1并配合expo-channel-namemain作为 EAS 更新请求头来手动验证 Updates API 功能。七、BSPatch 与 BZip2二进制增量补丁expo-updates 内置了对资源文件assets应用二进制补丁的能力用于实现增量更新——只传输补丁而不是整个资源从而显著减少下载量。BSPatch 的来源与改造BSPatch 代码取自 FreeBSD 版本的 bspatch.cExpo 对其做了修改使其在移动环境中安全使用、不会导致用户应用崩溃例如引入严格的错误处理与资源边界检查。在仓库中的落地位置AndroidJNI 桥接位于 BSPatch.happlyPatch(oldFilePath, newFilePath, patchFilePath)与 BSPatchModule.cppC 源码作为 CMake 构建的一部分编译。iOSSwift 封装 BSPatch.swift 调用 Objective-C 桥接 EXUpdatesBSPatch.m后者以bspatch_main(4, argv)方式调用 C 实现并把非零返回值转换为BSPatchError.failed抛出。实际应用点位于 FileDownloader.swift 的资源下载流程中。BZip2 的链接方式差异BSPatch 依赖 BZip2 解压iOS直接链接系统 BZip2 库。podspec 中s.libraries bz2EXUpdates.podspec无需随包携带源码。Android仓库内包含一份 BZip2 源码副本位于 packages/expo-updates/android/src/main/cpp/third-party/bzip2包含bzlib.c、decompress.c、huffman.c、blocksort.c、compress.c等随原生代码一起编译。需要特别说明的是这份 BZip2 副本只包含处理解压decompression的部分因为 expo-updates 只负责应用补丁不需要压缩能力。如果未来需要升级 BZip2官方流程是下载新的 bzip2 源码替换packages/expo-updates/android/src/main/cpp/third-party/bzip2中的必要文件后重新构建。在 JS 侧补丁支持由enableBsdiffPatchSupport配置控制默认true见 UpdatesConfiguration.kt可通过 AndroidManifest meta-dataexpo.modules.updates.ENABLE_BSDIFF_PATCH_SUPPORT或 iOS Expo.plist 关闭。八、故障排查Troubleshooting如果 expo-updates 没有加载你期望的更新按以下两个方向排查内嵌 bundle 的创建时间正在尝试加载的更新其 creation time 必须晚于内嵌 bundle否则会被选择策略拒绝或者你已按上文配置忽略内嵌 bundle。这是最常见的「更新不生效」原因——每次新构建都会产生新的内嵌 bundle覆盖掉之前发布的更新。SDK / Runtime Version 不匹配Expo.plistiOS或AndroidManifest.xmlAndroid中配置的 SDK 或 runtime version 必须与服务器 manifest 中的一致。这正是上一节讲到的配置校验逻辑getUpdatesConfigurationValidationResult会在启动期把关的环节。原文档标注了TODO: add more common scenarios here as they come up——即官方计划随着社区反馈持续补充常见问题这也说明排查此类问题时的核心思路始终是先确认「远端更新是否满足选择策略」再确认「配置是否通过原生校验」。九、总结本地开发 expo-updates 推荐使用bare app file:依赖或手动拷贝避免 Metro 不支持 symlink 的坑两端配置核心是URL 与 Runtime Version它们也是配置校验的硬性条件测试自有服务器时务必忽略内嵌更新需要自定义 manifest 请求头时 iOS 用Expo.plist、Android 用overrideConfiguration默认只有Release 构建启用 expo-updates通过EX_UPDATES_NATIVE_DEBUG1Android 还可经 Gradle 属性 iOS 的FORCE_BUNDLING与pod-install可以在 Debug 构建中打断点调试完整更新流程增量更新基于FreeBSD BSPatch BZip2iOS 链接系统bz2库Android 编译仓库内的第三方源码副本仅解压部分升级时替换 third-party/bzip2 目录即可。这套开发手册配合源码阅读建议从 UpdatesConfiguration.kt 与 EXUpdates.podspec 入手能让你从「会用 expo-updates」进阶到「能调试、能改、能贡献」。【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考