Highlight
游戏和大型 app 的资源分发一直是痛点:全打进主包太大(还有 4GB 限制),On-Demand Resources 又要跟着 app 一起更新。Background Assets 的 Managed 模式解决了这些问题。
核心内容
做一款带教程关卡的游戏,开发者通常面临一个尴尬选择。要么把所有资源塞进主包,用户下载时要等几百兆甚至几个 G 才能进游戏,多数平台还有 4GB 上限;要么用 On-Demand Resources,但每次更新一个素材文件都要重新提交整个 app。还有第三条路 URLSession 自己写下载逻辑,得自己处理重试、断点续传、版本协调(01:54)。
WWDC25 给出的新方案是 Managed Background Assets,加上 Apple 托管。资源被切成多个 asset pack,每个 pack 有自己的下载策略:Essential 跟着 app 安装一起下,Prefetch 安装后台继续下,On-demand 等代码请求才下(04:00)。系统提供一个现成的 downloader extension,零代码即可接入;Apple Developer Program 会员还能拿到 200GB 免费托管空间,asset pack 和 app build 在 App Store Connect 独立上传、独立审核、独立版本管理,更新资源不再需要发新版 app(03:36)。教程关卡用 Essential + firstInstallation 限定,新用户安装时直接拿到,老用户更新版本不会重复下载。
详细内容
第一步是写 manifest。xcrun ba-package template 生成一个 JSON 模板,填入 asset pack ID、下载策略、平台和文件选择器(08:26)。
{
"assetPackID": "Tutorial",
"downloadPolicy": {
"essential": {
"installationEventTypes": [
"firstInstallation",
"subsequentUpdate"
]
}
},
"fileSelectors": [
{ "file": "[Path to File]" },
{ "directory": "[Path to Directory]" }
],
"platforms": ["iOS", "macOS", "tvOS", "visionOS"]
}
关键点:
assetPackID:自定义字符串,运行时代码用它来引用这个 pack。downloadPolicy三选一:essential、prefetch、onDemand。前两种要给installationEventTypes数组,onDemand给空对象{}。installationEventTypes决定何时下:firstInstallation只在首次安装时下,subsequentUpdate在升级时也下。fileSelectors可加任意多个,按文件或按目录都行,路径相对于源码根。platforms写明这个 pack 支持哪些平台,多个平台用同一个 pack。
写完 manifest 后再跑一次 ba-package 把内容打包成 .aar 压缩包(09:53)。
接下来在 Xcode 里加一个 Background Download 模板的 extension target。今年新增的系统级 downloader extension,几乎不用写代码(10:44)。
import BackgroundAssets
import ExtensionFoundation
import StoreKit
@main
struct DownloaderExtension: StoreDownloaderExtension {
func shouldDownload(_ assetPack: AssetPack) -> Bool {
return true
}
}
关键点:
StoreDownloaderExtension是 Apple 托管场景的协议,自托管对应BackgroundDownloaderExtension。shouldDownload(_:)系统在调度每个新 pack 之前会调用一次,返回false可以跳过下载,适合做兼容性筛选。- 如果不需要定制,整个
shouldDownload(_:)都可以删掉,系统默认实现会处理一切。
主 app 里读取 asset pack 的代码也很短(11:39):
let assetPack = try await AssetPackManager.shared.assetPack(withID: "Tutorial")
// Await status updates for progress information
let statusUpdates = AssetPackManager.shared.statusUpdates(forAssetPackWithID: "Tutorial")
Task {
for await statusUpdate in statusUpdates {
// …
}
}
// Download the asset pack
try await AssetPackManager.shared.ensureLocalAvailability(of: assetPack)
关键点:
AssetPackManager.shared是入口单例,所有操作都从它出发。assetPack(withID:)拿到 pack 句柄;多数情况下 downloader extension 已经在后台下好了。statusUpdates(forAssetPackWithID:)返回AsyncSequence,可以在循环里拿到.downloading(_, progress)之类的状态用于 UI。ensureLocalAvailability(of:)是核心方法:已经下好就立即返回,没下好就等下载完。罕见情况下会真的等很久,所以建议同时订阅状态做进度展示。
下载完之后读文件用 contents(at:)(12:41):
// Read a file into memory
let videoData = try AssetPackManager.shared.contents(at: "Videos/Introduction.m4v")
// Open a file descriptor
let videoDescriptor = try AssetPackManager.shared.descriptor(for: "Videos/Introduction.m4v")
defer {
do {
try videoDescriptor.close()
} catch {
// …
}
}
关键点:
contents(at:)的路径是相对于源码根的相对路径,跟你跑ba-package时的工作目录一致。系统把所有 asset pack 合并成一个共享命名空间,读文件时不用关心它在哪个 pack 里。- 默认返回 memory-mapped
Data,适合大文件,不会一次性占内存。 descriptor(for:)拿到文件描述符做底层读取,必须自己close(),建议用defer。- 两个方法都接受可选的
searchingInAssetPackWithID:参数把搜索范围限定到单个 pack。
不再需要的 pack 主动调 remove(assetPackWithID:) 释放空间,下次还可以用 ensureLocalAvailability(of:) 重新下回来(13:56)。系统自动保持已下载 pack 是最新版本,但不会主动删除,用户能在 Settings 里看到你的 app 占了多少空间。
最后是配置(14:53):
<key>BAAppGroupID</key>
<string>group.com.naturelab.thecoast</string>
<key>BAHasManagedAssetPacks</key>
<true/>
<key>BAUsesAppleHosting</key>
<true/>
关键点:
BAAppGroupID:主 app 和 downloader extension 必须加入同一个 App Group,系统靠这个协调两端。BAHasManagedAssetPacks:声明使用 Managed 模式,系统才会接管下载和更新。BAUsesAppleHosting:用 Apple 托管时设为true;自托管的话需要另外配BAManifestURL等键。
本地测试用 ba-serve 起一个 mock server。下载走 HTTPS,所以要先建一个根 CA、装到测试设备上、签一张 SSL 证书、再启动 mock server。设备端在 Developer Settings > Development Overrides 里填入 mock server 的地址即可(15:29)。
上线流程也独立。Asset pack 通过 Transporter app 拖拽上传,或者走 App Store Connect REST API 三步走:先 POST backgroundAssets 建 pack 记录,再 POST backgroundAssetVersions 建版本,最后用 backgroundAssetUploadFiles 上传 .aar(18:47)。同一个 asset pack 可以有多个 live 版本:App Store、TestFlight 外测、TestFlight 内测各一个,服务器按 app 来源选择对应版本下发(05:34)。这意味着资源包更新后,所有仍然装着老版 app 的用户也会自动切到新资源包,所以发新 pack 前一定要测向后兼容。
核心启发
-
把首次安装体验切出一个 Essential pack:教程关卡、首屏素材打到一个 pack 里,下载策略选
essential加firstInstallation,老用户升级时不会重复下载。新用户安装完直接能玩,留存数据会好看不少。- 为什么值得做:游戏类 app 主包动辄 1GB 起,用户在 App Store 等下载完就掉一批,进游戏再等加载又掉一批;Essential 把这两段下载合并到 App Store 进度条里,用户感知里只有一次等待。
- 怎么开始:跑
xcrun ba-package template拿到 manifest 模板,把首屏素材的目录路径填进fileSelectors,installationEventTypes只留firstInstallation,配合 Apple 托管直接用,不用搭服务器。
-
把 DLC 改造成 On-demand pack 配合 IAP:付费章节、限时活动地图、节日皮肤这类内容,下载策略改成
onDemand,购买成功后再调ensureLocalAvailability(of:)触发下载。- 为什么值得做:DLC 不下载就不占用户的设备空间,关卡用完还能
remove(assetPackWithID:)清掉;活动版本通过新 pack 推送,不用走 app 审核,节日运营节奏可以更快。 - 怎么开始:每个 DLC 单独打一个 pack,购买流水回来后用 pack ID 拉资源;进度展示监听
statusUpdates(forAssetPackWithID:)的.downloading状态。
- 为什么值得做:DLC 不下载就不占用户的设备空间,关卡用完还能
-
把端上 ML 模型挪到 Background Assets:CoreML、向量索引、嵌入向量表这些会迭代的二进制资源,单独发一个 pack,模型升级走 asset pack 通道而不是 app 通道。
- 为什么值得做:模型一改就发整个 app 太重,App Store 审核也慢;asset pack 审核更快,用户不需要主动升级 app 就能拿到新模型。
- 怎么开始:把模型文件放进一个独立目录,packaging 时写一个
directory选择器整目录打包;用descriptor(for:)拿文件描述符喂给 ML 框架。注意发新版模型前一定要在老版本 app 上回归测试,因为旧 app 也会自动切到新资源。
-
从 On-Demand Resources 迁移:ODR 已宣布废弃,新项目不要再用,老项目排个迁移计划。
- 为什么值得做:ODR 资源跟 app build 绑死,更新一张图都要重新过审;Background Assets 解耦后开发节奏完全不同。
- 怎么开始:先把 ODR tag 按业务功能重新切成 asset pack,下载策略对应原来的 prefetch/on-demand 行为,一次迁一个功能模块,先跑 mock server 自测再上 TestFlight。
-
善用 Apple 托管的 200GB:Developer Program 会员费包含 200GB 免费托管,自托管的 CDN 账单可以省掉。
- 为什么值得做:自己搭 CDN 要处理签名、限流、防盗刷,Apple 托管把这些都接管了,还自动按平台/构建匹配版本。
- 怎么开始:Info.plist 里加
BAUsesAppleHosting = true,downloader extension 模板选 Apple hosting,.aar通过 Transporter 拖上去就行。
关联 Session
- Dive deeper into Writing Tools — 在 app 里集成 Writing Tools 校对、改写、转换文本的能力。
- Dive into App Store server APIs for In-App Purchase — App Store Server API 与通知的最新更新,配合 DLC 类 on-demand asset pack 使用。
- Enhance child safety with PermissionKit — 用 PermissionKit 增强儿童通讯安全的新框架。
- Explore enhancements to your spatial business app — visionOS 26 给空间业务应用带来的 API 增强。
评论
GitHub Issues · utterances