性能优化-①列表优化

列表优化

[toc]

其他参考文档:列表无限滚动时,数据如何预加载,从而达到无缝加载的效果 Demo

一、请求时,列表网络数据的预加载

1、第一页的预加载

提前创建 vm 或者 manager 管理请求数据。 (平常用的数据携带、默认数据等本地数据暂不在此讨论)

2、下一页的预加载

预加载是指在Cell还没有出现在屏幕上时,就提前加载它所需的数据和资源。这可以减少Cell出现时的加载时间,提升用户体验。

举例:在用户阅读了最新页码数据的70%(contentSize:UIScrollView所有内容的尺⼨)时(根据实际情况调节),提前进行下一页数据的加载。这样用户可以省去本来在阅读完已加载的时候需要做一次上拉加载等待数据的过程。

可以看到第一页阈值是70%,即代表进入后即使没滑动也会自动加载第二页。

image-20240815012027334

图片来源:列表的预加载.graffle

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentOffset = scrollView.contentOffset.y + scrollView.frame.size.height
let totalHeight = scrollView.contentSize.height
let ratio = currentOffset / totalHeight // 当前滚动内容占总内容的比例

//let threshold = 0.7 // 优化前
var threshold = (curPageIndex+0.7)/(curPageIndex+1.0) // 优化后,可以看到第一页还是70% 即使进入后没滑动也会自动加载第二页
// 超过阈值 threshold 则进行预加载下一页数据。可以看到第一页阈值是70%,即代表进入后即使没滑动也会自动加载第二页
if ratio >= threshold {
fetchNextPageData(page: currentPage)
}
}

var currentPage = 0
func fetchNextPageData() {
currentPage += 1
loadPage(pageIndex: currentPage)
}


func loadPage(_ pageIndex: int) {

}

参考文章:预加载与智能预加载(iOS)

其他参考文章:

二、请求后,数据渲染时的按需加载

滑动时,按需加载:UITableView禁止或者减速滑动结束的时候,进行异步加载图片,快滑动过程中,只加载目标范围内的Cell。

问:从第1个cell滑动到第100个cell。请问在快速滑动情况下如果在tableView(_:cellForRowAt:) 中打印indexPath,能够打印到1到100的indexPath吗

答:在快速滑动 UITableView 从第一个单元格到第100个单元格时,tableView(_:cellForRowAt:) 方法可能会被多次调用,但并不意味着它会为每个索引路径(从0到99)都打印出对应的值。如果用户滑动得非常快,UITableView 为了保持流畅的滚动性能,可能会跳过一些单元格的 tableView(_:cellForRowAt:) 调用,尤其是那些在屏幕外的单元格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class TableViewController: UITableViewController {
// 标记是否应该加载图片
var shouldLoadImages = false

override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 100 // 设置预估行高
self.tableView.rowHeight = UITableView.automaticDimension
}

// ... 其他代码
}


func scrollViewDidScroll(_ scrollView: UIScrollView) {
// 计算滚动速度,并根据滚动速度设置是否加载图片,有时候快停止的时候就可以加载了,不用完全停止。
let currentVelocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
shouldLoadImages = abs(currentVelocity.y) > 1.0 ? false : true
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath)
// 检查是否应该加载图片(快速滑动过程中,可能有多次调用到该方法)
if shouldLoadImages {
loadImageAsync(for: cell) // 异步加载图片
} else {
cell.imageView?.image = UIImage(named: "placeholder") // 快速滑动时,只加载占位图或者不加载图片
}

// 配置cell的其他内容
// ...

return cell
}

写法二:不太推荐,性质一样,不过写法有点别扭,相当于cell内容的处理位置变了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// 计算滚动速度,并根据滚动速度设置是否加载图片,有时候快停止的时候就可以加载了,不用完全停止。
let currentVelocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
shouldLoadImages = abs(currentVelocity.y) > 1.0 ? false : true

