在企业级云盘、盘搜和短视频项目中,大文件上传会把内存问题放到放大镜下。一个视频共创项目需要把本地 4K 原始视频(2GB 到 5GB)直接上传到服务器。早期做法是在 ArkTS 侧读取文件,转成 ArrayBuffer,再用传统 HTTP 请求体一次性发送。小文件正常,超过设备可用内存临界值后,App 会因 OOM 无声闪退。即使改为传文件路径让底层读取,只要底层网络引擎仍采用全量加载后发送,内存依然会被瞬间拉爆。根因是磁盘 I/O 读取速度快于网络 I/O 发送速度,没有背压机制时,数据会堆积在应用层或网络层缓冲区。
HarmonyOS 7.0(API 26)在 Remote Communication Kit(RCP)的 C-API 中新增 HMS_Rcp_SetRequestGetDataCallback(),把数据流转从应用层主动 Push 改成网络层按需 Pull。只有 TCP 发送窗口有空余时,底层引擎才触发回调,向应用层索要指定长度的数据块。这样既能解决大文件上传的内存积压,也能为手工构建 multipart/form-data 多片段有序表单提供精确控制面。该方案的目标是:上传 10GB 文件时,进程内存稳定在 2MB 以下,并复用连接池。
一、核心 API 与数据流
RCP C-API 在 API 26 提供细粒度请求生命周期控制。方案主要依赖三类能力:
1. 会话与连接池复用:HMS_Rcp_CreateSession 创建持久化会话,同一会话下发起的所有 Rcp_Request 共享底层连接池。大文件分片上传或伴随状态同步请求时,可复用已建立的 HTTP/2 或 HTTP/3 隧道,降低 TCP 握手和 TLS 协商延迟。
2. 数据拉取回调:HMS_Rcp_SetRequestGetDataCallback 是解决 OOM 的关键。接口原型如下:
- uint32_t HMS_Rcp_SetRequestGetDataCallback(
- Rcp_Request* request,
- Rcp_GetDataCallback callback,
- void* clientContext
- );
- typedef uint32_t (*Rcp_GetDataCallback)(
- void* clientContext,
- uint8_t* buffer,
- uint32_t length
- );
复制代码
当网络层发送缓冲区空闲时,RCP 引擎会在内部工作线程触发 Rcp_GetDataCallback。buffer 是引擎预分配的空闲内存指针,应用层把数据直接写入这里,实现用户态到网络层缓冲区的零拷贝,避免中间 malloc。length 是引擎当前能接收的最大字节数,通常取决于 MTU 和 TCP 拥塞窗口状态。回调返回值是实际写入字节数,返回 0 表示 EOF,引擎闭合 HTTP 请求体。
3. 请求终止与内存清理:HMS_Rcp_CancelRequest 可以安全地从底层中断 TCP 传输。取消后,绑定的 clientContext 不能在调用点立即释放,必须等到请求完成或失败回调触发后再释放,否则会产生野指针崩溃。
背压的关键在于:文件读取是被动触发的。弱网环境下 TCP 发送缓冲区积压,RCP 引擎停止触发 GetDataCallback,应用层状态机暂停,不再从磁盘读新数据。应用层与网络层速率由此解耦,内存不会无限制上涨。
二、Native 层流式 multipart 状态机
流式上传中构建 multipart/form-data 不能事先拼一个巨大字符串。必须把 Header、File Data、Tail 分段写入引擎缓冲区。该实践在 Native 层定义了一个上传任务上下文,用 clientContext 贯穿 RCP C-API,并用状态机表示当前发送阶段:
- enum class MultipartState {
- SENDING_HEADER,
- SENDING_FILE,
- SENDING_TAIL,
- FINISHED
- };
复制代码
上下文中包含 boundary、预先拼好的 headerStr 和 tailStr、headerSentOffset、tailSentOffset、std::ifstream fileStream、当前状态以及 Rcp_Request 句柄。文件以二进制方式打开。Header 中设置 Content-Disposition、filename、Content-Type,并声明 boundary;Tail 用于闭合 boundary。
核心回调 OnDataPullCallback 的逻辑是:把 clientContext 强转为上下文,做空指针和零长度防御;维护 totalWritten、remainingSpace、currentWritePtr;在 while 循环中按状态推进。
- static uint32_t OnDataPullCallback(void* clientContext, uint8_t* buffer, uint32_t length) {
- StreamUploadContext* ctx = static_cast<StreamUploadContext*>(clientContext);
- if (!ctx || !buffer || length == 0) return 0;
- uint32_t totalWritten = 0;
- uint32_t remainingSpace = length;
- uint8_t* currentWritePtr = buffer;
- while (remainingSpace > 0 && ctx->state != MultipartState::FINISHED) {
- if (ctx->state == MultipartState::SENDING_HEADER) {
- // 写入 headerStr 剩余内容,更新 headerSentOffset;写完后切到 SENDING_FILE
- } else if (ctx->state == MultipartState::SENDING_FILE) {
- ctx->fileStream.read(reinterpret_cast<char*>(currentWritePtr), remainingSpace);
- std::streamsize bytesRead = ctx->fileStream.gcount();
- // 更新指针、空间和 totalWritten;EOF 时切到 SENDING_TAIL
- } else if (ctx->state == MultipartState::SENDING_TAIL) {
- // 写入 tailStr 剩余内容,更新 tailSentOffset;完成后切到 FINISHED
- }
- }
- return totalWritten;
- }
复制代码
文件阶段直接使用 std::ifstream 读取到 RCP 提供的 buffer,减少中间拷贝。每次回调只消耗 length 规定的字节数,通常在 16KB 到 64KB 之间浮动。即使不切片、单请求全量流式推送,内存也维持在这个量级。
三、连接池装配与资源释放
有了拉取回调,还要把上下文装配进 Rcp_Session。实践中用一个全局 Session 复用连接池,配置最大空闲连接数为 10,保活时间为 60000ms。创建请求后设置 POST,并声明 Content-Type 为 multipart/form-data 及 boundary,再通过 HMS_Rcp_SetRequestGetDataCallback 挂载状态机回调。最后用 HMS_Rcp_SessionFetch 异步触发,避免阻塞主线程或 JS 线程。
请求完成回调是最终安全释放点。无论成功、失败还是取消,都在 HMS_Rcp_SessionFetch 的完成回调里销毁 Rcp_Request、Rcp_Response,并 delete 堆上的上下文。该设计把 clientContext 与堆内存强绑定,利用完成回调回收,规避 C++ 内存泄漏和野指针。
四、避坑指南
1. 不要在 GetDataCallback 中做重度阻塞操作。该回调运行在 RCP 内部网络工作线程,通常位于底层 Socket 事件驱动循环所在线程。虽然示例使用同步 fileStream.read,但在企业级高并发场景中,如果磁盘 I/O 瞬时卡顿,会挂起 RCP 事件循环,导致同一 Session 下所有并发网络请求假死。更稳妥的做法是应用层维护异步读取缓冲队列,用单独 I/O 线程预读 1MB 文件切片,回调触发时只做内存 memcpy。这样既保持低内存水位,又保证网络事件循环顺畅。
2. 取消上传时不要立刻释放上下文。调用 HMS_Rcp_CancelRequest 后,底层 C 引擎可能仍在做取消清理。如果调用点立即 delete ctx,后续扫尾动作访问野指针,可能触发致命段错误。资源销毁必须写在 HMS_Rcp_SessionFetch 的完成回调中,遵循生命周期契约。
3. 该方案还以传输单文件 2GB、网络限速 10Mbps 为基准,对内存峰值与响应速度做了多维对比,用于辅助 API 版本选型。核心结论是:拉取式流式上传把磁盘读取与 TCP 拥塞窗口绑定,在低内存占用下获得稳定吞吐。
五、总结
HMS_Rcp_SetRequestGetDataCallback() 看似只是新增一个底层 C 函数,实际代表系统处理海量吞吐时向按需计算和背压控制演进。通过 Native 层多片段表单状态机,上传过程中磁盘读取速度被网络层按需拉取节流,10GB 级文件上传也能保持平滑稳定,应用层内存开销被压到 KB 级别。后续如果要获取精准上传进度或结合断点续传,可以在这个状态机基础上引入 Range 头继续扩展。 |