WWDC Quick Look 💓 By SwiftGGTeam
Meet Background Assets

Meet Background Assets

观看原视频

Highlight

Apple 在 iOS 16、iPadOS 16 和 macOS 13 中引入了 Background Assets 框架,允许开发者通过后台下载扩展(Background Download Extension)在应用安装后、首次启动前自动从 CDN 下载大文件资源,解决用户打开应用后仍需等待资源下载的痛点。

核心内容

用户等待的痛点

你花了半天时间在 App Store 找到一款心仪的游戏,点击下载,等了几分钟甚至更久,终于安装完成。满怀期待地打开应用,却看到一个大大的进度条——应用还要再下载几个 GB 的资源包。

这种体验对网络较慢的用户尤其糟糕。很多人会选择直接关闭并卸载应用。开发者也不想这样,但现有的方案都有局限:On-Demand Resources 需要把资源托管给 Apple;URLSession 后台下载只能在应用启动后使用。

Background Assets 的解决思路

Apple 的解决方案是一个全新的框架:Background Assets。它引入了一种新的应用扩展——后台下载扩展(Background Download Extension),这个扩展在应用生命周期之外运行。

扩展会在三个时机被系统唤醒:

  1. 首次安装后:用户还没打开应用,扩展就开始下载资源
  2. 应用更新后:App Store 自动更新应用后,扩展被唤醒下载新资源
  3. 定期运行:根据用户使用频率,系统会周期性唤醒扩展检查更新

开发者只需在扩展中调度下载任务,系统会选择合适的时机执行。下载完成后,文件存放在应用组(App Group)共享容器中,应用启动时直接可用。

与现有方案的关系

Background Assets 设计得很灵活。如果你已经有复杂的资源管理系统,它可以无缝接入。它适用于需要从自有 CDN 下载资源的场景,与 Apple 托管的 On-Demand Resources 形成互补。

详细内容

BADownloadManager:核心调度接口

05:28

BADownloadManager 是框架的核心类,一个单例对象,贯穿应用和扩展:

import BackgroundAssets

let url = URL(string: "https://cdn.example.com/large-asset.bin")!
let appGroupIdentifier = "group.WWDC.AssetContainer"
let download = BAURLDownload(
    identifier: "Large-Asset",
    request: URLRequest(url: url),
    applicationGroupIdentifier: appGroupIdentifier
)

let manager = BADownloadManager.shared
manager.delegate = self

// 由系统决定最佳时机执行
do {
    try manager.schedule(download)
} catch {
    print("Failed to schedule download. \(error)")
}

// 或立即在前台执行
do {
    try manager.startForegroundDownload(download)
} catch {
    print("Failed to start foreground download. \(error)")
}

关键点:

  • BAURLDownloadidentifier 必须唯一,系统不允许重复调度相同 identifier 的下载
  • applicationGroupIdentifier 指定 App Group,应用和扩展必须加入同一组才能共享下载的文件
  • schedule(_:) 让系统选择最佳时机执行,适合扩展中使用
  • startForegroundDownload(_:) 立即开始且优先级更高,只能从应用主程序调用

将后台下载提升到前台

07:58

如果用户打开了应用,而扩展调度的后台下载还在进行中,你应该立即把它们提升到前台:

do {
    for download in try await manager.fetchCurrentDownloads() {
        try manager.startForegroundDownload(download)
    }
} catch {
    print("Failed to promote downloads to foreground \(error)")
}

关键点:

  • fetchCurrentDownloads() 返回所有已调度、进行中或排队中的下载
  • 提升为前台后,已下载的部分不会重新下载,会从断点继续
  • 扩展本身不能调用前台下载 API,因为它没有 UI,用户感知不到

委托协议:监控下载状态

10:28

BADownloadManagerDelegate 协议接收下载状态回调:

public protocol BADownloadManagerDelegate : NSObjectProtocol {
    optional func downloadDidBegin(_ download: BADownload)
    optional func downloadDidPause(_ download: BADownload)
    optional func download(_ download: BADownload,
                           bytesWritten: Int64,
                           totalBytesWritten: Int64,
                           totalExpectedBytes: Int64)
    optional func download(_ download: BADownload,
                           didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
    optional func download(_ download: BADownload, failedWithError error: Error)
    optional func download(_ download: BADownload, finishedWithFileURL fileURL: URL)
}

关键点:

