You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo原生代码在模拟器、macOS正常,iOS TestFlight构建无响应

问题排查:Expo原生模块TestFlight环境无响应
  • 开发环境(模拟器、macOS构建)中,Swift编写并通过Objective-C绑定的sendPhoto原生模块可正常异步发送带图片的POST请求,服务器能接收数据。
  • 经EAS构建发布到TestFlight后,在iPhone 12 Pro、iPhone 14 Pro、iPhone 7(均为最新iOS版本)上无任何响应:既不崩溃,也不发送请求。
  • 添加了大量print和os_log调试语句,用于确认函数调用、异步任务执行、请求发送状态,但在iPhone日志存档和控制台中未看到任何调试日志,推测函数可能未被调用,但JavaScript调用时应用未崩溃。
  • 原生模块文件结构包含ReplateModule.swift、ReplateModule.m及桥接文件,未修改npx expo prebuild生成的AppDelegate等文件,怀疑EAS构建时缺少原生模块相关配置,但未找到对应文档。

核心代码

@objc public func sendPhoto(
    _ token: String,
    photoName: String,
    metadata: String,
    sender: String,
    resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) -> Void {
    
    let log = OSLog(subsystem: "it.myorg.manager", category: "ManagerDebugging")
    print("进入sendPhoto函数")
    os_log("进入sendPhoto函数", log: log, type: .default)
    DispatchQueue.global(qos: .userInitiated).async {
      print("进入异步队列")
      os_log("进入异步队列", log: log, type: .default)
      semaphore.wait()
      sentImagesCounter += 1
      semaphore.signal()
      print("图片计数器已递增")
      os_log("图片计数器已递增", log: log, type: .default)
      // 服务器请求地址
      guard let url = URL(string: "https://api.myendpoint.it/app/uploader/png") else {
        print("无效的端点URL")
        reject("0", "无效的端点URL", nil)
        os_log("创建URL对象失败", log: log, type: .default)
        return
      }
      
      // PNG图片数据
      guard let image = UIImage(named: photoName),
            let imageData = image.pngData() else {
        print("无法获取图片数据。")
        reject("0", "无法获取图片数据。", nil)
        os_log("获取图片数据失败", log: log, type: .default)
        return
      }
      
      let isLast = self.isLast(sender: sender)
      
      // 要发送的字符串参数
      let parameters: [String : Any] = [
        "metadata": metadata,
        "sender": sender,
        "last": isLast
      ]
      
      // 创建请求
      var request = URLRequest(url: url)
      request.httpMethod = "POST"
      print("已创建URL请求")
      os_log("已创建URL请求", log: log, type: .default)
      
      // 创建请求体
      let boundary = "Boundary-\(UUID().uuidString)"
      request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
      request.setValue(token, forHTTPHeaderField: "Authorization")
      
      var data = Data()
      
      // 向请求体添加图片数据
      data.append("--\(boundary)\r\n".data(using: .utf8)!)
      data.append("Content-Disposition: form-data; name=\"image\"; filename=\"\(photoName)\"\r\n".data(using: .utf8)!)
      data.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
      data.append(imageData)
      data.append("\r\n".data(using: .utf8)!)
      
      // 向请求体添加字符串参数
      for (key, value) in parameters {
        data.append("--\(boundary)\r\n".data(using: .utf8)!)
        data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
        data.append("\(value)\r\n".data(using: .utf8)!)
      }
      
      data.append("--\(boundary)--\r\n".data(using: .utf8)!)
      
      // 设置请求体
      request.httpBody = data
      
      // 执行请求
      let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
          os_log("发送请求失败", log: log, type: .default)
          print("请求错误: \(error)")
          return
        }
        
        if let data = data {
          // 处理服务器响应
          os_log("请求已发送", log: log, type: .default)
          print("服务器响应: \(String(data: data, encoding: .utf8) ?? "")")
        }
      }
      task.resume()
      print("已调用task.resume()")
      os_log("已调用task.resume()", log: log, type: .default)
      // 使用信号量处理竞态条件
      semaphore.wait()
      resolve(sentImagesCounter)
      if(isLast){
        sentImagesCounter = 0
        print("重置图片计数器")
        os_log("重置图片计数器", log: log, type: .default)
      }
      semaphore.signal()
      print("异步调用结束")
      os_log("异步调用结束", log: log, type: .default)
    }
  }
  
  @objc private func isLast(sender: String) -> Bool{
    semaphore.wait()
    let isLast = sentImagesCounter == requiredPhotos
    semaphore.signal()
    return isLast
  }

排查方向与解决方案

1. 确认原生模块在EAS构建中的注册状态

  • 检查ReplateModule.m是否正确声明模块和方法:确保RCT_EXPORT_MODULE()和RCT_EXPORT_METHOD(sendPhoto:photoName:metadata:sender:resolver:rejecter:)(参数名需与Swift函数完全匹配)存在且格式正确。
  • 验证桥接文件(如YourProject-Bridging-Header.h)是否正确导入Swift模块头文件,且Xcode项目设置中已配置桥接路径(EAS构建会读取Xcode配置,需确认prebuild后路径未被覆盖)。

2. 处理Release环境的日志输出限制

  • TestFlight属于Release环境,print语句会被系统优化移除,os_log需通过特定方式查看:
    • 用Xcode的Window > Devices and Simulators选中目标设备,点击View Device Logs,搜索subsystem(it.myorg.manager)和category(ManagerDebugging)。
    • 或在设备设置 > 隐私与安全性 > 分析与改进 > 分析数据中查找应用日志文件。
  • 临时开启Release日志:修改Xcode Build Settings,将OS_ACTIVITY_MODE设为default,EAS构建可在eas.json的build配置中添加xcodebuild参数实现。

3. 检查Release环境下的资源访问问题

  • UIImage(named: photoName)在Release环境可能加载失败:确保图片已添加到Assets.xcassets且EAS构建时被正确打包;或改用UIImage(contentsOfFile:)传入图片绝对路径,避免依赖资源目录加载机制。

4. 排查异步任务与信号量的潜在死锁

  • 确认semaphore初始化值为1(let semaphore = DispatchSemaphore(value: 1)),若初始值为0,异步队列中semaphore.wait()会导致线程阻塞,后续代码无法执行。
  • 检查isLast方法中信号量的使用,避免嵌套等待引发死锁。

5. 验证网络请求的权限与环境差异

  • 确认iOS设备已授予应用网络权限(在设置中查看应用的蜂窝网络权限)。
  • Release环境下URLSession请求可能被ATS拦截:检查服务器HTTPS证书有效性,若需特殊配置,在app.json的ios.infoPlist中添加NSAppTransportSecurity相关设置。

6. 检查EAS构建的配置完整性

  • 确保app.json/app.config.js中正确声明原生模块:Expo自定义模块需在expo.modules中添加模块信息,或确认npx expo prebuild已生成正确的模块注册代码。
  • 查看EAS构建日志,搜索原生模块编译、注册相关的警告或错误,比如模块未链接、编译失败等。

内容的提问来源于stack exchange,提问作者Filippo Cavallari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 21:07:15