大多数浏览器和
Developer App 均支持流媒体播放。
-
制作超快列表和精选集视图
构建始终平滑滚动的列表和精选集视图:探索单元格生命周期,学习如何应用这种知识去除粗糙滚动和丢帧。我们还将向您展示如何通过优化图像加载和自动单元格预取改进整体滚动体验,避免代价高昂的故障。为了充分理解本视频内容,我们建议观众要基本熟悉 diffable 数据源和组合布局。
资源
相关视频
WWDC21
WWDC20
Tech Talks
WWDC16
-
下载
♪ ♪ 大家好 我是阿迪蒂亚克里什那德文 我是UIKit团队的工程师 列表或集合视图是许多app的核心 为了提供绝佳的使用体验 能够顺畅滚动屏幕 是非常重要的 这个视频将引导你 设计出滚动起来 迅速又顺畅的列表与集合视图 让我们用这个app来示范 这个app用集合视图来展示 诱人旅游景点的图像列表 看起来似乎挺简单 一张景点的照片 加上两个标文 这个视频会教大家设置的方法 以及要如何达成人们想要的高性能 首先 我们要来学习怎样 稳扎稳打地使用 “局部更新数据源”和 “单元格注册”这两个应用程序接口 我们会先重温集合视图单元格的 运作周期 接着说明为什么 有时滚动的动作不顺畅 以及预取的尖端设定 将如何避免这种情况发生 最后 帕特里克会说明 当单元格的内容为异步导入时 要怎样正确更新单元格 以及如何用新的UIImage 应用程序接口 将各种装置的滚动都提升到 完美的最佳性能 好 我们先来看看 这个app的数据是如何组成的 这个范例app检索了一列供显示的图 每张图都由 这个DestinationPost结构表示 DestinationPost符合identifiable 意思是它有着 可以储存标识符的ID属性 每个DestinationPost 都有专属的标识符 即使其他属性改变 此标识符不变 局部更新数据源是用来储存 模式中项目的标识符 而不是模型对象本身 因此 这个范例app的局部 更新数据源 是通过ID属性来将数据输入的 而不是通过DestinationPost 这是app使用的局部更新数据源 我们刚刚说过 它用DestinationPost.ID类型 作为项目的标识符 这里的区段类别是同一例的枚举 因为这个app只有一个区段 为了将数据源输入 app会先建立一个空白的快照 然后修改主要的区段 接着 它会从后备储存撷取所有的图 然后修改它们的标识符 如此一来 如果DestinationPost中的其他属性 改变了 在局部更新数据源里的表示法 仍保持不变 因为标识符并未改变 最后的步骤 是将快照应用到指定数据源 在iOS 15之前 应用没有动图的快照 会被内部转译成reloadData 这对性能会有负面影响 因为集合视图必须丢弃 所有屏幕上的单元格 再重新把它们全部建立起来 iOS 15发布之后 应用没有动图的快照 只会触及到差异处 不会多做别的 在iOS 15的环境下 局部更新数据源还多了 新的reconfigureItems方法 让更新显示的单元格内容 变得更容易 我们之后会在视频中 详细介绍它的功能 首先 我们把数据从数据源 导入单元格 显示于屏幕上 单元格注册是非常好用的设定 将同类别单元格的设定全部归于一处 让我们可以便捷地 从局部更新数据源存取标识符 UICollectionView会将 每个用过的注册 都保存在重复使用队列里 您只需要为各类别的单元格 建立一次的注册就够了 我们来看一下 简化的app单元格注册 传递进来的postID 用来检索DestinationPost 以及含带图像的涵盖数据对象 DestinationPost的属性则用来 设置单元格的标题和图像 要使用注册 须在数据源的单元格提供者里面 调用dequeueConfiguredReusableCell 请注意 注册要先在 单元格提供者之外创建 然后再用在提供者里面 这点对性能表象很重要 要是先在提供者里创建注册 集合视图就无法重复使用这些单元格 大家应该都明白单元格的设定了 我们现在来讨论单元格要在何时设定 还有单元格的运作周期是怎么回事 单元格的运作周期有两个阶段 准备阶段和显示阶段 准备的第一个步骤 是撷取将要设定的单元格 UICollectionView需要单元格的时候 会从数据源里提取 如果是可局部更新的数据源 它就会执行单元格的提供者 并回传结果 一旦单元格的提供者开始执行 集合视图就会 利用注册 将新的单元格出列 如果单元格是在重复使用池里 UICollectionView则会 调用prepareForReuse 将这个单元格出列 如果重复使用池是空的 它就会初始化一个新单元格 这个单元格接着会从注册 传递到设定的处理程序 app就是在这时将显示的单元格的 项目标识符和索引路径建立好 设定好的单元格会被返回集合视图 准备进行下个步骤
集合视图查询单元格 找出想要的配置属性 并调整单元格的大小 这时 单元格的准备已完成 可以进入下一个阶段:显示 willDisplayCell被调用到委托 单元格即在UICollectionView中显示 此时单元格出现在屏幕上 单元格如果继续显示 它的运作周期就不会变 当单元格滚动出屏幕 didEndDisplaying被调用 单元格就会立刻返回重复使用池 单元格可以再从重复使用池中出列 重复这整个过程 我们来看看当这些基本步骤完成后 app使用起来是什么感觉 这个app特写秘鲁的库斯科 和加勒比海的圣卢西亚 我们往下滚动app 查看其他的旅游景点 但是你看 滚动并不顺畅
这种滚动的停滞现象叫做“卡顿” 要了解造成卡顿的原因 我们得先来看看app 是如何更新显示的 每个图帧都一样 当屏幕有碰触的事件发生 就会传送给app app接着会更新视图和各层的属性 譬如 滚动视图的contentOffset 会在拖曳手势做出时改变 变更所有视图在屏幕上的位置 如此变更之后 app的视图和层就会做出配置 这个过程称为“提交” 接着 层的树状图会送往算图服务器 每个图帧都有提交期限 也就是同图帧中所有的提交 都必须完成的时间点 app图帧提交所需的时间 取决于显示的刷新速率 譬如 和60赫兹的iPhone相比 有着120赫兹 较高刷新速率的iPad Pro 它的app完成图帧提交 需要的时间会比较短 我们来看一下滚动集合视图 或表格的单元格时 很常看到的一个情况 当新单元格显示时 提交时间就会变长 这时这个新单元格会被设定 并开始配置 再来 有两个图帧 它们就是已经显示在屏幕上 正被滑动的单元格 这两个图帧的提交时间很短 因为这时并不需要新单元格 滚动幅度一旦变得够大 新单元格就会显示 这个模式会反复进行 那 是什么造成 刚才示范过的卡顿呢? 当图帧的提交费时过久 没能在期限前完成 更新的部分就无法整合入图帧中 显示状态将使得 前个图帧继续留在屏幕上 直到提交完成 被延迟的图帧要到这时才能算图 这就是提交卡顿 滚动时因此有短暂的停滞感
若想要了解更多相关信息 以及其他类型的卡顿 请观看 “Expore UI aimation hitches”视频 为了避免卡顿发生 UICollectionView和UITableView 在iOS 15里都有 全新的单元格预取机制
我们现在回头看这个耗时过多 导致滚动卡顿的单元格 这里有个关键观念 一般而言 不是每个图帧 都需要一个单元格 有两个图帧的提交时间很短 运作量相当少 iOS 15的单元格预取 正是利用这多出来的时间 在短暂的提交结束之后 马上开始准备下一个新单元格
然后 当终于需要单元格时 直接显示它就行了 这就是为什么预取的单元格显示时 图帧提交得这么快 因为该做的都预先做好了 预取单元格所需的时间 和造成卡顿的时间差是一样的 但因为我们可以预先处理 卡顿也就不会发生了 为了弄清楚其间的过程 让我们逐个检视这些提交 进行预取前 我们提交了这个图帧 因为不需要单元格 提交很快就结束了 离期限还有一大段时间 iOS 15系统并不是坐等下个图帧 而是辨识到这个情况 并利用多余的时间 开始进行下一个单元格的预取 接下来的这个图帧 情况就复杂些了 因为被预取的单元格耗时过多 它其实导致这个图帧的提交 比预期时间要晚才开始进行 不过 即使提交过晚开始 因为所需时间很短 它仍然可以远在期限前就结束 和我们之前看到的 没有进行预取的图解比较 就会发现这时不再有提交 是超过期限的 因此 做好单元格预取后 卡顿也就不会发生了 这也是说 你的app可以有多达两倍的时间 准备单元格 绝对不会导致卡顿 不仅如此 为享有这个绝佳的新功能 你唯一需要做的事就是 用iOS 15软件开发工具包来构建app 上回我示范的时候 app是用iOS 14的软件开发工具包构建的 让我们来看看用iOS 15的软件开发工具包 构建的app滚动起来会是什么样子
这真是太棒了! 看来预取的设定 完美达成了我们想要的目标 滚动做起来顺畅无比 而我们连改动一个程序代码都不需要 记住 你唯一需要做的 就是用iOS 15的软件开发工具包来构建app UICollectionView的新预取机制扩充了 iOS 10引入的设置 单元格预取如今支持清单 也支持所有组合式配置 这个全新的预取机制 就是在UITableView里也能使用 预取可以免除卡顿并改善滚动的性能 也能降低电量使用 增加电池寿命 如果单元格可以很快准备好 多出来的时间就能让系统 用更节能的方式运作 而依旧避免卡顿 就算你没看到卡顿 让你的单元格设定和配置运作 保持最高效率 依然是非常重要的 现在让我们来看看预取 如何影响单元格的运作周期 这是我们之前看过的运作周期 没有预取 两个阶段界限分明 单元格预取是在准备阶段进行的 远在单元格需要出现在屏幕之前 为了让预取发挥最大功能 单元格一定要在这个阶段完全设定好 不要等到单元格显示了才加紧工作 当单元格返回集合视图 大小会做调整 取得所需的配置属性 这也是预取的一项功能 预取后 会出现这个 介于两阶段间的状态 单元格即在这里等候进入显示 面对这个新阶段 app面临两个状况 若是使用者突然改变滚动的方向 准备好的单元格有可能不会显示 还有 单元格显示之后 可能滚动出屏幕 立刻回到等待状态 同一个单元格可以不止一次 通过同样的索引路径来显示 我们不再需要将结束显示的单元格 立即放进重复使用池 预取让滚动做起来顺畅 原因无非是它可以省下更多时间 在换帧速率较高的装置上 app仍然可能会在滚动时产生卡顿 帕特里克接下来会进一步说明 app如何设定单元格 他也会介绍一些做法 来减少显示图像时 每次提交所需的时间 谢啦 阿迪蒂亚 大家好 我是帕特里克 来自High-Level Performance团队 我将教导大家如何 将同一个范例app里 现有的单元格更新 以及如何用iOS 15的一些 最新应用程序接口 以最高的性能 显示图像 范例app以装置磁盘中的图像构建 滚动app时 单元格会在进入屏幕前准备 其中的图像会直接 从文件系统中加载 现在 我们要做的是显示 储存在远距服务器的图像 当单元格滚动入屏幕时 图像有可能不会显示在视图里 当视图刚开始显示时 里头是空的 只有在服务器的存取请求完成后 图像才会填入 让我们试着扩展 注册的设定处理程序 来支持这个新的做法 这里我们可以看到 在这个注册的设定处理程序里 涵盖资料已经 从涵盖数据库撷取过来了 涵盖数据库一定都会将图像返回 但是图像的涵盖数据可能变得不完整 而需要下载 涵盖数据对象通过isPlaceholder属性 将此指出 这种情形发生的时候 我们可以要涵盖数据库下载完整图像 加载操作完成后 我们就可以更新单元格的视图了 这里 我们用现有的单元格对象 设定视图的涵盖数据 这个做法是错的 单元格会按照不同的旅游景点 而被重复使用 而当涵盖数据库 加载最后的涵盖数据时 我们获取的单元格对象 可能已经被设定到不同的图了 我们该做的不是直接更新单元格 而是告知集合视图的数据源 需要更新的部分是哪些 iOS 15引入 reconfigureItems快照方法 在准备好的单元格里 调用reconfigureItems 将会重新执行注册的设定处理程序 要用reconfigureItems 而不是reloadItemsf 因为它重复使用 这个项目的现有单元格 而不是将新单元格出列并做设定 在这个范例app里 我们将宣告setPostNeedsUpdate方法 然后调用reconfigureItems 在传递进来的ID上
现在 回头看注册的设定处理程序 当图像是占用符时 我们下载完整的涵盖数据 然后调用这个新方法 reconfigureItems就会 再次调用这个处理程序 不过现在呢 fetchByID 返回的会是完整涵盖数据 而不是占用符 这么做可以将所有的视图更新程序代码 归在同一处 一旦有了数据 就可以异步更新单元格 若想充分利用准备时间 我们也可以将downloadAsset方法 用在prefetchingDataSource里 进行集合视图项目的网络下载 从数据源预取开始就对了 它能让你在单元格显示前 有更多时间下载涵盖数据 减少使用者看到占用符内容的时间
我们来看看这个app的运作 还不错 可是滚动时有明显的卡顿 卡顿似乎与 新图像进入屏幕时同时发生 新单元格要是准备好了 卡顿是不会发生的 只有当全分辨率的图像被更新时 卡顿才会发生 这是因为将图像的显示解码需要时间 而有些图像 譬如那些较大、非占用符的涵盖数据 会因为档案过大而无法实时解码 当单元格注册的设定处理程序 最初被调用 而其涵盖数据是占用符时 它的程序代码就会异步请求 加载完整图像 以完成设定 之后当涵盖数据终于下载好了 单元格设定处理程序就会用 最新的图像重新执行 当视图要提交新的图像时 得先把即将显示的图像 在主线程上准备好 这可能花上很多时间 要是app错失了提交期限 卡顿就会产生 准备图像是必要的过程 所有的图像都需准备好才能显示
算图服务器 只能显示位图图像 也就是只有原始像素数据的图像 图像的格式分很多种 譬如PNG、HEIC和JPEG 它们都经过压缩 必须在处理、拆解之后 才能显示 视图要提交新的图像时 会先进行这个程序 且是在主线程上进行 按理来说 我们可以预先准备好图像 完成后 才在界面上更新 这么一来 主线程永远不会受阻 也不会产生卡顿 iOS 15引入 准备图像的应用程序接口 让你可以全面掌控 图像准备在哪里进行、何时进行 这些应用程序接口产生的新UIImage 只包含算图器所需的 像素数据 视图一旦设置好了 就不需要再做其他的操作 这种接口有两种形式 一种是同步的 可在任何线程上执行 另一种是异步的 在内部的UIKit的连续队列上执行
当使用这个应用程序接口时 要用我们创建出来的UTImage 在视图里设一个占用符图像 然后 调用这个新应用程序接口 在较大的图像的背后 开始进行准备 完成后 我们可以直接设在视图上 事先准备好的图像 为所有以图像为主的app 解决了一个大难题 但是还有需要注意的地方 准备好的图像包含 原初图像的原始像素数据 只要仍然存于内存中 图像可随时在视图中显示 但这也表示它占用了很多的内存 必须小心节制地存于缓存里 最后 由于形式的关系 它们并不适合用磁盘储存 应储存于磁盘的是原本的涵盖数据 最后一点要注意的是 图像准备可以运用预取 预取让图像 有多余的时间下载和准备 这过程若是有充裕时间来操作 使用者看到占用符的时间就短 也可能根本不会看到 在范例app里 我们已经有异步的图像检索路径 当下载完成之后 我们就可以在调入完成处理程序前 准备好涵盖资料 这些涵盖数据很庞大 但是也很有用 所以一旦图像准备好了 我们就可以将它们缓存 图像缓存使用图像的大小 来评估准备好的图像所占的内存量 当单元格要求涵盖数据时 我们要在从服务器那里撷取过来之前 先去检查缓存 如果图像比较小 我们就可以多缓存些 图像可能很大 所以iOS 15引入一种 功能相似的应用程序接口 来准备图像的缩图 缩图可以调整图像 让需准备的图像变小 它确保图像按照想要的大小 被读取和处理 因此省下许多中央处理器 运作的时间和内存空间
使用的方式 和Image Preparation接口一样 首先 用一个UIImage 在视图里设占用符图像 然后 以视图的大小 来作为缩图的预设大小 以调用新的调整大小的应用程序接口
当准备好之后直接用新的缩图 来更新视图 和Image Preparation接口一同使用 iOS 15的app 让视图的加快处理变得更加容易 也更能有效避免卡顿 在处理图像时 使用的异步应用程序接口 必须能在图像准备好时就更新界面 同时 使用的占用符图像 必须够小 才能达到同步显示 若和预取及reconfigureItems一起使用 显示集合视图以及列表的异步内容 都变得异常简单 性能也再好不过了
快速建好集合视图和表格的方法如下 一 用iOS 15的软件开发工具包 来创建你的app 你会发现多种最新的性能优化 很重要的是 请务必利用 我们最新的预取功能 来有效化 你的集合视图和表格的表现 所有我们示范过的应用程序接口 都可以在这个视频里的 示例程序代码里找到 赶快试一试 也务必将准备图像 和调整大小的应用程序接口 运用在你的app里 这会让你的集合视图 和表格操作起来快如闪电 谢谢大家观看这个视频 [欢快音乐]
-
-
1:25 - Structuring data
// Structuring data struct DestinationPost: Identifiable { // Each post has a unique identifier var id: String var title: String var numberOfLikes: Int var assetID: Asset.ID }
-
2:01 - Setting up diffable data source
// Setting up diffable data source class DestinationGridViewController: UIViewController { // Use DestinationPost.ID as the item identifier var dataSource: UICollectionViewDiffableDataSource<Section, DestinationPost.ID> private func setInitialData() { var snapshot = NSDiffableDataSourceSnapshot<Section, DestinationPost.ID>() // Only one section in this collection view, identified by Section.main snapshot.appendSections([.main]) // Get identifiers of all destination posts in our model and add to initial snapshot let itemIdentifiers = postStore.allPosts.map { $0.id } snapshot.appendItems(itemIdentifiers) dataSource.apply(snapshot, animatingDifferences: false) } }
-
3:47 - Creating cell registrations
// Cell registrations let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) cell.titleView.text = post.region cell.imageView.image = asset.image }
-
4:03 - Using cell registrations
// Cell registrations let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in ... } let dataSource = UICollectionViewDiffableDataSource<Section.ID, DestinationPost.ID>(collectionView: cv){ (collectionView, indexPath, postID) in return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: postID) }
-
13:58 - Existing cell registration
// Existing cell registration let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) cell.titleView.text = post.region cell.imageView.image = asset.image }
-
14:17 - Updating cells asynchronously (wrong)
// Updating cells asynchronously let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) if asset.isPlaceholder { self.assetsStore.downloadAsset(post.assetID) { asset in cell.imageView.image = asset.image } } cell.titleView.text = post.region cell.imageView.image = asset.image }
-
15:15 - Reconfiguring items
private func setPostNeedsUpdate(id: DestinationPost.ID) { var snapshot = dataSource.snapshot() snapshot.reconfigureItems([id]) dataSource.apply(snapshot, animatingDifferences: true) }
-
15:23 - Updating cells asynchronously (correct)
// Updating cells asynchronously let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) if asset.isPlaceholder { self.assetsStore.downloadAsset(post.assetID) { _ in self.setPostNeedsUpdate(id: post.id) } } cell.titleView.text = post.region cell.imageView.image = asset.image }
-
15:52 - Data source prefetching
// Data source prefetching var prefetchingIndexPaths: [IndexPath: Cancellable] func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths [IndexPath]) { // Begin download work for indexPath in indexPaths { guard let post = fetchPost(at: indexPath) else { continue } prefetchingIndexPaths[indexPath] = assetsStore.loadAssetByID(post.assetID) } } func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { // Stop fetching for indexPath in indexPaths { prefetchingIndexPaths[indexPath]?.cancel() } }
-
18:43 - Using prepareForDisplay
// Using prepareForDisplay // Initialize the full image let fullImage = UIImage() // Set a placeholder before preparation imageView.image = placeholderImage // Prepare the full image fullImage.prepareForDisplay { preparedImage in DispatchQueue.main.async { self.imageView.image = preparedImage } }
-
19:51 - Asset downloading without image preparation
// Asset downloading – before image preparation func downloadAsset(_ id: Asset.ID, completionHandler: @escaping (Asset) -> Void) -> Cancellable { return fetchAssetFromServer(assetID: id) { asset in DispatchQueue.main.async { completionHandler(asset) } } }
-
19:58 - Asset downloading with image preparation
// Asset downloading – with image preparation func downloadAsset(_ id: Asset.ID, completionHandler: @escaping (Asset) -> Void) -> Cancellable { // Check for an already prepared image if let preparedAsset = imageCache.fetchByID(id) { completionHandler(preparedAsset) return AnyCancellable {} } return fetchAssetFromServer(assetID: id) { asset in asset.image.prepareForDisplay { preparedImage in // Store the image in the cache. self.imageCache.add(asset: asset.withImage(preparedImage!)) DispatchQueue.main.async { completionHandler(asset) } } } }
-
20:50 - Using prepareThumbnail
// Using prepareThumbnail // Initialize the full image let profileImage = UIImage(...) // Set a placeholder before preparation posterAvatarView.image = placeholderImage // Prepare the image profileImage.prepareThumbnail(of: posterAvatarView.bounds.size) { thumbnailImage in DispatchQueue.main.async { self.posterAvatarView.image = thumbnailImage } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。