news 2026/4/30 0:46:55

iOS开发 实习产出(给我自己看的 笔记而已)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iOS开发 实习产出(给我自己看的 笔记而已)

app总览

这个 app 是一个通过多设备协同进行 AR 数据采集 / 录制 / 上传的 iOS 应用,主界面由 4 个一级 Tab 组成,背后由一组领域模块支撑。

一、主界面 4 个板块(一级 Tab)

enum Tab {

case prepare, record, upload, profile

}

Tab入口 View作用主要目录

prepare

PrepareV2

录制前准备:配对蓝牙夹爪、选择 UMI 版本、多机同步等

src/prepare_v2/

record

RecordView

AR + 相机录制主界面

src/record/,src/record_v2/

upload

UploadQueueView

录制产物排队上传(S3 / coscene)

src/upload/,src/upload_v2/,src/s3/,src/coscene/

profile

ProfileView

用户 / 设置

src/user/,src/settings_ui/

二、顶层 Dependencies(各 Tab 共享的核心 Manager)

final class Dependencies: ObservableObject {

let cfg: Utils.ObsRw<UserConfig>

let peers: PeersManager

let ble: BluetoothEncoderManager

let up: UploadManager

let ar: ArManagerV1

let inference: InferenceManager

  • cfg:用户配置(src/config/,src/config_v2/
  • peers:多设备同步 / 发现(src/peers/,src/nw/
  • ble:蓝牙夹爪编码器(src/bluetooth/,src/gripper/
  • up:上传队列(src/upload/,src/upload_v2/
  • ar:ARKit 姿态 / 轨迹(src/ar/,src/ar_v2/,src/ar_v3/,src/arkit/
  • inference:端侧推理(src/inference/

三、按业务领域划分的支撑模块

大致可以分成几层:

  1. AR & 传感:ar/,ar_v2/,ar_v3/,arkit/,calib/(标定),math/,opencv/
  2. 外设 / 硬件:bluetooth/,gripper/(UMI2/UMI3 夹爪),usb/,device/,robot/
  3. 多机协同:peers/,nw/(网络层),pubsub/,stream/
  4. 录制流水线:record/,record_v2/,camera_ui/,image/,img/,quality/,task/
  5. 上传 / 云端:upload/,upload_v2/,s3/,coscene/,totp/
  6. 推理与实验:inference/,expr/,stress_test/,nedf2/
  7. 配置 & 用户:config/,config_v2/,user/,settings_ui/
  8. UI / 基础设施:ui/,ui_kit/,swift_ui/,foundation/,utils/,log/,thread/,py_like/,tools/

四、一眼速览(业务主线)

一条完整的使用链路,基本就是 4 个 Tab 串起来的:

prepare(连夹爪 + 选 UMI3 前缀 + 多机同步)→record(AR 轨迹 + 拍摄 + 夹爪编码)→upload(本地队列上传到 S3/coscene)→profile(账号 / 设置)。

从录制到落盘的流程

下面从按下"开始"到成为本地"一条可上传的记录"为止,完整讲一遍录制数据在 app 内的落盘流程。为了看得清,我按"谁触发 → 在哪里 → 产生什么"分阶段走。


一、按下"开始":生成本次录制的"身份证"

入口在录制页面的按钮,它并不直接开始写文件,而是做一串前置校验,再把"开始命令"既广播给自己、也广播给同组的其他设备:

  • 必须所有设备都处于空闲且健康状态。
  • 多机同步时只允许右腕那台触发。
  • 如果是 UMI3 的腕部机位,必须先在首页填过合法的硬件版本前缀(300-310,且与当前相机位置一致),否则弹窗拦下。

校验通过后,主机做两件事作为"本次协同录制的身份锚":

  • 生成一个全局唯一的协同标识(本次录制所有设备共用一个),保证多机产物能合并成同一条记录。
  • 生成一个以当前时间命名的输出目录名,三台机都用同一个名字。

然后把"开始"这一条命令带上目录名、协同标识、起始 UNIX 时间一起广播出去。


二、各端启动:在文件系统里开出这一"录制坑位"

每台设备的录制总控收到开始命令后(在专门的帧队列里)执行:

  • Documents/<时间戳>/下创建本次录制的独立目录。
  • 给视频写入器指定recording.mp4作为输出路径,并按当前屏幕方向设置画面 transform、按夹爪代际选不同比特率(老一代 10 Mbps,新一代 2.5 Mbps)。
  • 给深度图写入器指定同一目录。
  • 把本次录制的"先验元数据"组装成一个起始快照放进内存中的"录制会话",包含:协同标识、用户指纹、多机模式、任务标签与描述、起始时间、起始方向、相机帧率、相对于主机的初始位姿等。
  • 状态切到"录制中",并广播模式变更事件。

注意:真正的帧数据还没有落盘,只是打开了文件句柄和建立了内存容器。


三、录制中:边跑 AR 边把三路数据往外写

ARKit 每出一帧,都会走到"录制中"的分支。每一帧做三件事:

  1. 帧级问题检测:是否离物体太远、跟踪是否失稳、蓝牙夹爪是否掉线、镜头是否装对……这些问题以"第 N 帧出现了哪些问题"的形式暂存在内存里。
  2. 把标量数据追加到内存:每帧追加一条记录 = 时间戳 + ARKit 位姿 + 夹爪宽度 + 夹爪角度 + 其它辅助量。还会把第一帧单独保留(用于最后生成元数据时填分辨率、深度图尺寸等)。
  3. 把像素/深度数据真正写入文件:
    • 视频写入器把当前画面缩放后写进recording.mp4
    • 深度图写入器把场景深度追加进depth_images.tar(如果某一帧 ARKit 没给深度图,会复用上一帧,避免张数对不齐)。

所以录制期间磁盘上持续在增长的是 mp4 和深度图包;其它每帧标量还在内存里累积,等结束时再一次性写入。


四、按下"结束":先停写、再"定稿"

"结束"按钮也走同样的广播路径。录制总控收到结束命令后:

  • 状态切到"结束中",广播模式变更。
  • 调用视频写入器和深度图写入器的 finish + 等待完成,确保 mp4/深度包都完整落盘。
  • 交给"产物定稿器"进行最后的收尾。

五、定稿:把一次录制变成一个"可归档的目录"

这是整个流程最关键的一步,都发生在"产物定稿器"里:

5.1 跑一遍离线质量检查

在所有帧都录完后,对采到的数据再跑一次状态机质检,把这些"事后才能发现的问题"合并到之前的帧级问题里,得到最终的问题字典。

5.2 帧数一致性校验

这一步会比较三者:内存里记录的帧计数 ↔ 每帧标量时间戳的条数 ↔ 实际写进 mp4 的帧数 ↔ 深度图的帧数。

  • 如果"帧计数"和"时间戳条数"对不上,直接判为严重存储异常,广播一个"存储异常"事件,本次录制作废。
  • 视频/深度图允许一定偏移(随时长变化的容忍值)。超过就判异常,作废。
  • 如果三者都对得上,才继续往下。

5.3 把内存里的帧数据序列化到磁盘

把前面累积的时间戳 / 位姿 / 夹爪宽度 / 夹爪角度 / 每帧问题 / 附加字段,用 BSON 编码成frame_data.bson写进录制目录。

5.4 腕部机位额外复制 3 张 ROI 掩膜

如果这台是左腕或右腕,会把夹爪宽度检测用到的 3 张 ROI 掩膜图(roi_mask_0/1/2.png)从Documents/gripper/拷贝到录制目录里,用于事后离线重现夹爪宽度的计算。

5.5 生成校验和 + 元数据 JSON

  • 对三个必需文件recording.mp4frame_data.bsondepth_images.tar各算一个 sha256。
  • 生成metadata.json,里面大致有:代码版本、本机录制的唯一 id、协同标识、设备代际、用户 id、用户指纹、多机模式、相机位置(头/左腕/右腕)、硬件版本号、任务标签/描述/执行 id、起始时间(ISO8601 + UNIX)、时长、帧数、帧率、视频宽高、深度图宽高/量程/位深/帧数、设备机型/系统版本/设备 id、起始方向、本机相对于主机的初始位姿、帧级问题、三个文件的 sha256 列表。

到这一步,磁盘上该目录的形态就稳定了,长这样:

Documents/

20260429_121855_xxxx/

recording.mp4

frame_data.bson

depth_images.tar

metadata.json

roi_mask_0.png # 仅腕机位

roi_mask_1.png

roi_mask_2.png


六、进入上传队列:成为 app 里的"一条记录"

定稿器最后一步会把这个目录交给上传管理器。上传管理器做的事:

  • 再做一次保护:目录不能是空的,且必要文件齐全,否则拒收(这时候直接失败返回)。
  • 以本机录制 id 作为主键,抽取元数据里的关键字段(文件夹名、起始时间、任务标签、任务执行 id、用户指纹、相机位置、帧数、时长、本次的问题、总字节数)组装成一条队列条目,初始上传状态为"待上传"。
  • 在主线程做原子 append:
    • 如果队列还在从磁盘加载中,就挂到"加载完成后再执行"的延迟队列里(避免先 append 又被覆盖)。
    • 如果已有同名目录,就拒绝重复入队。
    • 否则追加到内存队列并立刻把整个队列序列化回磁盘(让 app 被杀掉后也不会丢记录)。
  • 定稿成功后广播一个"录制已保存"事件,携带目录、时间、元数据。底部 Tab 栏的"上传"角标数、上传页面的列表等都是订阅到这个事件之后刷新的。

七、异常路径:什么情况下这条记录会消失

  • 结束过程中任一步失败(比如第一帧始终没到、元数据生成失败、sha256 失败、一致性校验失败、写frame_data.bson失败、写metadata.json失败、入队失败):finish 里的 defer 会把整个录制目录从磁盘上删掉,并广播"结束失败"事件,状态回到空闲。
  • 用户主动取消:同样是在空闲化之前,把正在写的视频 / 深度图取消,并把目录整个删掉。
  • 帧数严重不一致:在发布"存储异常"事件后,本次记录不会被加进上传队列,也会随失败路径被删除。

八、一张"数据去向"总结

来源落点时机

每帧画面(缩放后)

recording.mp4

录制中,逐帧写

每帧场景深度

depth_images.tar

录制中,逐帧写(缺帧时补上一张)

每帧时间戳 / ARKit 位姿 / 夹爪宽度 / 夹爪角度 / 每帧问题

frame_data.bson

结束时一次性写

前置信息(协同标识、任务、起始时间、起始方向)+ 首帧分辨率 + 事后统计(时长、总帧数、sha256)+ 设备信息 + 相机位置 + 硬件版本

metadata.json

结束时一次性写

夹爪 ROI 掩膜(仅腕机位)

roi_mask_0/1/2.png

结束时从Documents/gripper/复制

目录元信息(状态=待上传、问题摘要、字节数等)

磁盘上的上传队列(序列化)

入队成功后立即持久化

一句话概括:录制期间 mp4 和深度图是"边拍边写",其余结构化数据全部攒在内存里;结束时由"产物定稿器"做一致性校验、一次性把内存数据写成 BSON + JSON + 校验和,最后挂进持久化的上传队列。入队的那一瞬间,它才算是 app 里一条"可被看到、可被上传"的录制记录。

由app上传到平台的流程

一、上传被触发的几种方式

录制结束入队后,app 不会自动上传。真正发起上传的入口只有以下几种:

  1. 用户主动触发:在"上传"Tab 选中若干条记录,点上传按钮(也可以"上传所有待上传")。
  2. 协同录制时被广播带起来:协同录制中,主机一旦在云端"建好/复用了这条录制记录",会通过同组广播把"哪个录制对应哪个记录名"告诉所有从机;从机收到广播后,如果自己队列里有同名待上传条目,会自动开跑(避免每台从机各自再去云端建一条同名的空记录)。
  3. app 启动时的孤儿自愈:上次 app 在上传中途被系统杀掉,磁盘上残留一些标记为"上传中"的条目;启动后会被一律重置回"待上传",等用户再次手动上传。
  4. 失败重试:失败的条目会被保留在队列里,用户也可以选中、点"重置为待上传"再点上传。

二、调度:把"选中的几条"变成"一个串行任务"

入口接收到一组要上传的文件后,做这几件事:

  • 队列还在从磁盘加载时直接挂起。app 启动后队列是异步加载的,加载没完前主动避让;调用被排到一个"加载完后再执行"的延迟队列里。
  • 去重入队:把每条记录用文件夹名做去重,只追加未在队列里的。
  • 判断是否已经有"上传循环"在跑:
    • 如果已经在跑,只把"待跑总数"加上去,复用现有任务;
    • 否则起一个新的后台异步任务,开始 while 循环。

后台循环每次做一件事:

  • 从队列取下一条 → 把它的状态置为"上传中" → 跑这条文件的上传 → 拿到结果(成功 / 失败 / 取消)→ 更新这条状态 → 累积成功/失败计数和已传字节数 → 把进度推给 UI → 进入下一条。

整个 app 同一时间只跑一条(串行),但 UI 会持续看到全局进度(已完成几条/共几条、已传字节/总字节、当前文件名、当前文件已传/总大小)。

为了避免高带宽下 URLSession 一秒上百次回调把主线程刷爆,进度回调是在后台先累加 / 校正,再每"一帧"派一次到主线程更新(同一帧里所有进度字段一致更新)。


三、单条上传:先做"通用准备"

不管最终上传到哪个云,每条记录都先在本地经过同一套准备:

3.1 路径与元数据校验

  • 找到Documents/<时间戳>/这个录制目录,确认它真的存在、确实是目录。
  • 必须有metadata.json,并且能正常解码成元数据对象;解码失败直接报错(带可读路径信息)。

3.2 选打包工作目录

按当前选择的"上传目标"决定.tar.gz放哪儿:

  • DM3 平台:放在Documents/dm3UploadResume/<目录键>/这种持久化 staging 子目录里。这样即使 app 重启 / 进程被杀,下次进来还能续传。
  • 其他三种目标:放在系统临时目录里,跑完即扔。

"目录键"是把 (录制目录名, 协同标识, 相机位置) 三段做百分号编码、用下划线拼起来;这样下次 app 启动后能反向解析出来,方便垃圾回收。

3.3 打包成.tar.gz

  • 先用 tar 打成单文件.tar
  • 然后做 gzip:这里用了流式压缩,每次只在内存里持有 1 MB 数据。如果用最直观的"先把整个 tar 读进内存再压",几 GB 录制立刻把 jetsam 触发然后被系统杀掉。具体做法是:自己手写 RFC1952 的 gzip 头/尾(10 字节 header + 8 字节 CRC32 + 输入字节数),中间走 Apple 系统库的 raw deflate,边读边写边算 CRC。
  • DM3 续传场景下:如果 staging 子目录里上次的.tar.gz还在且非空,直接复用,不重新打,可以省一次几十秒到几分钟的压缩。

3.4 失败/取消时的清理约定

整个uploadFile用一个 defer 兜底:

  • 非 DM3 目标:.tar.tar.gz都在临时目录,无脑全删。
  • DM3 目标:中间的.tar总是删;.tar.gz留在 staging 给下次续传用;只有当用户主动取消时才把 staging 整个清掉(因为续传也没意义了)。
  • DM3 走到网络/服务端错误时,故意不清理 staging,下次进来 SDK 会用里面的 checkpoint 自动续传。

SDK = 软件开发工具包

一句话:别人把现成的功能封装好,打包给你,你直接调用,不用自己从零写底层代码

里面一般包含:

代码库、接口 示例 demo 文档、配置文件


四、四种"上传目标"分别怎么把 .tar.gz 推走

4.1 上传到刻行(默认路径)

这是 app 主要的上传目标,流程最长:

  1. 算 sha256:服务端要校验完整性。
  2. 解析"业务记录名":刻行平台上一次录制等于一条"记录",三机协同时三段视频要挂在同一条记录下。优先级:
    1. 调度层透传过来的(一般是从机收到广播带的);
    2. 本机内存缓存(之前收到过广播);
    3. 自己是主机:调"建立或复用记录"接口,建好后立即广播给所有从机;
    4. 自己是从机但有同步对端:等主机广播最多 15 秒,超时降级自建(保住可用性)。
  3. 拼好"标签"和"自定义字段":任务标签做成一个 label;采集人 = 用户指纹、任务描述 = 录制时填的描述;如果这次录制中检测到"距离过远 / 夹爪量程过小 / 夹爪宽度缺失 / 跟踪特征不足"这种轻问题,给打"黄色 SLAM 异常"标;其他严重问题打"红色 SLAM 异常"标。
  4. 拿一次性预签名 URL:调"生成上传 URL"接口,传文件名、大小、sha256,得到一个有限期的 PUT 直传地址。
  5. 直传 .tar.gz:用 URLSession 做 PUT,带 progress delegate 实时回报已传/总数;同时起一个每 5 秒打一行心跳日志的并行任务,方便排查"卡住了到底卡在哪",PUT 完成时心跳自动取消。
  6. 业务登记:上传完文件还不算结束。必须再调"同步文件信息"接口把 (任务执行 id、文件名、大小、时长) 登记到业务侧。这一步要求登录态有效且不是访客;如果没登录或是访客,文件其实已经在桶里了,但本地状态会标失败,提示用户"已上传未登记"。

只有走完这一整串(含登记成功)这条记录才会被标为"已完成"。

4.2 上传到 DM3(直接打到阿里云 OSS)

走的是阿里云 SDK 的"分片可断点续传"模式,封装比较厚但行为很清晰:

  1. 拉 STS 凭证:先用登录态 token 找后端要一次性的 endpoint / bucket / region / 路径前缀 + (ak / sk / securityToken / 过期时间)。这一步如果失败直接报错,不会进上传。
  2. 构造 SDK 凭证 provider:把"如何拉 STS(安全令牌服务)"这个动作包成一个同步闭包给 SDK;SDK 在内部判断 token 在 5 分钟内将过期时会自动回调它再拉一次新的,外面不需要自己起定时器。
  3. 构造 SDK 客户端:每个上传任务一个,不全局复用(不同账号/region 会切换 endpoint,复用反而麻烦;构造代价本身很轻)。配三个超时/重试:单请求 30 秒、整任务最长 24 小时、SDK 自动重试 3 次。endpoint 没带 scheme 时这里兜底加https://(苹果 ATS 强制要求)。
  4. 拼云端路径:<dir>/<录制目录名>_<协同标识>/<相机位置 basename>.tar.gz。其中 basename 是head/left/right中的一个,相机位置不是这三个就直接透传,空就用unknown。三机的<协同标识>是同一个,所以三段录制最终落在同一个云端二级目录下。
  5. 配置分片任务:分片大小 5 MB(OSS 限制 100 KB ~ 10000 片);checkpoint 目录就是本地 staging 子目录;CRC 校验开;并把"取消时不要删 checkpoint"打开(取消时由我们自己清,避免和续传语义打架)。
  6. 跑上传:把阿里云 SDK 那一套 OC 风格的 future 桥成 Swift 的 async;进度回调照样转给上层。
  7. 结果分支:
    • 成功 → 主动清掉 staging(连.tar.gz和 checkpoint 一起);
    • 用户取消 → 主动清 staging;
    • 网络/服务端错误 → 故意不清 staging,下次进来 SDK 会自动 ListParts 跳过已完成分片继续传;
    • 服务端孤儿分片不靠客户端 abort 接口,而是依赖云端桶上配置的"分片清理生命周期策略"(建议 7 天)兜底。
  8. 冷启动垃圾回收:app 每次冷启动会扫一遍持久化的 staging 根目录,下面这三种情况之一就把整个子目录删掉:
    • 目录名解析不出来(手撸残留 / 老版本结构);
    • (录制目录名, 相机位置) 已经不在当前活跃队列里了;
    • 7 天没改过文件(明显是孤儿)。 不会动正在跑的或仍在队列里的。

4.3 上传到 S3 / 兼容 S3 协议

最简单:

  • 先校验 S3 连接配置(access key、secret、桶、endpoint、region、路径前缀)。
  • 拼 object key =<前缀>/<文件名>.tar.gz
  • 走 SigV4 签名做一次PutObject,单次 PUT 把整个.tar.gz推上去;进度回调照样转给上层。

4.4 上传到自建通用服务

走通用的 multipart POST 到用户配置的 URL,由一个"什么都干一点"的客户端封装负责。这条路径主要用于自部署调试。


五、状态与进度的写回

每条记录贯穿整个上传都在动两组东西:

5.1 队列里的"这一条"的状态

队列条目的关键字段:本机录制 id、文件夹名、入队时间、起始时间、任务标签、任务执行 id、用户指纹、相机位置、帧数、时长、本次的问题字典、问题文字摘要、上传状态(待上传 / 上传中 / 已完成 / 失败)、上次尝试时间、失败原因、失败分类、字节数。

主要写点:

  • 开始跑这一条 → 状态 = 上传中;
  • 成功 → 状态 = 已完成(这条以后不会再被自动重跑);
  • 失败 → 状态 = 失败,并写入"失败原因 / 失败分类"(详见下一节);
  • 用户在循环跑到它之前点取消 → 状态从"上传中"回到"待上传",并把它从内存队列踢出。

每次状态变更都会立刻把整张队列序列化回磁盘的uploadQueue.json(同时维护一份.bak),这样 app 任何时候被杀掉都不会丢记录。

5.2 全局进度

UI 看到的几个数字(总文件数 / 已完成 / 失败数 / 总字节 / 已传字节 / 当前文件名 / 当前文件已传 / 当前文件总大小)。这一组是节流后批量推到主线程的,避免 URLSession 高频回调把主线程刷爆。

5.3 失败分类

失败时不会把原始错误直接展示给用户,而是过一遍"错误分类器",归到几大类:网络断开 / 鉴权失败 / 磁盘满 / 服务端 5xx / 业务登记失败 / 其它。这样 UI 上能给用户一个能理解的提示,并且后续可以按"鉴权失败"这种类做统一的退避策略。


六、取消、续传与孤儿处理

6.1 用户点取消

后台循环里会立刻看到取消标记 + 当前文件那把 async 抛出取消错误:

  • 当前正在跑这条 → 状态从"上传中"回到"待上传",从内存队列踢出,循环 break;
  • 后续未跑的条目 → 整个内存队列被清空,但磁盘上的状态仍是"待上传";
  • DM3 目标的 staging 子目录会被主动清掉(续传无意义);
  • 其他目标的临时.tar.gz由 defer 兜底删掉。

6.2 失败保留 + 重试

  • 刻行 / S3 / 通用:临时.tar.gz已经被删,重试会从头打包再传,SDK 的 3 次重试 + 用户重试都打不进可断点续传语义。
  • DM3:staging 子目录里.tar.gz和 SDK checkpoint 都还在;用户重试或下次 app 启动自动重试时,会复用.tar.gz不重压,再让 SDK 自动 ListParts,跳过已上传分片继续传。这是为什么.tar.gz会有"已存在则复用"的判断。

6.3 孤儿/碎片清理

  • 上传队列那一头:app 启动时会重置"上传中"为"待上传",避免转圈死锁。
  • DM3 staging 那一头:每次冷启动跑一遍前面说的 GC(解析失败 / 不在活跃队列 / 7 天未动)。
  • 云端 OSS 那一头:客户端不调 AbortMultipartUpload(怕对桶策略形成强依赖),孤儿分片由桶级生命周期策略兜底自动清理。

七、一张总表:整个上传链路的"数据流向"

阶段输入关键动作输出

触发

用户点按钮 / 主机广播 / 启动重试

入队(去重)+ 启或复用循环

内存上传队列

准备

录制目录 +metadata.json

校验 + 选 staging or 临时目录

工作目录

打包

录制目录

tar → 1 MB chunk 流式 gzip + CRC32 + 字节数

.tar.gz(DM3 持久化 / 其它临时)

走刻行

.tar.gz+ 元数据

算 sha256 → 解析记录名(含主机广播)→ 拿预签名 URL → PUT → 业务登记

云端记录 + 业务侧已登记

走 DM3

.tar.gz+ 元数据 + 协同标识

拉 STS → 构造 SDK 凭证/客户端 → 5 MB 分片 ResumableUpload + checkpoint → 成功/失败差异化清理

云端桶 + 同协同标识三段同前缀

走 S3

.tar.gz+ S3 配置

校验 → SigV4 PutObject 单次 PUT

云端桶里一个对象

走通用

.tar.gz+ 通用 URL

multipart POST

自建服务接收完成

状态

上传过程中的事件

节流推 UI + 写uploadQueue.json(含.bak)+ 失败分类

用户看到的状态 + 持久化队列

取消

用户点取消

抛 cancel + 状态回滚 + 清 staging(DM3)/ 临时(其它)

队列清空,磁盘条目仍可重新发起


八、几个容易忽略的设计点

  1. 单条串行:app 同一时间只跑一条文件,避免几个 GB 的 .tar.gz 并发压垮内存和带宽,也方便进度展示。
  2. 流式 gzip 是命脉:录制几分钟就能上 GB,整文件读进内存做 gzip 一定 OOM(内存耗尽);流式 1 MB chunk 是必须的。
  3. DM3 续传强依赖持久化目录:staging 是".tar.gz+ SDK checkpoint"共生命周期目录,它两个必须放在一起,单独丢任何一个续传都坏。
  4. 协同录制的"记录名"协调:靠主机一次广播就把三机的"挂哪条记录"绑死,避免每台从机各自建一条同名孤儿记录,是这条上传链路里最微妙的一段一致性逻辑。
  5. 业务登记是刻行链路的"真终点":文件就算 PUT 成功,没登记成功这条记录就算失败;这是为了让业务侧能查得到、能分配训练任务。
  6. DM3 直传不依赖客户端 abort:服务端孤儿分片靠桶级策略兜底清理,让客户端 IAM 只需要"上传/列分片"权限,不需要"中止分片",部署侧成本更低。

用户在"上传"Tab 选中条目并点上传后,后台会起一个串行任务循环跑选中的每条录制目录,单条流程是:先校验目录与元数据 → 把整个录制目录用 tar 打包再做 1 MB 流式 gzip 压成.tar.gz(避免大文件 OOM)→ 按目标分发上传,成功后写状态、推进度、持久化队列,失败按类别记错可重试。两个目标的差异主要在第三步——

走刻行(CoScene):临时目录里产出的.tar.gz算 sha256 后,先解析这次录制对应的"业务记录"(主机直接调建/复用接口并广播给从机,从机优先用主机广播带的或缓存里的,最多等主机 15 秒否则降级自建);拿到记录后调云端要一个一次性预签名 PUT URL,用 URLSession 直传.tar.gz并实时回报进度;传完还要再调一次"同步文件信息"接口做业务登记(任务执行 id、文件名、大小、时长),登记成功才算这条完成;临时.tar.gz全程跑完即扔。

走 DM3(阿里云 OSS):.tar.gz落在Documents/dm3UploadResume/<目录键>/这种持久化 staging 子目录里(已存在则直接复用不重压);先用登录态拉一次 STS 拿到 endpoint/桶/路径前缀和临时 ak/sk/securityToken,再把 STS 刷新逻辑包成阿里云 SDK 的凭证 provider(过期前 5 分钟自动回调刷新);每个任务起独立 SDK 客户端,配 30 s 单请求 / 24 h 整任务超时和 3 次重试;最后用 SDK 的可断点续传上传(5 MB 一片,checkpoint 写在同一 staging 目录),云端路径是<dir>/<录制目录名>_<协同标识>/<head|left|right>.tar.gz,三机协同时同一协同标识落在同一二级目录下;成功 → 清 staging,用户取消 → 清 staging,网络/服务端错误 → 故意保留 staging 让下次进来跳过已传分片续传,服务端孤儿分片由桶生命周期策略兜底,每次冷启动还会扫一遍 staging 删除"解析失败/不在活跃队列/7 天未动"的孤儿。

细拆刻行跟dm3上传逻辑

一、两条路径在做的事其实是一样的

不管走哪条,本质都是:把那个.tar.gz推到一个云端对象存储里。区别只在于"凭什么权限去推"和"推的姿势"。

  • 走刻行:app 自己没有云端的钥匙;它先找刻行后端要一把"一次性临时门票"(带签名的 PUT URL),再用普通 HTTP 一口气传上去。
  • 走 DM3:app 找 DM3 后端要一把"短期工牌"(STS 临时密钥,带过期时间),凭这把工牌直接以客户身份到阿里云对象存储那边,按云厂商原生的"分片+续传"协议传。

一句话:刻行那边是"门票一次性 PUT",DM3 是"工牌+原生协议分片"。所以 DM3 比较啰嗦,但能力上确实强不少。

二、刻行那条路径的"脆弱点"

把刻行的姿势再拆一下,几个隐性约束就出来了:

  1. 那个签名 PUT URL 是一次性且有时效的:必须一口气 PUT 完整个.tar.gz,中途断了,URL 可能就过期了,下次得重新去要一次。
  2. 它走的是 HTTP PUT 的"整文件上传"语义:没有断点,传到 80% 网断了,下次只能从 0% 开始。
  3. 一次性 PUT 由 URLSession 自己管,你没法精确地告诉它"这一段已经传过了不要重来"。
  4. 所有上传都得走刻行后端发 URL 这一跳;刻行后端挂了,传不了;要换别的 region 也得它配合。

对几十 MB 的录制,重传一次大不了等几秒钟,无所谓;但 app 的录制经常是几百 MB 到几 GB,PUT 到 90% 网络抖一下就全废,这事在差网络下非常痛。

三、DM3 是怎么把这些痛点逐一破掉的

理解 DM3 那套设计的钥匙,就是:把"凭证"和"上传"解耦,把"上传过程"变成可中断、可恢复的状态机。

它一共做了 4 件互相咬合的事:

1. 用 STS 把"长期账号"换成"短期工牌"(安全令牌服务,核心就是发临时权限凭证

DM3 后端不会把云厂商的长期 ak/sk 给客户端(那等于把保险柜钥匙给陌生人)。它每次只发一对临时凭证:access key、secret、安全 token、过期时间,再加上"你这次能往哪个桶 / 哪个目录写"。

好处:

  • 即使临时凭证泄露,几小时就过期了,破坏面很小。
  • 客户端拿这把工牌直连云厂商,不再每分钟都去 ping DM3 后端发 URL;DM3 后端只负责"发凭证"这一下,瞬时压力低,可用性更好。
  • 桶、地域是 STS 现说现给的,后端要换 region、换桶、按账号路由都不用改 app。

2. 凭证"用的时候才刷"——不是定时器,是回调

如果你写过定时器刷 token 的代码,会知道一堆边界 case:app 退后台定时器停了、刷新和上传请求打架、刷新失败要不要重试、临界点 ms 误差……

这里改成:把"如何从 DM3 后端拉一次新凭证"包成一个回调交给云 SDK;SDK 内部判断"当前凭证还剩 5 分钟内会过期吗?",是,就当场同步回调一次这个闭包拉新的,不是就直接复用旧的。app 这边一行定时器都没有,不会有"刷过头""刷漏""刷打架"。

底层有个细节:SDK 要的是同步返回,但拉凭证是异步网络请求;为此那段代码用Task.detached+信号量把异步硬转同步——这事看起来很怪,但被严格限定在"只在 SDK 后台线程的回调里调",所以不会卡主线程。

3. 真正的杀招:原生分片 + 断点续传

这一步是 DM3 比刻行强出最多的地方。

分片:把.tar.gz切成 5 MB 一块,每块独立上传一次。一块失败只重传这一块,不影响别的。

断点续传:上传任务在云端有个 uploadId,一边传,云端就一边记"哪些块已经收到了"。客户端这边对应也有一份本地 checkpoint 文件,记着 uploadId、分片大小、已完成块号。

把这两条合起来,就有了非常关键的能力:

"这次传到 60% 时进程被杀掉。下次启动 app,SDK 看到本地 checkpoint,会先去云端 ListParts 问一句'我上次报的这个 uploadId,你那边收到哪些块了?',然后只补传剩下那 40%,并把所有块合并成最终对象。"

这就是为什么 DM3 路径里还需要一个持久化的 staging 目录——不像刻行那样把.tar.gz扔临时目录里跑完就丢,DM3 要把.tar.gz和 SDK 的 checkpoint 绑在一起存到Documents下,进程被杀、用户重启 app、网卡换 Wi-Fi 再回 4G 都能接着传。

刻行那一条是"传到 90% 断了从 0 重来";DM3 是"传到 90% 断了从 90 接着来"。在几 GB 文件 + 弱网场景下,这就是"能上得去"和"永远上不上去"的差别。

4. 失败、取消、重启的三种状态机非常清晰

DM3 这套之所以"看起来深奥",是因为它把三种异常做了截然不同的处理(不像刻行只能一刀切重传):

情况本地 staging云端那个未完成的分片任务

用户主动取消

主动整个删掉(续传也没意义了)

不调中止接口,让云端按生命周期策略自动清

网络/服务端错误

故意保留

保留;下次进来 ListParts 续传

上传成功

删掉(含 .tar.gz + checkpoint)

自动转正,孤儿分片自动消失

app 冷启动后扫一遍

三种条件之一就清:目录名解析失败 / 不在活跃队列 / 7 天没动

同上,由桶生命周期兜底

staging = iPhone 上Documents/dm3UploadResume/下的一个本地暂存目录,每条 DM3 上传任务一个,里面同时放打好的.tar.gz和阿里云 SDK 的续传 checkpoint,作用就是让上传可以跨进程被杀、跨重启、跨网络中断地从中间接着传。

这里有几个故意为之的设计判断特别值得说:

  • 取消时不调"中止上传"接口,而是让云端的桶生命周期策略(建议 7 天)来兜底清理孤儿分片。原因是:调这个接口需要给客户端一个额外的 IAM 权限,部署时多一个坑;不调它,权限可以收得更紧,"放在那儿过几天自己烂掉"反而更稳。
  • 错误时不删 checkpoint:直觉上失败应该清场,但这里反过来——失败保留,重试才能续传。"清干净"的语义只在"用户取消"和"传成功"两个场景下才执行。
  • 冷启动 GC 是兜底:万一某条上传被人为从队列里删除了 / 永远没机会重试 / 网络真的卡了一周,那个 staging 目录会被自动回收,不会无限占磁盘。

四、形象一点的类比

  • 刻行那条:你委托一个中间人去寄快递。每次寄,你都要先打电话给中间人要一张"今天寄到上海仓的提货单"(一次性、有时效);然后你必须一口气把整个箱子送到。送一半箱子掉路上,那张提货单作废,你下次得重新打电话要一张,整个箱子从头送。
  • DM3 那条:DM3 给你发了一张"今天有效的临门工牌",让你直接进阿里云仓库。你可以把箱子拆成 5 MB 一份的小盒子一个一个交,仓库会逐个登记到你的"任务单 uploadId"下。送一半你走了?没事,明天回来打开你家里的小本子(本地 checkpoint),看上次送到第几个盒子,再问仓库前台核对一下,没送的接着送,全部送完仓库自动把它们拼成一个完整的箱子;这期间工牌过期了,你的 SDK 助理会自动回去 DM3 前台再换一张新工牌,你都感知不到。

五、一句话回答"强在哪"

DM3 比刻行那条路径强出来的能力,本质就这五条:

  1. 断点续传:不再"全有或全无",弱网/被杀/取消重启都能从中间接着传。
  2. 凭证短期化:长期密钥不下发,泄露代价低。
  3. 凭证自动刷新无感知:不依赖任何定时器。
  4. 不依赖一次性签名 URL:不会出现"签名过期了得回头再要一张"的连锁失败。
  5. 后端只在发凭证时被路过一次:链路短,DM3 后端故障不直接拖死上传。

代价就是它必须额外维护"持久化 staging 目录 + 冷启动 GC + STS 同步桥接"这三块工程量,看起来比刻行那条复杂;但只要文件足够大、网络足够烂,这套复杂度立刻就会变成"传得上去 vs 传不上去"的硬差别。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/30 0:31:02

订单超时自动关单失效,库存扣减重复,支付状态不一致……PHP分布式订单常见12类血泪坑,现在修复还来得及!

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;PHP分布式订单系统的典型故障全景图 在高并发电商场景下&#xff0c;PHP构建的分布式订单系统常因架构松散、状态不一致与中间件协同失配而暴露出系统性脆弱点。故障并非孤立发生&#xff0c;而是呈现链…

作者头像 李华
网站建设 2026/4/30 0:30:08

从门禁卡到5G通信:国密算法SM1/SM4/SM7/ZUC在你身边的隐藏应用图鉴

从门禁卡到5G通信&#xff1a;国密算法SM1/SM4/SM7/ZUC在你身边的隐藏应用图鉴 每天早晨&#xff0c;当你用公司门禁卡"滴"的一声打开办公室大门时&#xff0c;可能不会想到这张小小的卡片背后运行着怎样的加密魔法。同样&#xff0c;当你用手机进行5G视频通话时&…

作者头像 李华
网站建设 2026/4/30 0:22:10

【Hot 100 刷题计划】 LeetCode 15. 三数之和 | C++ 排序+双指针

LeetCode 15. 三数之和 &#x1f4cc; 题目描述 题目级别&#xff1a;中等 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重…

作者头像 李华
网站建设 2026/4/30 0:21:39

OpenAI烧钱扩张、广告化引担忧,AI可靠性成下一战关键

OpenAI&#xff1a;光鲜背后的尴尬困境OpenAI如今站在尴尬的十字路口。它有9.6亿月活用户&#xff0c;是C端王者&#xff0c;年化收入250亿美元&#xff0c;最新估值8520亿美元。但背后却是一年烧掉570亿美元、净亏440亿美元的残酷现实。二级市场上&#xff0c;其股份价格较官宣…

作者头像 李华