WWDC Quick Look 💓 By SwiftGGTeam
Discover Apple-Hosted Background Assets

Discover Apple-Hosted Background Assets

观看原视频

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 三选一:essentialprefetchonDemand。前两种要给 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 上传 .aar18:47)。同一个 asset pack 可以有多个 live 版本:App Store、TestFlight 外测、TestFlight 内测各一个,服务器按 app 来源选择对应版本下发(05:34)。这意味着资源包更新后,所有仍然装着老版 app 的用户也会自动切到新资源包,所以发新 pack 前一定要测向后兼容。


核心启发

  • 把首次安装体验切出一个 Essential pack:教程关卡、首屏素材打到一个 pack 里,下载策略选 essentialfirstInstallation,老用户升级时不会重复下载。新用户安装完直接能玩,留存数据会好看不少。

    • 为什么值得做:游戏类 app 主包动辄 1GB 起,用户在 App Store 等下载完就掉一批,进游戏再等加载又掉一批;Essential 把这两段下载合并到 App Store 进度条里,用户感知里只有一次等待。
    • 怎么开始:跑 xcrun ba-package template 拿到 manifest 模板,把首屏素材的目录路径填进 fileSelectorsinstallationEventTypes 只留 firstInstallation,配合 Apple 托管直接用,不用搭服务器。
  • 把 DLC 改造成 On-demand pack 配合 IAP:付费章节、限时活动地图、节日皮肤这类内容,下载策略改成 onDemand,购买成功后再调 ensureLocalAvailability(of:) 触发下载。

    • 为什么值得做:DLC 不下载就不占用户的设备空间,关卡用完还能 remove(assetPackWithID:) 清掉;活动版本通过新 pack 推送,不用走 app 审核,节日运营节奏可以更快。
    • 怎么开始:每个 DLC 单独打一个 pack,购买流水回来后用 pack ID 拉资源;进度展示监听 statusUpdates(forAssetPackWithID:).downloading 状态。
  • 把端上 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

评论

GitHub Issues · utterances