大多数浏览器和
Developer App 均支持流媒体播放。
-
后台素材简介
了解如何利用后台素材框架直接从您的 CDN 中下载大型文件,并优化您的 App 和游戏的初次启动体验。我们将介绍如何制定计划,以便在初次安装 App、App 更新或使用 App 期间定期完成后台下载。我们还将探索如何管理下载计划,以确保用户在需要时获得他们想要的内容。
资源
相关视频
Tech Talks
WWDC23
WWDC21
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ 嗨 我是 Jared 我是 Apple 的一名 软件工程师 今天我想向您介绍 我们今年将为 iOS iPadOS 和 macOS 推出的一个新框架 这个新框架叫做 Background Assets 我们相信它将极大地丰富 我们了解和喜爱的 App 的用户体验 更重要的是 它也将丰富您的开发体验 首先 我将向您介绍 新的 Background Assets 框架 之后 我将向您展示 如何将这个新框架 应用到您的 App 中 接下来是关于扩展和它提供的 新功能的快速概览 最后我们将探讨一些最佳实践 并对学到的一切进行总结 在开始之前 先来谈一谈 我们试图解决的问题 事实上 等待并不是一件有趣的事 每当我们要求软件使用者等待时 我们都在增加用户的消极性并折损 我们期待 App 可以提供的体验 比如说 您是不是经常发现自己 在无休止地 浏览 App Store 寻找完美的 App 当您终于找到时 不禁感叹 哦 它看起来如此完美 然后 您轻点“获取”按钮 每一刻 您的兴奋程度都在增加 然后您很快意识到 取决于 您的网络连接状况 或 App 的大小 您不得不等待 App 下载 等了几秒之后 您发现自己已经放下手机 端起咖啡 开始读您最喜欢的一本 关于练习正念和掌握耐心 会带来什么好处的书 然后几个小时过去了 您终于拿起了手机 当您准备好好研究等了一整天的 完美 App 时 您变得更加兴奋了 然而刚一启动 App 就收到了这句欢迎信息 更多下载 这很令人困惑 您已经一整天没用手机了 为什么现在这个 App 还要让您等待更长时间? 难道这个 App 就不能 在安装之后 自动下载这些内容吗? 任何网络连接速度较慢的用户 都可能因为这种消极体验 而关闭或删除 App 事实是 这不是我们任何人 希望经历的事 我们知道这不是用户的错 我们也相信可以大幅改善这种体验 这就是我们今年自豪地向您介绍 Background Assets 的原因 开发这个框架是为了 帮助您丰富 App 的用户体验 让您能在 App 启动的那一刻 为用户提供惊艳的第一印象 Background Assets 可以在您 现有的工作流上灵活使用 很多开发者已经开发了 复杂的资产管理系统 我们希望这个新框架能够轻松融入 您已经开发的解决方案 我们也知道您希望能够推送更新内容 到您的 App 而无需额外提交 到 App Store 对于游戏或其他 App 来说 在 App 发布后需要 额外内容的情况很常见 可以想想更新的艺术纹理 或游戏关卡数据中的错误修复 Background Assets 让您可以在 App 生命周期 之外对资产进行排期和更新 我们认为 App 在首次启动 或临时更新之后 资产的就绪非常重要 因此 我们创建了一种机制来帮助您 确保内容 在 App 启动时已经就绪 最后 框架越容易使用 我们就越能鼓励您 在 App 中使用它 我们希望 Background Assets 可以用在任何 需要预下载大型资产的地方 这样一来 我们就可以最大限度地 减少您 App 的等待时间 并在其内容可用之前显示进度条 您可能想知道 这个新框架会如何 帮助您解决这个问题? 为了使可扩展性尽可能提升 我们创建了一个新的 App 扩展 用于在后台下载内容 这个新扩展是在 其他扩展在我们平台上使用的 强大的 App 扩展技术上构建的 这提供了在 App 生命周期之外 运行代码的机会 比如 扩展将在用户 首次安装您的 App 但尚未启动它时运行 每当 App 更新时 该扩展也会 自动在后台运行 这有助于确保在用户 打开更新后的 App 之前 安排和下载您的内容 最后 扩展将在后台定期运行 让您能够检查更新的资产 并随着时间的推移定期进行安排 要注意的重要一点是 该扩展的运行时是短暂的 因此所有工作都需要由您的扩展 快速规划 如果没有快速为下载排期 系统可能会终止该扩展的运行 还有一点也很重要 扩展定期运行的能力 将根据 App 的使用情况 出现减退 如果您的 App 不是经常使用 那么扩展将获得较低频率的运行时间 以上就是对新的 Background Assets 框架的概况介绍 它将为您提供所需的工具 以确保 您的资产在 App 启动时可用 这是通过在每次 您的 App 进行安装或更新 但用户尚未启动之前 运行的扩展来完成的 现在 让我们来看看如何 将 Background Assets 框架 采用到您的项目中 并开始使用吧 框架内的下载管理器 是用来和 Background Assets 系统服务 交流的主要工具 管理器是一个单件对象 可以在整个 App 中使用 使用管理器 您可以规划 资产在前台或后台的下载 您还可以检索当前处于进程中的下载 它们可能在您的 App 启动之前 就已经开始了 下载也可以取消 如果下载已经安排好 或正在下载中 而您不再需要最初 请求的资产时 这就非常有用 我们还引入了同步机制 用于管理您的 App 和扩展的互斥访问 这样扩展和 App 就不会出现 同时安排或修改现有下载的情况 我为您准备了一个案例 但我们稍后再详细看 让我们先来看看 开始使用 Background Assets 是多么简单 我将首先向您介绍一些 API 的基础操作 之后我将进一步展示如何将 这些都集成到一个 App 扩展中 首先 您需要导入后台资源 框架模块 然后 就像定义一个指向您 远程资产位置的 URL 一样简单 接下来 我们定义一个 App Group 容器 您的扩展和 App 都是其中的成员 将您的 App 和扩展 放在同一个组中 将使它们可以在 下载期间和下载完成后 管理您的资产 如果您还不熟悉 App Group 的话 它可以从 Xcode 14 的 “Signing & Capability”板块 进行添加 这项强大的功能 允许两个或多个 App 访问相同的资源 在我们的例子中 就是您的 App 及其扩展 接下来要做的是创建下载对象 Background Assets 框架的目的在于支持 多种不同类型的下载对象 不过 在这个例子中 我们将关注最常见的一种 BAURLDownload 您立刻就会注意到 初始化程序需要 URL 和 App Group 标识符 这项信息告知了系统 我们正在下载的内容 以及生成的文件将出现在哪里 它还需要一个标识符 您将使用此标识符在 App 的 多次启动以及在扩展中追踪下载 引擎不允许多次下载 使用相同的标识符进行排期 因此 您应当创建唯一标识符 接下来 我们将获取对共享对象 BADownloaderManager 的引用 下载管理器是您进入 Background Assets 的 单一界面 它让您可以观察、取消和规划下载 然后我们将弱引用传递给一个 符合 BADownloadManagerDelegate 协议的 委托 稍后我将详细介绍此协议 但现在需要知道的最重要的一点是 它的用途是 接收已排期下载的信息 最后要做的就是要求下载管理器 进行下载排期 如果出于某种原因无法为下载排期 就会抛出错误 除了在后台安排下载之外 我们还提供了 用于前台下载的 API 在前台运行 不仅为您提供更高的优先级 也可以让您立即开始下载 这类似于 使用 URLSession 中的 默认会话配置 我们提供的 API 可以让您的 App 将在后台安排的任何下载 推送到前台 需要记住的是 不可以从扩展内执行 前台下载 它只能从 App 启动 由于扩展从不呈现 UI 用户也注意不到它们正在运行 扩展只能安排后台的下载 如果您的 App 想要将 现有的后台下载推送到前台 可以通过从管理器提取 当前下载列表轻松完成 返回的列表包含所有当前 已排期的下载 其中可能包括 运行中或在调度器中排队的下载 接下来 您的 App 可以通过调用 startForegroundDownload 开始推送 如果下载已经在前台 调用这个方法实际上不执行任何操作 但是 如果下载在后台 它将首先暂停 然后在前台恢复 不需要请求任何已经下载的内容 进行重新下载 这些都有效而简洁地表明了 使用 Background Assets 将安排在 后台的下载推送到前台 是多么容易 它真的就是那么简单 下载管理器是您进行下载安排 和监控时使用的主要界面 由于这些下载对象由系统处理 您将在委托对象中收到信息 现在让我们来看一看委托 委托负责接收已由扩展 或您的 App 安排的 所有下载的信息 如果有大量已排期下载 您将收到它们所有的回调 这就是您使用下载对象 唯一标识符来区分它们的地方 委托在 BADownloadManager 上 创建的那一刻起 您的 App 就将开始接收回调 系统不会将回调放入队列 如果您的 App 不处理委托方法 或者您没有设立委托 您的扩展将被唤醒来处理信息 这意味着如果您尚未在 App 的 BADownloadManager 上设立委托 则应该预期信息将发送到您的扩展上 如果您的 App 正处于前台 呈现给用户 并且已经设立了委托 那么回调就将发送到您的 App 并且扩展不会被唤醒 扩展只有在 App 不处理 其委托回调时才会被唤醒 如果下载完成 或下载失败 且 App 不处理此信息 这时扩展将被唤醒 请记住 不是所有类型的回调 都能唤醒扩展 仅适用于共享 BADownloadManagerDelegate 和 BADownloaderExtension 协议 之间公共接口的回调 下载成功或失败就是一个 委托和协议之间公共接口的例子 尽管您的 App 扩展自己有 唤醒的入口点 如果扩展当前正在运行 它可以使用 BADownloadManager 并建立一个委托 这将允许 App 和扩展 接收重复的信息到委托 请记住 扩展不会被唤醒 以处理委托信息 它们只在 BADownloaderExtension 协议 定义的扩展入口点被唤醒 让我们来看一看用于 下载管理器委托的协议 第一个函数用于每次下载开始时 接收信息 这在追踪设备最终选择何时 安排特定的下载时很有用 如果下载暂停 您也会收到通知 发生暂停的一种情况是 当扩展在后台开始下载 但随后您的 App 要求将其推送到前台 在此推送期间将有一小段时间 下载会在恢复之前暂停 下载管理器还允许您监控 前台下载时的 操作进度 我们还提供了一种机制 来回应质询请求 这在验证连接真实性 或提供凭据以授权连接时非常有用 它最重要的功能是处理下载失败 或下载成功 如果下载失败 您可能需要重新排期 或确定原因 如果下载成功 系统会将文件放在一个 由操作系统管理的位置 如果设备空间不足 系统就会替您删除文件 我们强烈建议您将文件留在 系统提供的位置 仅在绝对必要时才移动文件 而且 请不要复制它 除非您之后会将原始文件删除 再提醒一下 下载管理器委托的协议 是用来接受您的 App 或扩展已经安排的 下载相关的信息 它不是您扩展的入口点 这就让我们可以开始讨论下一个主题 现在就让我们来看看 Background Assets 最激动人心的部分 扩展 这个扩展使您 能够在用户启动 App 之前安排资产下载 使您能够确保资产到位 并准备好在最短的 等待时间里提供 最佳的 App 使用体验 正如之前所谈到的 我们引入了新的 App 扩展 它可以从您已有的项目的 Xcode 中进行创建 提醒一下 每当您的 App 安装 或更新时 扩展就会运行 让您可以灵活地确保 针对您 App 的更改 始终拥有最新的资产 该扩展还将基于用户 使用您 App 的频率定期运行 如果用户每天都使用您的 App 系统就会学习这种行为 您的扩展也将更频繁地运行 但是 如果 App 从未启动过 这种定期检查的频率就会降低 新扩展较短的生命周期 和紧凑的沙箱 确保它的使用被限制 为仅供下载资产 我们也鼓励您在扩展中快速做出决定 并将扩展限制为 仅供 Background Assets 框架使用 在我们开始浏览扩展之前 还有几项配置是 您需要在扩展启动之前完成的 这些更改也是让您的 App 获准在 App Store 上分发的要求 在您 App 的信息属性列表中 您需要定义几个额外的键值 这些键值不应该放在 扩展的 Info.plist 中 只放在 App 中 第一个键值是 BAInitialDownloadRestrictions 这是一个字典 您将在其中指定 将用于扩展的限制 这些限制将由 App Review 进行审核 所以请做到尽可能准确 现在 让我们深入研究字典里 每个单独的键值 第一个限制是下载限额 以字节表示 表示您要求在 最初安装 App 时 在扩展中进行的 最大下载量 此大小指的是您请求下载的 所有文件总和 而不是每个单独文件的大小 下一项是域 AllowList 它将一系列域表示为字符串 域 AllowList 支持前缀通配符并 要求允许您的扩展执行下载的 主机名列表 需要注意的重点是 BAInitialDownloadRestrictions 中的键值 例如 DownloadAllowance 和 AllowList 仅在 第一次安装 App 后执行 在您的 App 启动之后 这些限制将不再执行 最后一个必需的键值 位于 Info.plist 的根目录 是您的 App 需要的 最大资产额外存储空间 我们设想您可能会下载压缩资产 所以这个值应该是最终提取的 未压缩的资产大小 填在这里的数字 将在 App 下载之前 展示在 App Store 中 现在我们已经完成了琐碎的筹备工作 让我们来谈谈更详细地谈一谈 扩展的入口点 您从协议中定义的函数 将由系统 而不是您的 App 进行调用 与其他 App 扩展中 App 负责 与扩展交流不同 后台下载扩展 由系统代理 由于系统负责维护扩展的生命周期 它应该被视为一种临时服务 每当任何协议内的 函数被调用 将放在那里完成的工作量 降到最低限度很重要 扩展启动后将 很快终止 这里不适合进行解压缩 或其他可能需要一段时间的复杂操作 在扩展中进行操作的一大优点是 所有可用于您 App 的 BackgroundAssets API 也可以在扩展中使用 唯一的例外是 ForegroundDownload API 这意味着您将可以 像在您的 App 中一样 使用 BADownloadManager 事实上 您完全有可能 发掘一些创建功能 在您的 App 和扩展中 使用相同代码 来安排和管理您的资产 此外 在创建扩展时 确保两者处于 同一个 App Group 很重要 您需要使用相同的组标识符 使您的 App 及其扩展可以读取 和写入内容 现在让我们来看看您的扩展将要 符合的下载器扩展协议 您会注意到的第一件事是 它与下载管理器委托协议 看起来是多么相似 正如我之前所说 您可以 使用 BADownloadManager 并从扩展中构建一个委托 但是 只有这些入口点才能唤醒扩展 第一个函数被调用是在 每次您的 App 被首次安装时 App 尚未启动 但扩展已启动 这是一个绝佳的机会 您可以 开始安排下载 App 在启动时 为了带来最佳体验 所需要的资产 同样重要的是要记住 在第一次安装 App 期间 下载限制生效 您需要查阅在 Info.plist 中定义的 BADownloadRestrictions 键值 来了解最大允许下载量 和允许的域 下一个函数的调用 是在每次 App Store 更新您的 App 时 只要用户没有在 App 切换器中退出您的 App 您最近更新的扩展将被唤醒 您就可以开始对操作进行安排了 checkForUpdates 函数支持 由系统定期唤醒您的扩展 以便您可以检查需要 后台下载的任何更新 该函数由系统基于 用户使用您 App 的频率 进行调用 我们也支持响应 身份验证质询请求 以便您更好地限制和确保 正在下载的文件 来自可信赖的来源 最后 就像委托一样 下载失败或成功 您都会收到通知 您可以注意到在 backgroundDownloadDidFail 函数中 没有返回错误 您可以在返回的 BADownload 对象内 一个变量中检索错误及其状态 还有一点也很重要 请注意最后三个函数 即使在下载不由您的扩展安排时 也可以进行调用 如果您的 App 安排了下载 但已经放在后台运行 这时预期 由扩展为下载提供服务 现在我们已经了解 如何在 App 及其扩展中 使用 BADownloaderManager 我们必须开始考虑 这对 App 及扩展 同时运行的情况来说 意味着什么 例如 假设系统已经认为 是时候唤醒扩展 进行定期检查更新了 当然 因为扩展需要访问 网络来进行检查 它将使用 BADownloaderManager 来安排下载目录 或其他类型的元数据 它们将为可用的 更新资产提供列表 比如说 假设文件 是一个 100KB 的小目录 其中包含了我们需要下载的 数 GB 资产的列表 由于扩展需要知道 它安排的下载是成功了还是失败了 它会将一个委托附加到下载管理器上 下载管理器的委托 在其扩展入口点使用 因为它下载一个小文件来确定 需要安排哪些更大的资产 而扩展入口点并不保证 能立即被调用 下载完成后 扩展通过其委托接收到消息 您的扩展现在可以访问目录文件 并且必须做出将如何 处理下载文件的选择 您可以想象扩展会读取文件 确定目录中的哪些资产 需要下载到设备 然后扩展可以在后台安排 这些较大资产的下载 现在已经不再需要下载的文件 所以扩展应该删除它 虽然这看起来很合理 但如果扩展正在运行时 您的 App 启动 并创建自己的 BADownloadManager 情况会怎样呢? 就让我们来看一看吧 App 启动并立即想知道 是否有更新的内容 可能 App Group 中存储了版本编号 双方都进行查询 来确定是否有更新资产 由于该 App 在较新的目录 下载完成前启动 它将从管理器 提取当前下载 并意识到目录的下载 目前已在进程中 只需等待它在 代理中完成 但这里有一个问题 扩展和 App 都将在各自 连接到下载管理器的代理中 收到下载完成的消息 这意味着对正在 下载的文件存在数据竞争 App 和扩展 都会在同一时间会尝试 读取和删除文件 情况不妙 也就是说 您的 App 或扩展 尝试读取该文件时 它可能已经丢失 这意味着您应该将 App 和扩展 当作类似 App 中的两个线程来考虑 幸运的是 Background Assets 提供了一种 在您的 App 与其扩展之间进行同步的方法 现在就让我们来探讨一下 将您的 App 及其扩展进行同步 在使用 Background Assets 的情况下非常简单 我们现在看到是下载管理器中 下载完成时的委托函数 它提供了 包含文件本地路径的 URL 您的 App 或扩展有权限访问 在这个例子中 我们将确保 针对这个文件的互斥 接下来 我们获取下载管理器的引用 并使用 withExclusiveControl 函数 此函数要求一个完成处理器 在完成处理器范围内执行的所有代码 将确保与其他需要 独占控制的调用互斥 这意味着 如果您的扩展在 App 尚未从完成处理器返回时 调用 withExclusiveControl 扩展将会等待 反过来也一样 如果扩展首先获得独占控制 那么 App 就将等待 直到扩展终止 或通过退出作用范围释放控制 您需要记住的重点是 获得独占控制可能会失败 这种情况发生的可能性极小 但如果真的发生 您的代码应该对它进行处理 您可以通过检查函数提供的错误 不为 nil 来确定是否无法获得 独占控制权 从此时开始 您可以保证您的 App 或扩展在其 执行环境中拥有独占访问权 所以根据我们之前的例子来看 现在读取文件内容并根据您的选择 决定是否清理的操作是完全有效的 只需要注意 当您的其他 App 或扩展 获得进入独家控制范围的机会时 要知道您已经处理了该文件 实现的一种方法是首先检查 文件是否存在 是否写入数据库或 plist 提醒一下 后台下载器扩展 用于为您的 App 收集和安排 大型资产的下载 它的运行时间很短 所以请确保将通过此扩展进行的 操作减少到最低限度 您还应该将您的扩展和 App 放在共享的 App Group 中 以便两者都可以访问 对方下载的文件 最后 扩展由系统代理 而不是您的 App 现在您已经知道如何开发 基础的后台下载扩展 您就做好了将 Background Assets 添加到 您的 App 所需要的所有准备 现在 让我们回顾一下学到的东西 下载管理器用于协调和安排 您的 App 与其扩展之间的下载 因此您应该在这两处都 使用下载管理器 即使您的 App 不在前台 扩展也会运行 这可能发生在 App 安装和更新时 或以系统确定的间隔定期运行 如果您的 App 已经启动 后台正在下载的内容 现在需要等待 请立即将这些下载推送到前台 扩展只能在后台安排下载 通过让 App 将下载推送到前台 可以确保您的内容尽快到达 如果您发现需要为下载管理器设置 独占访问权限 请使用独占控制 API 这将确保只有您的 App 或扩展 在该区间内拥有运行时 这非常有用 它可以让您不必考虑 扩展在访问其容器或管理下载时 与您的 App 发生竞争 如果您可以从今天的 讲座中获得些什么启发的话 那就是 等待会导致糟糕的 App 体验 您可以通过使 App 在等待任务进行时 进行时保持可用 来将等待时间降到最低 减少您 App 等待时长的方法之一 就是采用全新的 Background Assets 框架 以及构成其基础的后台下载扩展 这有助于确保您的 App 在启动之前就已经准备好所有内容 请您确保查看开发者文档 其中还有许多今天的演讲 没能涵盖的信息 包括如何测试您的扩展 以及如何模拟它的入口点 我们真的很高兴能与您分享 Background Assets 我们也非常重视您的反馈 请使用反馈助理让我们知道 哪些内容对您有用 以及您希望我们做出哪些改进 这还是一个新框架 我们仍然有机会 在传播期间进行调整 还有其他几场 您可能会觉得有趣的讲座 我们也推荐您看一看 “使用 HTTP3 加速网络” 是一场精彩的讲座 与 Background Assets 的使用非常匹配 我还想请您查看另一个讲座 “介绍按需资源” 它介绍了 Background Assets 的一项替代方案 在此方案中 您的内容将由 Apple 托管 并根据您的要求下载文件 这两场讲座都非常吸引人 并且信息量充足 感谢您花时间听我的讲座 我谨代表 Apple 的每个人 祝您度过一届精彩的 WWDC ♪
-
-
5:28 - Getting started with Background Assets
// Getting started with Background Assets 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 // BADownloadManagerDelegate protocol // Schedule download at an opportunistic time determined by the system do { try manager.schedule(download) } catch { print("Failed to schedule download. \(error)") } // or Schedule download in foreground do { try manager.startForegroundDownload(download) } catch { print("Failed to start foreground download. \(error)") } // or Promote downloads to foreground. do { for download in try await manager.fetchCurrentDownloads) { try manager.startForegroundDownload(download) } } catch { print("Failed to promote downloads to foreground \(error)") }
-
10:28 - BADownloadManager delegate protocol
// BADownloadManager protocol definition 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) }
-
15:37 - BADownloaderExtension protocol
// BADownloaderExtension protocol definition 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() }
-
19:40 - Synchronizing between app and extension
// Synchronizing between app and extension 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 } // Exclusive control acquired // All code in this scope ensures mutual exclusion between extension and app do { let data = try Data(contentsOf: fileURL, options: .mappedIfSafe) // Do something with memory mapped data try FileManager.default.removeItem(at: fileURL) } catch { print("Unable to read/cleanup file data. \(error)") } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。