  • 所有委托方法都是 optional,按需实现
  • downloadDidPause 在后台下载被提升为前台时触发,中间会有短暂暂停
  • 下载成功后的文件由系统管理,存储空间不足时系统会自动清理
  • 建议保留文件在系统提供的位置,如需移动,移动后删除原文件避免重复占用空间

后台下载扩展:在应用未启动时运行

15:37

扩展需要实现 BADownloaderExtension 协议:

public protocol BADownloaderExtension : NSObjectProtocol {
    optional func applicationDidInstall(metadata: BAApplicationExtensionInfo)
    optional func applicationDidUpdate(metadata: BAApplicationExtensionInfo)
    optional func checkForUpdates(metadata: BAApplicationExtensionInfo)
    optional func download(_ download: BADownload,
                           didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
    optional func backgroundDownloadDidFail(failedDownload: BADownload)
    optional func backgroundDownloadDidFinish(finishedDownload: BADownload, fileURL: URL)
    optional func extensionWillTerminate()
}

关键点:

  • applicationDidInstall:应用首次安装后调用,适合下载初始资源包
  • applicationDidUpdate:App Store 更新应用后调用
  • checkForUpdates:系统周期性唤醒,频率取决于用户使用频率
  • 扩展生命周期很短,只能快速调度下载,不能做解压等耗时操作
  • 扩展运行频率会根据应用使用情况动态调整:常用应用扩展运行更频繁

Info.plist 配置要求

12:56

应用的主 Info.plist 需要添加两个键:

  1. BAInitialDownloadRestrictions(字典):

    • DownloadAllowance:首次安装时允许下载的最大总字节数(所有文件之和)
    • AllowList:允许下载的域名数组,支持前缀通配符
    • 这些限制只在首次安装时生效,应用启动后不再限制
  2. BADownloadSize(根级别):

    • 资源解压后的总大小
    • 这个数值会显示在 App Store 中,让用户知道需要额外下载多少内容

应用与扩展的同步机制

19:40

扩展和应用可能同时运行,存在竞争条件。比如扩展下载了一个目录文件,应用和扩展都想读取并删除它:

func download(_ download: BADownload, finishedWithFileURL fileURL: URL) {
    let manager = BADownloadManager.shared
    manager.withExclusiveControl { error in
        guard error == nil else {
            print("Unable to acquire exclusive control \(String(describing: error))")
            return
        }
        // 此范围内的代码与扩展互斥
        do {
            let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
            // 处理数据
            try FileManager.default.removeItem(at: fileURL)
        } catch {
            print("Unable to read/cleanup file data. \(error)")
        }
    }
}

关键点:

  • withExclusiveControl 确保应用和扩展不会同时执行关键代码
  • 如果一方已获取独占控制,另一方会等待
  • 获取独占控制可能失败,需要处理 error 情况
  • 扩展终止时会自动释放独占控制

核心启发

  • 游戏资源预加载:在应用安装后自动下载高清纹理包、关卡数据。用户在首次打开游戏时直接进入主界面,无需等待。入口 API:applicationDidInstall 中调度 BAURLDownload

  • 离线内容缓存:新闻或阅读类应用在后台定期下载最新文章、图片和视频。用户打开应用时内容已就绪。入口 API:checkForUpdates 中检查服务器版本号,调度增量下载。

  • AI 模型动态更新:机器学习应用在后台下载更新的 Core ML 模型文件。用户始终使用最新模型,无需手动更新应用。入口 API:扩展中下载 .mlmodelc 文件,应用启动后加载到 MLModel

  • 地图数据包管理:导航应用按城市或区域下载离线地图包。首次安装时下载用户所在城市的基础包,后续根据行程自动下载途经城市的地图。入口 API:applicationDidInstall + checkForUpdates 组合使用。

  • 视频课程预缓存:教育类应用在夜间自动下载用户订阅的下一节课的视频文件。第二天用户打开应用即可离线观看。入口 API:结合用户学习进度数据,在扩展中判断需要下载的课程 ID。

关联 Session

评论

GitHub Issues · utterances