// 1. 获取当前可见的单元格索引路径数组,再根据索引路径获取对应的单元格
if let visiblePaths = self.tableView.indexPathsForVisibleRows {
for indexPath in visiblePaths {
if let cell = self.tableView.cellForRow(at: indexPath) {
// 检查是否应该加载图片
if shouldLoadImages {
loadImageAsync(for: cell) // 异步加载图片
} else {
cell.imageView?.image = UIImage(named: "placeholder") // 快速滑动时,只加载占位图或者不加载图片
}
}
}
}
}

优化加强:如果滚动方向改变,快速下滑后又上滑,取消可见区域下面的部分(可见区域上面的部分)。类似于 PrefetchDataSource 的 prefetchRowsAtIndexPaths 和 cancelPrefetchingForRowsAtIndexPaths

延伸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
var shouldLoadImages: Bool = true										// 假设有一个标记来记录是否应该加载图片
var loadingOperations: [IndexPath: Operation] = [:] // 用于存储正在加载的图片的单元格的IndexPath

func scrollViewDidScroll(_ scrollView: UIScrollView) {
// 计算滚动速度,并根据滚动速度设置是否加载图片,有时候快停止的时候就可以加载了,不用完全停止。
let currentVelocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
shouldLoadImages = abs(currentVelocity.y) < 1.0

if shouldLoadImages {
if let visiblePaths = self.tableView.indexPathsForVisibleRows {
prefetchRows(at: visiblePaths)
}
} else {
// 如果不需要加载图片,取消预加载
cancelPrefetchingForRows(in: scrollView)
}
}

func prefetchRows(at indexPaths: [IndexPath]) {
// 调用预加载方法
self.tableView.prefetchRows(at: indexPaths)
}

func cancelPrefetchingForRows(in scrollView: UIScrollView) {
// 检查当前滚动速度和方向
let currentVelocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
let isScrollingUp = currentVelocity.y < 0

// 找出所有不在可视区域内的单元格indexPaths
guard let visiblePaths = self.tableView.indexPathsForVisibleRows else { return }
let indexPathsToCancel = loadingOperations.keys.filter { !visiblePaths.contains($0) }

// 如果滚动方向改变,取消不在可视区域内的单元格的预加载
if isScrollingUp || !visiblePaths.contains(where: { $0 >= indexPathsToCancel.first! }) {
cancelPrefetchingForRows(at: indexPathsToCancel)
}
}

// 实现 UITableViewDataSourcePrefetching 协议的方法
extension YourTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAtIndexPaths indexPaths: [IndexPath]) {
// 在这里执行图片的预加载操作
for indexPath in indexPaths {
if shouldLoadImages, let cell = tableView.cellForRow(at: indexPath) as? YourTableViewCell {
loadImageAsync(for: cell, at: indexPath)
}
}
}

func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAtIndexPaths indexPaths: [IndexPath]) {
// 在这里取消图片的预加载操作
for indexPath in indexPaths {
if let operation = loadingOperations[indexPath], !operation.isFinished {
operation.cancel()
}
}
}
}

// 异步加载图片的示例方法
func loadImageAsync(for cell: YourTableViewCell, at indexPath: IndexPath) {
// 执行异步图片加载操作,例如从网络下载
// 这里应该使用你的图片加载逻辑
let operation = BlockOperation {
// 模拟异步图片加载
guard let downloadedImage = UIImage() else { return }

// 将下载的图片缓存到内存中
DispatchQueue.main.async {
cell.imageView.image = downloadedImage
}
}

// 将操作添加到后台队列
operationQueue.addOperation(operation)

// 记录这个操作
loadingOperations[indexPath] = operation
}

func cancelPrefetchingForRows(at indexPaths: [IndexPath]) {
// 调用tableView的取消预加载方法
self.tableView.cancelPrefetchingForRows(at: indexPaths)
}

其他参考文档:

三、渲染时候的优化

1、ASDK

Texture 拥有自己的一套成熟布局方案,虽然学习成本略高,但至少比原生的 AutoLayout 写起来舒服,重点是性能远好于 AutoLayout

参考文档:

iOS 开发一定要尝试的 Texture(ASDK)

iOS原生开发视角下的复杂列表开发与性能优化

四、UITableView的性能优化

< 返回目录

