原生回调:进程单例解决入口,不解决结果属于谁

自动关联目录:原生回调:进程单例解决入口,不解决结果属于谁

核心判断:JNI、NAPI 或 Objective-C 回调需要一个稳定的进程入口,但入口对象长期存活不代表返回值仍属于当前页面、当前 World 或最近一次请求。跨语言异步必须有 RequestId 或严格的单飞协议。

一、业务冲突:系统选择器由平台拥有,调用方却随页面和 World 生灭

图片、视频或脚本文件选择会离开 UE 控制流,结果可能在授权、前后台切换或系统界面关闭后才返回。原生层无法天然持有发起请求的 Widget、脚本实例或 World,因此工程用 Root UObject 作为各平台统一回调入口,再把数据派发回 Game Thread。

二、实际实现:跨线程和 JNI 内存处理正确,请求关联仍是全局槽位

原生回调先把平台字节复制到 UE 数组,再释放平台缓冲;需要时切回 Game Thread;完成后执行并清空对应动态委托。这些做法正确地解决了缓冲区寿命、线程亲和性和重复回调。

但路由状态只有一个进程级 bool,以及图片、视频、权限、脚本各一个可覆盖的 Delegate 槽。回调通过 GetInstance() 找当前全局对象,没有 RequestId、调用方身份或 World Generation。由控制流可以直接推出:

  • 同类请求在前一次返回前再次绑定,会覆盖旧调用方。
  • 旧请求晚到时,可能执行新请求刚写入的 Delegate。
  • 全局 bool 只能说明“某类读取大概在进行”,不能区分是哪一笔、哪种媒体或哪一代页面。
  • 回调时重新获取单例,可能在原业务域结束后重新创建入口并吞掉结果,或把结果送给后来建立的域。

三、目标设计:桥接层稳定,请求对象短命

struct NativeRequest {
    RequestId id;
    RequestKind kind;
    TWeakObjectPtr<UObject> requester;
    SessionGeneration session;
    Deadline deadline;
    RequestState state;
    Completion completion;
};

RequestId OpenPicker(RequestSpec spec, UObject* requester) {
    check(IsInGameThread());
    RequestId id = Requests.Insert(spec, requester, CurrentSession());
    NativeBridge.Open(spec, id);       // 原生层原样回传 id
    return id;
}

void OnNativeResult(RequestId id, NativePayload payload) {
    GameThread::Enqueue([id, payload = MoveTemp(payload)] {
        auto* r = Requests.FindPending(id);
        if (!r || !r->requester.IsValid() || !Sessions.IsCurrent(r->session))
            return RecordLateResult(id);
        r->state = RequestState::Completed;
        MoveTemp(r->completion)(MoveTemp(payload));
        Requests.Remove(id);
    });
}

若系统 API 天然只允许一个选择器,最小方案是显式单飞:第二次请求直接返回 Busy,而不是覆盖 Delegate。取消后也不能立即复用槽位,除非平台确认旧请求已结束;否则旧回调仍会被误认成新结果。无法让系统 API 携带 ID 时,由原生桥保存当前 Generation,并在回调中一并返回。

进程单例只保留无业务状态的 Router;Pending Request 归 GameInstance 或更短的会话宿主。页面销毁只撤销自己的完成权,不需要销毁平台桥。

四、取舍与验证

Request Table 增加了 ID 透传和超时清理;严格单飞会让第二个调用方等待或失败。两者都比静默覆盖更可控。图片和视频字节还应在入表前检查类型、大小和解码上限,但那属于载荷安全,不应混进请求关联判断。

测试连续双击打开、A 取消后 B 打开、权限回调晚于页面销毁、应用切后台、World Travel、平台重复回调和结果乱序。指标记录 InFlight 数、Busy 拒绝、取消、超时、Late Result、错误路由和从原生返回到 Game Thread 提交的延迟。正确标准是:每个结果只能结束创建它的那一笔请求,绝不能借用“当前 Delegate”。