全部文章

跨团队协作,也像跨进程通信

从两个 App 的同步发布 SDK 出发,比较传文件与传标识符,理解小协议、共享事实来源、版本协商和异步回调的价值。

一个勾选项,背后是两个 App

发布页增加“同时发布到另一个应用”,听起来只是一个 checkbox。背后却是不同 bundle ID、代码库、排期和发版节奏,以及几百 MB 的导出视频。iOS 用沙盒隔开进程,组织边界也隔开团队。

真正的问题是:双方约定让什么跨过边界?这个技术问题,后来也改变了我理解跨团队协作的方式。

方案一:传文件

编辑器将视频导出到自己的沙盒:

let exportURL = FileManager.default
    .temporaryDirectory
    .appendingPathComponent("export.mp4")

再通过系统分享面板交出去:

let controller = UIActivityViewController(
    activityItems: [exportURL],
    applicationActivities: nil
)
present(controller, animated: true)

接收方的 Share Extension 从 NSItemProvider 加载表示,并复制到可以保留的位置:

itemProvider.loadFileRepresentation(
    forTypeIdentifier: UTType.movie.identifier
) { url, error in
    // the URL is temporary; copy the file
    // into our own container before it disappears
}

扩展通常再使用 App Group 容器交给主应用。它是通用方案,不要求双方来自同一组织,也不需要相册权限,适合向任意第三方分享。

代价是可能保留多份应用拥有的文件,几百 MB 视频的复制会产生明显存储与等待成本;产品已经选择目的地,用户却还要绕过一次系统分享面板。

方案二:传标识符

先把导出结果保存到系统相册:

var placeholderIdentifier: String?

PHPhotoLibrary.shared().performChanges({
    let request = PHAssetChangeRequest
        .creationRequestForAssetFromVideo(atFileURL: exportURL)
    placeholderIdentifier = request
        .placeholderForCreatedAsset?.localIdentifier
}) { saved, error in
    guard saved, let identifier = placeholderIdentifier else {
        // Surface the save failure; do not hand off an unresolved identifier.
        return
    }

    routeToSocialApp(assetIdentifier: identifier)
}

交给另一方的只是 localIdentifier:

69D53A4B-8F24-4D7A-.../L0/001

接收方通过 PhotoKit 解析:

let assets = PHAsset.fetchAssets(
    withLocalIdentifiers: [identifier],
    options: nil
)
guard let asset = assets.firstObject else { return }

PHImageManager.default().requestAVAsset(
    forVideo: asset,
    options: nil
) { avAsset, _, _ in
    // feed the publish pipeline
}

相册成为双方可以访问的系统存储,跨边界传递的是引用。它不是字面意义上的共享内存;Photos 仍可能按系统策略下载、转码或生成资源表示。

文件复制与 PhotoKit 标识符交接文件路径为接收者保留应用副本;标识符路径先导入系统相册,再传递引用。

这不需要私有文件通道,只需要双方可访问的系统服务和一份小型版本化协议。

SDK 真正承载的是协议

共享 SDK 的请求与回调发送方校验并路由小请求;接收方再次校验、解析相册资源,以一次性 state 关联异步结果。

发送前检查资源存在、类型与时长数量符合约束、目标应用可用且协议版本兼容。尽早失败,可以避免把本地可发现的问题留给另一方解释。

请求载荷只保留媒体引用和业务元数据:

{
  "protocol_version": 3,
  "media_type": "video",
  "local_identifiers": ["69D53A4B-.../L0/001"],
  "state": "7F92E1C4-9B7A-4B15-A3F1-82C2F769D8F4",
  "title": "…",
  "sync_publish": true,
  "callback_id": "editor-v1"
}

send() 构造 URL 并请求系统打开目标应用,负责地址与版本,不传输视频字节。到达接收方后,必须重新验证版本、资源、权限与业务约束。发送端校验改善体验,接收端校验维护自身不变量。

发布是异步且可取消的。请求带密码学随机、一次性的 state,完成、取消或失败都回传它,用于关联待处理请求。

接收者不应打开载荷里的任意 callback URL,而应把白名单 callback ID 映射到事先约定的 scheme 或 universal link。发送者校验回调来源、设置超时、只消费 token 一次,拒绝无请求和重放结果。自定义 URL scheme 本身不能证明调用者身份。

request.send { response in
    switch response.result {
    case .success: markSyncPublished()
    case .cancelled: revertCheckbox()
    case .failure(let error): surface(error)
    }
}

两条路径各自付出什么

维度传文件传标识符
跨边界内容文件字节小型资源 ID
持久存储接收应用或 App Group 的副本清理导出临时文件后的 Photos 资源
相册权限不需要两侧都需要符合访问条件
体验经过分享面板直接进入约定的发布流程
适用范围任意兼容应用接入该 SDK 的应用
失败情况复制失败、扩展资源限制资源被删除、权限不足

决定性的要求是:同步发布不能额外保留一份由接收应用长期拥有的视频副本。导入时仍有临时导出文件,系统也可能生成内部表示;保存成功后,编辑器可以清理临时文件,后续目的地只接收标识符。

iOS 14 的有限照片库权限让情况更复杂:接收应用可能无权访问刚保存的资源,用户也可能在交接后删除视频。这些都应成为显式失败状态,而不是假设系统存储永远可用。

为什么这也像团队协作

沙盒像团队边界,不能假设对方能直接读取自己的代码、排期和上下文。复制大文件,也像不断复制文档、数据快照和工具模块:副本从生成开始就可能过期。

更好的接口是一个小引用,加一个双方认可的事实来源。工单 ID、schema 名称和文档链接,允许对方在真正需要时读取当前版本。

协议版本对应不同发版节奏:不能要求双方永远同步上线。带 state 的回调对应异步协作:明确请求、关联标识,允许答案晚到,而不是让会议或整个工作流同步阻塞。

双方校验也不是不信任。请求方尽量准备充分,接收方仍然对自己的验收条件负责。

最终,勾选项上线了,视频本身不再通过应用间的 URL 通道传递。更长久的经验是:跨越边界时,先找双方都能访问的权威来源,再传递足够小、可版本化、可关联的引用。优化协议,往往比继续加粗传输管道更有效。