参考资料:UITableView性能优化,超实用

①Cell重用

1
2
3
4
5
6
7
> // 返回Cell的代理方法会调用很多次,为防止重复创建,我们使用static 保证只创建一次reuseID,提高性能
> static NSString *reuseID = “reuseCellID”;
>
> // 从缓存池中取相应identifier的Cell并更新数据。
> // 如果没有,才开始alloc新的Cell,并用identifier标识Cell。每个Cell都会注册一个identifier(重用标识符)放入缓存池,当需要调用的时候就直接从缓存池里找对应的id,当不需要时就放入缓存池等待调用。(移出屏幕的Cell才会放入缓存池中,并不会被release)
> UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseID];
>

附:比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> 缓存池获取可重用Cell两个方法的区别
>
> -(nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;
> // 方法1:这个方法会查询可重用Cell,如果注册了原型Cell,能够查询到,否则,返回nil;而且需要判断if(cell == nil),才会创建Cell,不推荐
>
> -(__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0);
> // 方法2:使用这个方法之前,必须通过xib(storyboard)或是Class(纯代码)注册可重用Cell,而且这个方法一定会返回一个Cell
>
> // 附:方法2需要的注册Cell的方法
> - (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);
> - (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);
>
> // 好处:如果缓冲区 Cell 不存在,会使用原型 Cell 实例化一个新的 Cell,不需要再判断,同时代码结构更清晰。
>

②定义一种(尽量少)类型的Cell及善用hidden隐藏(显示)subviews。即可以初始化时就添加,然后通过hide来控制是否显示。(比如朋友圈),而不要用addView给Cell动态添加View,

③提前计算并缓存Cell的高度;(Model去缓存,或者使用SDAutoLayout工具)
④网络数据的异步加载(如cell中的图片加载),不要阻塞主线程;
⑤滑动时,按需加载,常见于大量图片时候。即当UITableView静止或者减速滑动结束之后才去进行异步加载图片。
⑥渲染优化:减少subviews的个数和层级;对于不透明的View,设置opaque为YES;阴影绘制及性能优化。

更新时候:使用局部更新,如果只是更新某组的话,使用reloadSection进行局部更新

1、Cell的重用

基于Cell的重用,真正运行时铺满屏幕所需的Cell数量大致是固定的,设为N个。所以
①如果如果只有一种Cell,那就是只有N个Cell的实例;
②但是如果有M种Cell,那么运行时最多可能会是“M x N = MN”个Cell的实例;
虽然可能并不会占用太多内存,但是能少点不是更好吗。

四、列表加载图片的优化

1、缩略图的使用

图片划分两个地址.一个地址获取缩略图,一个地址获取原图>> 这样你就可以在TableViewCell使用缩略图(展示用),点击图片查看(使用原图). 这样就大大减少了内存的使用.

2、UITableView优化

更轻量的 View Controllers 把 Data Source 和其他 Protocols 分离出来

各页面的预加载

以上内容为同一页面内的预加载处理。那如果是页面间的呢?

在iOS中,使用UITabBarController作为应用程序的主视图控制器时,通常会有几个子视图控制器与之关联。

问:那么这些子视图控制器的初始化和内容渲染分别是在什么时候?或者说刚启动app时候让app在默认的第一个tab,此时第二tab的视图控制器加载了哪些方法,其其他方法又是什么时候触发的。

答:①第二个tab的初始化即init ,在UITabBarController被初始化时,并被设置为窗口(UIWindow)的根视图控制器之后执行。附其他tab的init也一样。

②第二个tab的viewDidload,在 UITabBarController 切换到该tab 时候才会调用。

以上验证代码可在 https://github.com/dvlproad/001-UIKit-CQDemo-iOS 中验证

问1:UITabBarController下,如何预加载指定的视图控制器?

答:数据通过 vm 或者 manager 提前在UITabBarController初始化时进行获取。

问2:UITabBarController下的子视图控制器中如果还有多tab(在顶部),则又如何进行预加载。

答:同1一样。数据通过 vm 或者 manager 提前在该子视图控制器初始化时进行获取。