微信公众号:小武码码码
前面终于将跨平台、前端两类开发的复杂列表进行了分享,当然以后还会继续分享更加详细深入的。但现在,终于能作为一名iOS这个老本行的开发者,来讲讲在开发iOS App的过程中,可以说是最常见、最重要的组件之一的列表。
看似简单的列表,在实际开发中却往往包含了各种复杂的需求和优化挑战。今天,就让我来跟大家分享一下我这些年在iOS复杂列表开发方面的一些经验和思考。
一、iOS开发中的复杂列表样式及应用场景
在iOS开发中,列表组件主要对应的是UITableView和UICollectionView两个类。UITableView 主要用于展示一列纵向排列的列表,而UICollectionView则可以实现更加灵活多变的布局。通过这两个类,我们可以实现各种常见的复杂列表样式:
1. 分组列表
分组列表是最常见的一种复杂列表,它将列表项按照某种规则分成不同的组,每个组都有自己的头部和尾部。典型的应用场景包括通讯录、设置页面等。
2. 嵌套列表
嵌套列表是指列表中的每一项本身又是一个列表,用户可以展开或收起每一项。这种样式常用于展示树状结构的数据,如文件浏览器、多级菜单等。
3. 瀑布流列表
瀑布流列表的特点是列表项大小不一,排列紧凑,像瀑布一样连绵不断。它通常用于图片或者卡片的展示,如图片墙、商品列表等。
4. 左右联动列表
左右联动列表由两个列表组成,左侧是分类列表,右侧是内容列表。当用户在左侧选择不同分类时,右侧的内容会随之变化。这种样式常用于新闻客户端的新闻分类,电商App的商品分类等。
5. 聊天气泡列表
聊天气泡列表是IM社交类App中常见的列表样式,它的特点是列表项是气泡状的,且区分左右两种气泡(代表两个聊天角色)。
6. 卡片堆叠列表
卡片堆叠列表给人一种层叠的视觉效果,通常最上面的卡片显示完整,下面的卡片只露出一小部分。用户可以上下滑动切换卡片。这种列表常用于信息流的展示,如社交动态、推荐内容等。
以上就是iOS开发中几种常见的复杂列表样式及其应用场景。可以看到,虽然都是列表,但不同的样式给人的视觉感受和交互体验是完全不同的。作为开发者,我们需要根据App的设计需求,选择合适的列表样式。
二、iOS复杂列表的几种开发方式
对于复杂列表的开发,iOS 提供了几种不同的实现方式,各有优劣。
1. UITableView + Cell子类化
这是最传统、最常用的一种方式。我们通过定义一个UITableViewCell的子类来描述列表项的样式和行为,然后在UITableViewDelegate和UITableViewDataSource中返回Cell实例和相关数据。
这种方式的优点是简单直观,控制力强。但当Cell种类较多、逻辑较复杂时,会导致ViewContoller过于臃肿。维护成本较高。
2. UICollectionView + Cell子类化
UICollectionView 提供了比 UITableView 更加灵活的布局方式,特别适合实现一些不规则的列表样式,如瀑布流、循环列表等。
同样地,我们也需要定义UICollectionViewCell的子类,并实现UICollectionViewDelegate、UICollectionViewDataSource等协议方法。
相比UITableView,UICollectionView 在布局上灵活了不少,但也更加复杂。我们需要额外实现一个 UICollectionViewLayout 的子类来定义布局的各种参数。
3. 基于 MVVM 的开发方式
MVVM 是一种流行的架构模式,它通过引入ViewModel这一中间层,将View和Model解耦,使得代码更加清晰、易于测试。
在 MVVM 模式下,我们通常将列表的数据、状态、业务逻辑等都封装在 ViewModel 中,Cell 只负责从 ViewModel 获取数据并展示。这样可以避免 View Controller 过于臃肿的问题。
常见的 MVVM 绑定库有 RxSwift、ReactiveCocoa 等,它们提供了一套声明式的 API,让我们可以优雅地在 View 和 ViewModel 之间建立绑定关系。
4. 基于Texture(原AsyncDisplayKit)的开发方式
Texture是Facebook开源的一个UI框架,它的核心思想是将UI的各种属性(布局、渲染、更新等)都封装到一个Node对象中,然后以类似UIView的方式来使用这个Node。
Texture 最大的特点是它的布局和渲染都是异步的、并发的,不会阻塞主线程。这使得它在复杂列表的性能优化方面有着巨大的优势。
在 Texture 中,我们通过定义ASCellNode的子类来描述列表项,类似于UITableViewCell。但ASCellNode更加轻量,因为它并不是一个真正的View,只是一个配置信息的容器。
5. SwiftUI 的 List 组件
如果你的项目使用的是 SwiftUI 框架,那么列表的开发方式又有所不同。SwiftUI 提供了一个 List 组件,可以非常方便地创建各种列表。
在 SwiftUI 中,我们通过声明式的语法来描述UI,列表的每一项都是一个独立的View。这种方式非常简洁,代码可读性很高。
但目前 SwiftUI 还处于发展的早期阶段,在性能和功能方面还有一些局限性,特别是对于复杂列表场景。
以上就是iOS开发中几种常见的复杂列表开发方式。在实际项目中,我们需要根据具体的需求和团队的技术栈来选择合适的方式。
三、iOS复杂列表的高度自适应和优化
在复杂列表的开发中,一个常见的需求就是列表项的高度不固定,需要根据内容自适应。这种需求给列表的性能优化带来了不小的挑战。
1. 为什么高度自适应会影响性能?
在UITableView和UICollectionView中,如果我们没有提前指定Cell的高度,系统就需要在每次加载Cell时动态计算其高度。这个计算过程通常需要以下几个步骤:
- 创建Cell实例
- 将数据填充到Cell中
- 调用Cell的
layoutSubviews
方法,让子视图重新布局 - 调用Cell的
sizeThatFits
方法,计算Cell的最佳尺寸 - 返回计算出的尺寸
可以看到,这个过程是同步的、耗时的,特别是当Cell的布局复杂、内容动态变化时,计算的开销会非常大。如果有几百上千个Cell,这个计算过程就会严重阻塞主线程,导致列表卡顿。
2. 高度缓存
为了避免重复计算Cell高度的开销,一种常见的优化策略是高度缓存。即我们在第一次计算出Cell的高度后,将其缓存起来,下次加载相同indexPath的Cell时,直接使用缓存的高度,而不是重新计算。
以下是一个简单的高度缓存的实现:
swift
class TableViewController: UITableViewController {
var dataSource: [String] = []
var heightCache: [IndexPath: CGFloat] = [:]
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = heightCache[indexPath] {
return height
}
let height = calculateHeight(at: indexPath)
heightCache[indexPath] = height
return height
}
func calculateHeight(at indexPath: IndexPath) -> CGFloat {
// 创建一个临时的Cell实例
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CustomCell
// 填充数据
cell.configure(with: dataSource[indexPath.row])
// 计算高度
let height = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
return height
}
}
在这个例子中,我们定义了一个heightCache
字典来缓存高度,key是IndexPath,value是对应的高度。在tableView(_:heightForRowAt:)
方法中,我们首先检查缓存中是否已经有了该IndexPath的高度,如果有就直接返回;如果没有,则调用calculateHeight(at:)
方法计算高度,并将结果存入缓存。
这种方式可以显著减少高度计算的次数,提升列表的滚动性能。但缓存的空间占用也是一个问题,如果列表项过多,缓存的内存开销也会比较大。因此,我们还需要在合适的时机(如收到内存警告时)清理缓存。
3. 高度预估
除了高度缓存,另一种常用的优化策略是高度预估。即我们不需要精确地计算每个Cell的高度,而是给出一个估计值。这个估计值不需要完全准确,只要能保证Cell的内容不会被截断即可。
在UITableView中,我们可以通过实现tableView(_:estimatedHeightForRowAt:)
方法来提供一个估计高度。系统会以这个估计高度作为初始值来布局Cell,等到Cell真正显示时,再动态调整其高度。
swift
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
这种方式可以显著减少高度计算的次数,因为大部分Cell的高度计算都发生在其可见时,而不是一次性全部计算。同时,由于估计高度通常比实际高度要小,所以初次加载列表时的速度也会更快。
但这种方式也有一定的局限性。如果估计高度与实际高度差距太大,可能会导致列表滚动时出现跳动的情况。因此,我们需要尽可能提供一个接近实际高度的估计值。
4. Texture的异步布局和渲染
前面提到,Texture框架的核心优势就在于它的布局和渲染都是异步的,不会阻塞主线程。这对高度自适应的列表性能优化有着巨大的帮助。
在Texture中,我们通过重写ASCellNode的layoutSpecThatFits
方法来定义Cell的布局:
less
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let titleNode = ASTextNode()
titleNode.attributedText = NSAttributedString(string: "Title")
let subtitleNode = ASTextNode()
subtitleNode.attributedText = NSAttributedString(string: "Subtitle")
let verticalStack = ASStackLayoutSpec(direction: .vertical,
spacing: 5,
justifyContent: .start,
alignItems: .stretch,
children: [titleNode, subtitleNode])
return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10),
child: verticalStack)
}
在这个例子中,我们创建了两个文本节点(titleNode
和subtitleNode
),然后用一个垂直的StackLayout将它们组合起来,最后添加了一些内边距。
当Texture需要计算这个Cell的高度时,它会在后台线程调用layoutSpecThatFits
方法,并将其结果缓存起来,等到Cell真正显示时再应用这个布局。整个过程都是异步的,不会阻塞主线程。
此外,Texture还提供了一些其他的优化手段,如预加载、智能压缩、增量渲染等,可以进一步提升复杂列表的性能。
5. SwiftUI的LazyVStack和LazyHStack
在SwiftUI中,列表的高度自适应是默认支持的,我们不需要手动计算或缓存Cell的高度。这得益于SwiftUI的声明式语法和响应式更新机制。
SwiftUI提供了LazyVStack
和LazyHStack
两个组件,用于创建垂直和水平方向的懒加载列表。它们只会在Cell可见时才创建和加载,可以显著减少内存占用。
scss
struct ContentView: View {
let data = (0..<100).map { "Item ($0)" }
var body: some View {
ScrollView {
LazyVStack {
ForEach(data, id: .self) { item in
Text(item)
.padding()
}
}
}
}
}
在这个例子中,我们创建了一个包含100个字符串的数组作为列表的数据源,然后用LazyVStack
和ForEach
循环来创建列表。SwiftUI会自动处理Cell的高度计算和布局。
四、复杂列表的性能考核和优化策略
除了高度自适应,复杂列表的性能优化还需要从多个方面来考虑。下面我就来分享一下我在实践中总结的一些性能考核指标和优化策略。
1. 性能考核指标
要优化列表的性能,首先我们需要有一套可量化的性能指标。常用的指标有:
- FPS(Frames Per Second):表示列表滚动时的帧率,反映了列表的流畅度。一般来说,FPS越高,用户体验越好。通常我们以60FPS作为优化目标。
- CPU占用:表示列表滚动时CPU的占用情况。CPU占用过高,会导致其他任务(如网络请求、动画等)受到影响,也会加剧电池的消耗。
- 内存占用:表示列表占用的内存大小。内存占用过高,可能会导致App崩溃或被系统终止。
- 加载时间:表示列表从开始加载到完全显示的时间。加载时间过长,会影响用户体验。
为了获取这些指标,我们可以使用Xcode提供的Instruments工具,如Core Animation、Time Profiler、Allocations等。通过分析这些工具的结果,我们可以找出列表性能的瓶颈所在。
2. 优化Cell的复用
Cell的复用是列表性能优化的基本手段。通过复用,我们可以在列表滚动时避免不断地创建和销毁Cell,从而减少CPU和内存的开销。
在UITableView
和UICollectionView
中,Cell的复用是通过dequeueReusableCell(withIdentifier:for:)
方法实现的。我们需要在注册Cell时提供一个复用标识符,然后在cellForRowAt
或cellForItemAt
方法中通过这个标识符来获取可复用的Cell实例。
swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
// 配置Cell...
return cell
}
需要注意的是,在复用Cell时,我们需要确保将Cell重置到一个干净的状态,避免上一次使用的数据残留。通常我们可以在Cell的prepareForReuse
方法中进行重置操作。
3. 异步加载和预加载
对于一些耗时的操作,如图片的下载和解码,我们可以考虑使用异步加载和预加载的技术,避免阻塞主线程。
异步加载是指在后台线程执行耗时操作,等到操作完成后再在主线程更新UI。这可以避免耗时操作阻塞列表的滚动。常见的异步加载库有SDWebImage、Kingfisher等。
预加载是指在Cell还没有出现在屏幕上时,就提前加载它所需的数据和资源。这可以减少Cell出现时的加载时间,提升用户体验。预加载的范围需要根据设备的性能和网络状况来适当调整。
以下是一个简单的图片预加载的实现:
swift
override func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let imageUrl = dataSource[indexPath.row].imageUrl
prefetchImage(from: imageUrl)
}
}
func prefetchImage(from url: URL) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
// 缓存图片数据...
}.resume()
}
在这个例子中,我们实现了UITableViewDataSourcePrefetching
协议的tableView(_:prefetchRowsAt:)
方法,在这个方法中获取即将出现的Cell的图片URL,并调用prefetchImage(from:)
方法在后台线程下载图片数据。
4. 局部更新
当列表的数据发生变化时,我们需要更新列表的显示。一种简单的方式是调用reloadData
方法,让列表重新加载所有的数据。但这种方式的效率比较低,特别是当列表数据很大时,会导致明显的卡顿。
更好的做法是使用局部更新,即只更新发生变化的那部分数据。UITableView
和UICollectionView
提供了一系列的局部更新方法,如:
insertRows(at:with:)
deleteRows(at:with:)
reloadRows(at:with:)
moveRow(at:to:)
通过这些方法,我们可以更细粒度地控制列表的更新,减少不必要的重绘和布局。
csharp
func updateDataSource() {
let oldCount = dataSource.count
dataSource.append("New Item")
let indexPath = IndexPath(row: oldCount, section: 0)
tableView.insertRows(at: [indexPath], with: .automatic)
}
在这个例子中,当数据源新增了一条数据时,我们通过insertRows(at:with:)
方法来局部更新列表,而不是重新加载整个列表。
5. 避免不必要的布局和绘制
在列表滚动时,每一帧都会触发列表的布局(layoutSubviews
)和绘制(draw
)。如果在这两个方法中执行了耗时的操作,就会导致列表卡顿。
因此,我们需要尽量避免在layoutSubviews
和draw
方法中执行复杂的计算或者修改视图层次。对于一些需要实时更新的视图(如UILabel),我们可以考虑使用异步绘制或者预渲染的技术。
此外,我们还需要注意列表的重绘范围。默认情况下,列表的重绘范围是整个可视区域。但有时候,我们只需要重绘列表的一小部分(如单个Cell)。这时,我们可以通过重写UIView
的contentMode
属性,将重绘范围缩小到单个Cell的范围。
ini
class CustomCell: UITableViewCell {
override var contentMode: UIView.ContentMode {
didSet {
contentView.contentMode = contentMode
textLabel?.contentMode = contentMode
detailTextLabel?.contentMode = contentMode
}
}
}
在这个例子中,我们重写了Cell的contentMode
属性,并将其应用到Cell的contentView和子视图上。这样,当Cell的内容发生变化时,只会重绘Cell本身,而不会影响其他Cell。
五、前端列表与iOS列表的区别和优劣分析
随着 React Native、Flutter、Weex 等前端跨平台框架的兴起,越来越多的App开始采用前端技术来开发iOS应用。那么,前端开发的iOS列表与原生列表有什么区别呢? 它们各自的优缺点又是什么呢?
1. 开发效率
前端开发的一大优势就是开发效率高。前端框架通常采用声明式的 UI 语法,可以快速搭建页面结构。很多前端框架还提供了丰富的 UI 组件库,开发者可以直接使用,而不需要从头开发。
以 React Native 为例,我们可以使用 FlatList 组件来快速创建一个列表:
javascript
<FlatList
data={[{key: 'a'}, {key: 'b'}]}
renderItem={({item}) => <Text>{item.key}</Text>}
/>
相比之下,原生开发需要手动创建 UITableView 或 UICollectionView,并实现一系列的 DataSource 和 Delegate 方法,开发效率相对较低。
2. 性能
尽管前端框架在不断优化其性能,但在复杂列表的场景下,原生开发仍然具有性能优势。这主要有以下几个原因:
首先,前端框架在渲染列表时,需要先将数据从 JavaScript 传递到原生层,再由原生层进行渲染。这个过程会有一定的通信开销。而原生开发直接在原生层渲染,没有这个开销。
其次,前端框架的列表组件通常是基于 UIScrollView 实现的,而原生的 UITableView 和 UICollectionView 是专门为列表场景优化过的。它们在内存管理、复用机制、渲染效率等方面都有更好的表现。
再次,原生开发可以更直接地利用系统提供的一些特性,如 UITableViewCell 的自动布局、预加载等。而前端框架受限于其封装,有时无法完全发挥系统的特性。
当然,这并不意味着前端框架就不能开发高性能的列表。通过一些优化手段,如分页加载、懒加载、避免重复渲染等,我们仍然可以在前端框架中实现流畅的列表体验。
3. 动态化
前端开发的一个优势是动态化能力强。前端框架通常采用 JavaScript 作为开发语言,可以在运行时动态地修改页面结构和样式。这对于一些需要频繁迭代的业务场景非常有利。
例如,我们可以通过下发 JavaScript 脚本的方式,在不发版的情况下修改列表的样式或者交互逻辑。这在电商、资讯等快速变化的领域尤为重要。
原生开发受限于编译语言的特性,其动态化能力相对较弱。尽管 iOS 也提供了一些动态化的手段,如 JSPatch、WaxPatch 等,但它们的使用门槛较高,而且苹果对动态化的限制也越来越严格。
4. 开发成本
前端开发的另一个优势是开发成本低。由于 JavaScript 是一种通用的 Web 开发语言,有大量的前端开发者。相比之下,原生 iOS 开发需要专门的 Objective-C 或 Swift 开发技能。
此外,前端框架通常采用单一的 JavaScript 代码库来同时支持 iOS 和 Android 两个平台。这种"一次开发,多处运行"的特性可以显著减少开发和维护成本。而原生开发需要为每个平台单独开发和维护代码库,成本相对较高。
但需要注意的是,前端框架的跨平台特性也意味着我们无法完全利用每个平台的特性。有时为了兼顾跨平台,我们不得不放弃一些平台特有的功能或性能优化。这也是前端开发需要权衡的一个点。
5. 用户体验
在用户体验方面,原生开发通常有更好的表现。原生控件在交互体验、动画效果、手势支持等方面都是经过精心设计和优化的,可以给用户提供更加自然、流畅的体验。
相比之下,前端框架由于其跨平台的特性,有时很难完全模拟原生控件的体验。尽管前端框架也在不断改进其动画性能和手势支持,但在一些对交互体验要求较高的场景下,原生开发仍然有一定优势。
另一方面,前端开发有利于保持 iOS 和 Android 两个平台间的体验一致性。由于共享了同一套 UI 组件和交互逻辑,前端框架可以确保两个平台的页面表现一致。而原生开发由于平台特性的差异,要做到两个平台体验一致是比较困难的。
六、复杂列表的性能考核和优化策略
除了高度自适应,复杂列表的性能优化还需要从多个方面来考虑。下面我就来分享一下我在实践中总结的一些性能考核指标和优化策略。
通过上面的分析,我们可以看到前端iOS列表开发和原生列表开发各有其优势和局限。前端开发强调开发效率、动态化能力和跨平台支持,而原生开发强调性能、用户体验和对系统特性的利用。
在实际项目中,我们需要根据具体的业务需求和团队情况来选择合适的技术方案。一些对性能和交互体验要求较高的核心业务,如首页信息流、实时聊天等,更适合采用原生开发;而一些对开发效率和动态化要求较高的业务,如活动页面、内容详情页等,更适合采用前端开发。
同时,我们也不必完全对立前端开发和原生开发。很多团队采用了混合开发的模式,即在原生框架中嵌入前端框架,实现两者的优势互补。例如,我们可以在原生的 UITableView 中嵌入 React Native 的页面作为Cell,既利用了原生列表的性能优势,又利用了前端框架的动态化优势。
总之,作为一名iOS开发者,我们需要对前端开发和原生开发都有所了解,根据实际情况灵活选择,而不是教条地坚持某一种技术。只有综合运用各种技术,才能开发出最优质的列表体验。
七、展望
展望未来,我认为iOS列表开发还有以下几个趋势和方向:
1. 列表的智能化
随着人工智能技术的发展,未来的列表可能会更加智能化。例如,列表可以根据用户的喜好、阅读历史、时间地点等信息,自动调整内容的排序和筛选,给用户推荐最感兴趣的内容。
列表还可以通过机器学习算法,自动优化其布局和设计。例如,根据用户的点击和滚动行为,列表可以动态调整每个 Item 的高度、间距、字体大小等,以提供最佳的阅读体验。
2. 列表的动态化
随着5G时代的普及化,列表的内容将更加实时和动态。例如,直播列表、实时竞价列表、在线协作列表等。这对列表的性能提出了更高的要求,需要列表能够快速响应数据的变化,并平滑地更新UI。
同时,动态列表也对前端框架提出了新的挑战。如何在保证性能的同时,提供灵活的 UI 更新机制,将是前端框架需要解决的问题。
3. 列表的交互创新
随着移动设备的不断升级,列表的交互方式也在不断创新。例如,3D Touch、触觉反馈、手势识别等新技术的出现,为列表交互提供了更多可能性。
未来的列表交互可能会更加自然和直观。例如,用户可以通过压感力度来控制列表的滚动速度,通过手势来实现列表项的合并和拆分,通过语音来实现列表的筛选和搜索等。
4. 列表的可视化
除了传统的文本、图片列表,未来列表的内容呈现可能会更加多样化和可视化。例如,VR/AR 列表、3D 列表、图表列表等。这些新型列表不仅可以提供更加直观和吸引人的视觉体验,还可以帮助用户更快地理解和分析列表数据。
5. 列表的个性化
未来的列表可能会更加注重个性化和定制化。每个用户看到的列表内容、样式、交互方式可能都不一样。这就要求列表框架和开发者要提供灵活的配置和定制能力,能够快速响应不同用户的个性化需求。
八、参考资料
以下是一些对本文有启发和帮助的参考资源:
- WWDC 2018: High Performance Auto Layout - 介绍了自动布局的性能优化技巧
- WWDC 2019: Advances in Collection View Layout - 介绍了 CollectionView 布局的新特性和改进
- ASDK 文档 - Texture(ASDK)的官方文档,介绍了其异步渲染架构
- IGListKit 文档 - IGListKit 的官方文档,介绍了其数据驱动的列表框架
- React Native 文档 - React Native 的官方文档,介绍了其原理和基本用法
- Flutter 文档 - Flutter 的官方文档,介绍了其响应式 UI 框架
- SwiftUI 文档 - SwiftUI 的官方文档,介绍了其声明式 UI 语法
- Weex 文档 - Weex 的官方文档,介绍了其跨平台解决方案
- Flutterverse - 收录了大量 Flutter 相关的文章和实战案例,包括列表的开发和优化
- RxSwift - 一个函数响应式编程框架,可以用于列表的数据处理和状态管理
当然,iOS列表开发是一个非常广泛和深入的话题,还有很多优秀的文章、书籍、开源库值得学习和参考。我们需要在实践中不断吸收和运用这些知识,并结合自己的思考和创新,才能设计和开发出优秀的列表体验。
以上就是我对iOS复杂列表开发的全部分享。希望这些内容能给大家一些启发和帮助。也欢迎大家留言交流,分享自己的经验和想法。让我们一起探讨和优化移动端列表的开发,为用户创造更加出色的产品体验。
谢谢大家!