视图-更新机制

[toc]

视图-更新机制

系统有基本稳定的刷新频率,在layer内容改变的时候,把这个layer做个需要刷新的标记,即是setNeedsDisplay

然后每次刷新时,把上次刷新之后被标记的layer一次性全部提交给图形系统,所以这里还有一个东西,就是事务(CATransaction)

其他类似举例:

1
2
3
4
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: tableData.count-2, section: 0)], with: UITableViewRowAnimation.automatic)
tableView.insertRows(at: [IndexPath(row: tableData.count-1, section: 0)], with: UITableViewRowAnimation.automatic)
tableView.endUpdates()

beginUpdatesendUpdates 方法的作用是,让这两条语句之间的对 tableView 的操作( insert/delete)不立即执行,而是先聚合起来,然后同时更新 UI

1、刷新频率

1.1、显示器的刷新率 VS 显卡渲染的帧率

1、显示器的刷新率为 50Hz,显卡渲染的帧率是 200FPS

即显示器20ms显示一帧,显卡5ms渲染一帧。

则20ms里,显卡渲染了4帧数据,但显示器只能显示了一次,该画面由4帧数据组成,造成撕裂。

所以需要压制显卡的渲染速率,使显卡的帧缓冲区切换行为与显示器的帧绘制保持同步。垂直同步(Vertical Synchronization, VSync)即是为了处理此。

好文推荐: 扫描,撕裂和垂直同步 - VSync 技术实现

1.1.1、显示数据的提供来源:帧缓冲区

通常,显示器是一台独立工作的设备,状态与显卡无关,会以恒定不变的频率从某个「池」里面读取画面,以保证稳定的图像输出。与此同时,显卡也会往这个「池」里写画面,以供显示器读取。这个「池」叫「Framebuffer(帧缓冲区)」。

一张显卡通常有 2 个帧缓冲区:主/副(Primary/Secondary)缓冲区(也称前/后缓冲区(Front/Back)),由数据选择器(Multiplexer)选择连接到显示器的缓冲区。连接到显示器的缓冲区总是主缓冲区,显示器从中读取图像内容;未连接到显示器的缓冲区总是副缓冲区,显卡向其中写入渲染好的内容。

显卡总是会尝试以最快的速度渲染内容,每完成一帧渲染即切换主/副缓冲区,与显示器的工作状态完全无关。

image-20240905163331025

当渲染完成后,副缓冲区的内容会与主缓冲区的内容进行交换,这个过程是双缓冲(Double Buffering)技术的一部分,用于避免屏幕撕裂和闪烁,同时提高图像渲染的效率。下面详细解释这个过程:

  1. 主缓冲区(Front Buffer):这是当前正在屏幕上显示的图像所在的缓冲区。用户看到的所有内容都存储在这里,显示器会不断读取这个缓冲区的内容来显示图像。
  2. 副缓冲区(Back Buffer):这是显卡用来准备下一帧图像的缓冲区。当一帧图像显示在屏幕上时,显卡可以在副缓冲区中渲染下一帧图像,而不会影响当前显示的内容。
  3. 渲染过程:显卡开始在副缓冲区中渲染新的一帧图像。这个过程可能包括执行复杂的图形计算,如光照计算、纹理映射、深度测试等。
  4. 缓冲区交换(Buffer Swap):一旦副缓冲区中的新帧渲染完成,显卡会执行一个缓冲区交换操作。这个操作会将副缓冲区的内容复制到主缓冲区,这样显示器就可以开始显示新的一帧图像,而副缓冲区则准备好接受下一帧的渲染数据。
  5. 避免屏幕撕裂:由于显示器是逐行刷新的,如果在显示器刷新过程中显卡正在渲染新的帧,就可能出现屏幕撕裂现象。双缓冲技术通过在渲染完成后才进行缓冲区交换,确保了显示器在任何时候都不会读取到半成品的帧,从而避免了屏幕撕裂。
  6. 提高效率:在双缓冲机制下,显卡可以在一帧显示的同时准备下一帧,这样可以更有效地利用显卡资源,提高渲染效率。
  7. 垂直同步(V-Sync):为了进一步提高图像质量和减少撕裂,有时会使用垂直同步技术。垂直同步会同步显卡的渲染速度和显示器的刷新率,确保缓冲区交换发生在显示器刷新周期的合适时刻。

通过这种方式,双缓冲技术能够在不牺牲渲染效率的情况下,提供流畅且无撕裂的图像显示。

1.2、垂直同步(Vertical synchronization)

V-Sync 的主要作用就是保证只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中

理想情况下每次 V-Sync 发生时,CPU 以及 GPU 都已经完成了对图像的处理以及绘制,显示器可以直接拿到缓冲区中的帧。即 V-Sync 会按这种方式工作:

image-20240905184838586

但是,如果 CPU 或者 GPU 的处理需要的时间较长,就会发生掉帧的问题,即在 V-Sync 信号发出时,CPU 和 GPU 并没有准备好需要渲染的帧,显示器就会继续使用当前帧。

image-20240905184900129

其实到这里关于屏幕渲染的内容就已经差不多结束了,根据 V-Sync 的原理,优化应用性能、提高 App 的 FPS 就可以从两个方面来入手,优化 CPU 以及 GPU 的处理时间。

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。

RunLoop是一个事件循环对象,用于管理线程的事件处理。每个线程都有一个与之对应的RunLoop对象,主线程的RunLoop在应用程序启动时自动创建并运行,而子线程的RunLoop需要手动创建和维护。RunLoop的作用是让线程能够随时处理事件而不退出,它通过循环检查输入源(如Timer、Source等)和定时源,等待接收事件,当没有事件时让线程休眠以节省资源。

Core Animation(CA)是 iOS 中的动画和图形渲染引擎,它在 RunLoop 中注册了 Observer 来监听特定的事件,以便在适当的时机进行渲染操作。这些 Observer 可以监听多种 RunLoop 的事件,包括:

  1. BeforeWaiting:RunLoop 即将进入休眠状态之前,此时没有待处理的事件,系统可能在等待新的输入事件或定时器事件。Core Animation 会在这个时候进行渲染操作,准备下一帧的显示内容。
  2. Exit:RunLoop 即将退出时,这通常意味着线程将结束运行。在这个阶段,Core Animation 可能会进行一些清理工作。

手机硬件时钟生成Vsync信号

=> 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。

=> app收到当VSync信号到达时,应用程序的主线程开始处理显示内容,如视图创建、布局计算等,然后将内容提交给GPU进行渲染。渲染完成后,GPU将结果提交到帧缓冲区,等待下一个VSync信号到来时显示到屏幕上。

=> Runloop接收到时钟信号(App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

当VSync信号到来时,系统图形服务会通过CADisplayLink等机制通知应用程序,

离屏渲染

在iOS设备上,GPU渲染通常发生在帧缓冲区中,这是GPU用来临时存储即将显示到屏幕上的像素数据的区域。帧缓冲区通常采用双缓冲机制,即存在一个前台缓冲区和一个后台缓冲区。前台缓冲区是当前显示在屏幕上的帧,而后台缓冲区是GPU正在渲染的下一帧。

离屏渲染(Off-Screen Rendering)是当GPU无法直接在帧缓冲区中渲染某些效果时所采用的一种技术。以下是一些可以在帧缓冲区处理的效果,以及一些需要离屏渲染的效果:

可以在帧缓冲区处理的效果:

  1. 简单的颜色填充和边框绘制:这些可以直接在GPU的帧缓冲区中进行,因为它们不涉及复杂的像素操作。
  2. 使用contents属性设置的图像:如果图层的内容是一个简单的图像,且没有复杂的变换或混合,这些可以直接在帧缓冲区中渲染。

需要离屏渲染的效果:

  1. 圆角(Rounded Corners):当为UIView或其子类设置cornerRadius属性时,如果视图同时具有不透明背景色或复杂的背景图像,可能会触发离屏渲染。
  2. 阴影(Shadows):设置layer的shadow属性(如shadowColor、shadowOffset、shadowRadius等)会产生阴影效果,这些效果通常需要离屏渲染。
  3. 透明度(Opacity):当视图的alpha值小于1或使用了CALayer的opacity属性时,如果有复杂混合层级,可能触发离屏渲染。
  4. 遮罩(Masking):使用CALayer的mask属性或UIView的maskView时,遮罩效果通常需要离屏渲染。
  5. 非默认混合模式:当视图或图层使用非默认的混合模式(如multiply、screen、overlay等)时,系统可能需要在离屏缓冲区中进行混合操作。
  6. 多重渲染目标(Multiple Render Passes):需要多次渲染才能完成的效果,如复杂动画、多重叠加效果等,可能需要离屏缓冲区进行中间结果的存储和合并。

为什么某些效果不行:

某些效果需要在渲染过程中进行多次像素级的处理,这在帧缓冲区的单次渲染流程中难以实现。例如,阴影效果需要在原始图层渲染后,再在其周围绘制额外的阴影像素,这涉及到对已经渲染的像素进行二次处理,因此需要在离屏缓冲区中先进行渲染,然后再与主帧缓冲区的内容合并。

优化建议:

  • 避免不必要的离屏渲染:例如,对于圆角效果,可以考虑使用系统提供的圆角属性,而不是通过离屏渲染实现。
  • 合理利用视图层级关系:在iOS中,视图层级关系会影响渲染的优先级,可以通过调整视图的层级来优化渲染性能。
  • 使用offscreen rendering进行调试:通过打开offscreen rendering的调试选项,可以观察到应用在进行离屏渲染时的具体情况,帮助定位性能瓶颈。

通过深入理解离屏渲染的原理并采取有效的优化措施,可以提升应用的性能和用户体验。在实际开发中,应尽量避免不必要的离屏渲染操作,合理利用视图层级关系和Metal API进行自定义渲染,从而打造出流畅、高效的iOS应用。

Flutter刷新机制

Flutter setState 标脏 子widget build 等是什么关系?

Widget不可改变。

StatefulWidget是有状态的Widget,其持有一个关联的State对象,用来保存状态信息,并可以通过调用setState来更新状态。StatefulWidget调用setState时候,该Widget被标记为“脏”(dirty),且它的所有子Widget也会被标记为脏。

通常情况下,被标记为“脏”的Widget会在下一次绘制帧中调用其build方法。然而,这并不意味着它的build方法一定会被调用,即如果Flutter框架能够确定一个Widget及其子树在逻辑上没有变化,即使它被标记为“脏”,Flutter也可能跳过重建过程,复用之前的Widget实例。这种情况通常发生在为一个Widget设置了GlobalKeyUniqueKey,并且Flutter能够识别出这个Widget在逻辑上没有变化,即使它被标记为“脏”,它的build方法也可能不会被调用。

性能优化-①列表优化

列表优化

[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 提前在该子视图控制器初始化时进行获取。

性能监控-①卡顿监控

卡顿监控

一、如何监控卡顿

以下内容摘自:iOS-Monitor-Platform 项目中的 如何监控卡顿

那怎么监控应用的卡顿情况?通常有以下两种方案

  • FPS 监控:这是最容易想到的一种方案,如果帧率越高意味着界面越流畅,上文也给出了计算 FPS 的实现方式,通过一段连续的 FPS 计算丢帧率来衡量当前页面绘制的质量。

  • 主线程卡顿监控:这是业内常用的一种检测卡顿的方法,通过开辟一个子线程来监控主线程的 RunLoop,当两个状态区域之间的耗时大于阈值时,就记为发生一次卡顿。美团的移动端性能监控方案 Hertz 采用的就是这种方式

    主线程卡顿监控的实现思路:开辟一个子线程,然后实时计算 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况

    其他参考文章:iOS 性能监控(二)—— 主线程卡顿监控

更详细内容推荐查看项目中md文档。

二、FPS

FPS”通常指的是”Frames Per Second”,即每秒传输帧数,用于衡量动画或视频渲染的性能。

一般情况下,我们的屏幕刷新率是 1/60s 一次。CADisplayLink 实际上跟平常用的 NSTimer 的用法基本相似,NSTimer 的时间间隔是以秒为单位,而 CADisplayLink 则是使用帧率来作为时间间隔的单位。

计算核心:CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。

实践代码:CQDemoKit 的 CQTSFPSView.m

附:其他定时器

iOS:三种常见计时器(NSTimer、CADisplayLink、dispatch_source_t)的使用

1、NSTimer:进入后台时停止

iOS开发:解决App进入后台,倒计时(定时器)不能正常计时的问题

方法一(主流方法):根据记录开始的时间和获取当前时间进行时间差操作进行处理。监听进入前台、进入后台的消息,在进入后台的时候存一下时间戳,停掉定时器(系统会强制停止定时器);在再进入前台时,计算时间差。若剩余的时间大于时间差,就减去时间差,否则赋值剩余时间为0。

方法二:苹果只允许三种情况下的App在后台可以一直执行:音视频、定位更新、下载,若是直播、视频播放、地图类、有下载的应用可以这样使用,但是有些小需求就不需这样做。

方法三:通过向苹果的系统申请,在后台完成一个Task任务。

2、NSTimer:滑动时停止

2内存-②循环引用Timer.md》 中的 【3、NSTimer和NSRunLoop的关系?】

Instrument

一、耗时/卡顿

卡顿的原因:

设备通常以60Hz的频率刷新屏幕,即每16.67毫秒刷新一次。如果在16.67ms内帧缓冲区没有准备好下一帧数据就会使画面停留在上一帧,造成丢帧,没法达到60fps,从而形成卡顿的现象。

每段16.67ms,其实是一个VSync信号,该信号用于保证只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中

3视图-更新机制.md

在VSync信号到来后,系统图形服务通过CADisplayLink等机制通知App,App主线程开始在CPU中计算显示内容,包括视图的创建、布局计算、图片解码、文本绘制等。然后CPU将计算好的内容提交到GPU,由GPU进行变换、合成、渲染,并将渲染结果提交到帧缓冲区,等待下一次VSync信号到来时显示到屏幕上

iOS默认刷新频率是60HZ,所以GPU渲染只要达到60fps就不会产生卡顿。

只要能使CPU的计算和GPU的渲染能在规定时间内完成,就不会出现卡顿。所以目标是减少CPU和GPU的资源消耗。

卡顿造成的原因是CPU和GPU导致的掉帧引起的:

  • 主线程在进行大量I/O操作:直接主线程写入大量数据
  • 主线程进行大量计算:主线程进行大量复杂的计算
  • 大量UI绘制:界面过于复杂,绘制UI需要大量的时间
  • 主线程在等锁

iOS卡顿优化

https://github.com/dvlproad/CJOptimizeProject/blob/main/CJOptimizeProject/TSDemo_Optimize/FPS/TSOptimizeTableViewController.m

我们写个列表,计算高度时候模拟耗时,且加载图片时候不使用缓存。代码示例如下:

image-20241027010604427

则经在Instrument中跑起来可查出耗时的位置即在上述①②。

image-20241027010515562

点击进入也能看到详细的时间分布

image-20241027015737011

二、Leak

https://github.com/dvlproad/CJOptimizeProject/blob/main/CJOptimizeProject/TSDemo_Optimize/Leak/TSLeakViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 内存泄漏示例--循环引用导致的
- (void)leakMemoryCircularRreference:(UIButton *)button {
//__weak typeof(self) weakSelf = self;
//__strong __typeof(self) strongSelf = weakSelf;

TSLeakViewController *viewController = [[TSLeakViewController alloc] init];
if ([button.titleLabel.text containsString:@"会泄露"]) {
viewController.leakBlock = ^(NSString * _Nonnull title) {
viewController.title = title; // 会泄露
};
} else {
__weak typeof(viewController) weakViewController = viewController;
viewController.leakBlock = ^(NSString * _Nonnull title) {
//self.title = title; // 不会泄露
weakViewController.title = title; // 不会泄露,使得在 Block 中不会出现对 自身 的强引用

//__strong __typeof(viewController) strongViewController = weakViewController;
//strongViewController.title = title; // 不会泄露
};
}
[self.navigationController pushViewController:viewController animated:YES];
}

ios Instruments之Allocations

ios_alloction_01

Flutter FPS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Widget buildList(BuildContext context) {
return ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
preBuildItem(index); // 模拟耗时操作,例如使用延时

int imageIndex = index % 10;
String imageName = "assets/images/cqts_${imageIndex + 1}.jpg";
return Container(
color: index % 2 == 0 ? Colors.red : Colors.blue,
child: Row(
children: [
JankText("Item $index"),
Image(
image: AssetImage(imageName),
width: 100,
height: 100,
),
],
),
);
},
);

通过进出测试FPS页面,看到有红色区域,点击看到BUILD时长很长,理论上应该是一群绘制,结果在绘制前有一堆控的时间。所以点击BUILD段分析,其中有标记⑦的方法耗时97.28%。即为我们设置进去模拟卡顿的地方。

image-20241029012336176

BUILD分析完已找出第一个造成卡顿的方法。我们继续往BUILD里的JankText分析,发现其也是在绘制Text前有很长一段被其他占用的方法。至此两处耗时操作都找出来了。

image-20241029012559483

内存

image-20241029015507146

flutter_leak_memory_01

image-20241029015611284

image-20241029015635482

通过比较进入后的列表页面的内存情况 main-3 与 未进入前的首页的内存情况 main-2,得到进入到列表后,增加的内存为如下:

image-20241029015741757

通过比较从列表页返回的首页内存情况 main-4 与 未进入前的首页的内存情况 main-2,得到返回到首页后,内存无增加,即正常全部释放:

image-20241029015830539

启动优化

App Launch

image-20241030221807138

寻找卡顿的切入点

监控卡顿,最直接就是找到主线程都在干些啥玩意儿。

我们知道一个线程的消息事件处理都是依赖于NSRunLoop来驱动,所以要知道线程正在调用什么方法,就需要从NSRunLoop来入手

参考文章

iOS 性能监控(一)—— CPU功耗监控

iOS 性能监控(三)—— 方法耗时监控

iOS-卡顿监测-FPS监测

iOS实时卡顿检测-RunLoop(附实例)

性能监控-②其他

性能优化

[toc]

目录

帧率优化?卡顿优化?

Flutter DevTools 的视频使用教程:轻松调试和提高 APP 性能

一、耗电量、耗流量优化

< 返回目录

通过

1
2
3
4
5
6
①优化位置服务(尽量降低定位精度)、
②网络操作(减少传输、压缩数据、缓存数据)、
③任务处理(减少任务处理量、按需处理,常见于一些后台任务的处理,比如不需要计算里程时候,鹰眼服务可以先关闭)、
④内容更新(减少app使用的视图数量、去除不必要的内容更新)、
⑤定时器(降低触发频率、及时关闭不再需要的重复性定时器)
>

优化参考文章:iOS进阶–App功耗优化看这篇就够了

定位服务:按需取用,定位频率该降低降低,该关闭关闭

根据位置特性对静止不动的点、位置变化小的点、位置变化大的点,结合不同业务决定位置上报情况,减少不必要的上报,降低耗电量与节省流量。

相似问题参考:解决iOS地图持续定位耗电问题

网络请求优化的

这个需要和后台API一起优化,尽量减少不必要的请求,比如一次API请求尽量把客户端要用到的数据都返回过来,而不是要通过多个请求去返回,同时最好注意数据分页,不要几万条数据都扔给客户端了。

CPU:

使用 Instruments 中的 Time Profile 时间分析工具用来检测应用CPU的使用情况。 定位 app 使用过程中占用高CPU、耗时长的地方。

使用方法参考:Instrument 的 Time Profiler总结

Timer:合理使用Timer数和Timer时间间隔,不宜太短,满足需求即可

设置上报检测计时器(一般设为1秒,但合理的是取不同业务上报频率的最大公约数,比如报班状态下需要5秒上传一次位置,未报班状态只需要20上传一次位置,则取5秒)

合理使用线程,线程适量,不宜过多,不要阻塞主线程

太多线程会导致消耗大量内存(在iOS中,如果把需要消耗大量时间的操作放在主线程上面,会妨碍主线程中被称为RunLoop的主循环的执行,从而导致不能更新用户界面、应用程序的画面长时间停滞等问题。)。

优化算法,减少循环次数

还有关键的就是图片尺寸了,最好客户端需要啥尺寸,服务端就直接给啥尺寸,而不是到客户端上再缩放。

二、内存优化、内存泄露处理

< 返回目录

1、正确的地方使用 reuseIdentifier

2、内存泄漏

1、使用Product-Analyze分析内存泄
利用Product-Analyze分析内存泄露,并不能把所有的内存泄露查出来,因为有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。

2、使用Instruments检测定位并解决iOS内存泄露

三、其他优化

< 返回目录

问题:优化多线程处理,改善多线程嵌套严重,请求耗时的问题

解决:优化多线程处理,改善多线程嵌套严重,请求耗时的问题。

详细:原本项目,采用多线程嵌套的同步方式处理多个线程请求到数据后,再执行最后操作。经优化多线程处理为异步执行时,改善了多线程嵌套严重,请求耗时的问题。

定时器使用的优化

问题:定时器多,其在主线程

为什么要在非主线程创建NSTimer

将 timer 添加到主线程的Runloop里面本身会增加线程负荷
如果主线程因为某些原因阻塞卡顿了,timer 定时任务触发的时间精度肯定也会受到影响
有些定时任务不是UI相关的,本来就没必要在主线程执行,给主线程增加不必要的负担。当然也可以在定时任务执行时,手动将任务指派到非主线程上,但这也是有额外开销的。

iOS 应用性能调优其他参考:

耗时(instruments的Time Profiler)、卡顿(主线程)

iOS app性能优化的那些事

iOS应用性能调优的25个建议和技巧

四、渲染

参考文章:iOS 渲染原理解析

1、渲染原理CPU 与 GPU

  • CPU(Central Processing Unit):现代计算机整个系统的运算核心、控制核心。
  • GPU(Graphics Processing Unit):可进行绘图运算工作的专用微处理器,是连接计算机和显示终端的纽带。

GPU 的渲染流程图

GPU 的渲染流程图

1、Application 应用处理阶段:得到图元

这个阶段具体指的就是图像在应用中被处理的阶段,此时还处于 CPU 负责的时期。在这个阶段应用可能会对图像进行一系列的操作或者改变,最终将新的图像信息传给下一阶段。这部分信息被叫做图元(primitives),通常是三角形、线段、顶点等。

2、Geometry 几何处理阶段:处理图元

3、Rasterization 光栅化阶段:图元转换为像素

光栅化的主要目的是将几何渲染之后的图元信息,转换为一系列的像素,以便后续显示在屏幕上。这个阶段中会根据图元信息,计算出每个图元所覆盖的像素信息等,从而将像素划分成不同的部分。

img

一种简单的划分就是根据中心点,如果像素的中心点在图元内部,那么这个像素就属于这个图元。如上图所示,深蓝色的线就是图元信息所构建出的三角形;而通过是否覆盖中心点,可以遍历出所有属于该图元的所有像素,即浅蓝色部分。

4、Pixel 像素处理阶段:处理像素,得到位图

经过上述光栅化阶段,我们得到了图元所对应的像素,此时,我们需要给这些像素填充颜色和效果。所以最后这个阶段就是给像素填充正确的内容,最终显示在屏幕上。这些经过处理、蕴含大量信息的像素点集合,被称作位图(bitmap)。

2. 屏幕成像与卡顿

屏幕撕裂 Screen Tearing

CPU+GPU 的渲染流程是一个非常耗时的过程。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,那么已扫描的部分和未扫描的部分就不是同一帧图像,这就造成屏幕撕裂。

解决屏幕撕裂、提高显示效率的一个策略就是使用垂直同步信号 Vsync 与双缓冲机制 Double Buffering。

屏幕卡顿的本质

手机使用卡顿的直接原因,就是掉帧。前文也说过,屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。

CALayer 与 UIView 的关系

当我们创建一个 UIView 的时候,UIView 会自动创建一个 CALayer,为自身提供存储 bitmap 的地方(也就是前文说的 backing store),并将自身固定设置为 CALayer 的代理。

核心关系

  1. CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
  2. UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理。

有了这两个最关键的根本关系,那么下面这些经常出现在面试答案里的显性的异同就很好解释了。举几个例子:

  • 相同的层级结构:我们对 UIView 的层级结构非常熟悉,由于每个 UIView 都对应 CALayer 负责页面的绘制,所以 CALayer 也具有相应的层级结构。
  • 部分效果的设置:因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。
  • 是否响应点击事件:CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
  • 不同继承关系:CALayer 继承自 NSObject,UIView 由于要负责交互事件,所以继承自 UIResponder。

当然还剩最后一个问题,为什么要将 CALayer 独立出来,直接使用 UIView 统一管理不行吗?为什么不用一个统一的对象来处理所有事情呢?

这样设计的主要原因就是为了职责分离,拆分功能,方便代码的复用。通过 Core Animation 框架来负责可视内容的呈现,这样在 iOS 和 OS X 上都可以使用 Core Animation 进行渲染。与此同时,两个系统还可以根据交互规则的不同来进一步封装统一的控件,比如 iOS 有 UIKit 和 UIView,OS X 则是AppKit 和 NSView。

必备知识架构-线程与网络-③网络

必备知识架构-线程与网络-③网络

[toc]

目录

1、传输层协议

传输层的主要协议有TCP、UDP。

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。应用于需要可靠传输的应用中,如网页浏览(HTTP)、文件传输(FTP)、邮件传输(SMTP)、远程登录(SSH)等。

  • UDP:适用于对实时性要求高的应用,如在线游戏、VoIP(Voice over IP)、视频会议、DNS查询等。

UDP是一种无连接的传输层协议。相比TCP不用建立三次握手,请求更快。适用于对实时性要求高的应用,如视频会议、在线游戏、实时监控等。

2、应用层协议

HTTP基于tcp,且默认是短连接,如果要长链接,则设置Connection “keep-alive”

WebSocket也是基于tpc,固定是长链接。

RTSP基于TCP或UDP,通常使用TCP来保证控制消息的可靠传输。

HTTP、RTSP都是应用层、WebSocket也可算是应用层(在四层模型:TCP/IP模型中,应用层是一个综合层,它包括了七层模型:OSI模型中的会话层、表示层和应用层的功能。)。

九、在一个https连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么

< 返回目录

回答这个问题,我们必须要先了解,一次完整的HTTP请求过程,大概是什么样的。

点击登录和服务器返回这个请求前,中间发生的起始就是一个域名解析和一次完整的http请求过程。

域名解析:即请求DNS服务器,进行域名解析。DNS服务器负责将你的网络地址解析成IP地址,这个IP地址对应网上一台机器;

而一次完整的HTTP请求过程,下面介绍之。

1、一次完整的HTTP请求过程

一次完整的HTTP请求过程如下图所示:

一次完整的HTTP请求过程
详情可参考:一次完整的HTTP请求过程

HTTP与HTTPS的区别

http和https使用的是完全不同的连接方式。

区别统计:

①HTTP是不安全的(它的信息使用明文传输),而HTTPS是安全的(会进行加密);

②使用的端口也不同,http使用的是80端口,https使用的是443端口。

③HTTP无需证书,而HTTPS需要认证证书。

在网络模型中,HTTP工作于应用层,而HTTPS工作在传输层;

2、TCP三次握手

一个故事让大家明白为什么会有tcp三次握手

1
2
3
4
5
> 敌人封锁江面, 我方间谍和联络员只能通过电报机隔着江面交流. 但是那时的电报机质量不稳定, 有可能会出现失灵的情况. 所以就出现了如下对话:
间谍: 联络员, 你能收到我发的话么? 你要是能收到, 就说明我这个电报机可以发电报.
联络员: 间谍, 我收到你发的话了, 这说明我的电报机可以收. 但是我不确定我的电报机能不能发, 你能收到我发的这句话么? 你要是能收到, 就说明我的电报机是可以发电报的.
间谍: 联络员, 我收到你的话了. 我的电报机也是能发能收, 我们可以正式交流情报了.
>

TCP三次握手过程

第一次握手: 客户端给服务器发送一个含有同步序列号SYN 报文。

主机A通过向主机B 发送一个含有同步序列号SYN的标志位的数据段给主机B ,向主机B 请求建立连接,通过这个数据段,主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我.

第二次握手: 服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。

主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用哪佧序列号作为起始数据段来回应我

第三次握手: 客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。

主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:”我已收到回复,我现在要开始传输实际数据了,这样三次握手就完成了,主机A和主机B 就可以传输数据了.

服务器收到 ACK 报文之后,三次握手建立完成。

附:

TCP三次握手的作用是为了确认双方的接收与发送能力是否正常。

握手过程中传送的包里正常不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据

但其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

3、TCP四次挥手

1
2
3
4
5
6
> 举个栗子:把客户端比作男孩,服务器比作女孩。通过他们的分手来说明“四次挥手”过程。
> "第一次挥手":日久见人心,男孩发现女孩变成了自己讨厌的样子,忍无可忍,于是决定分手,随即写了一封信告诉女孩。
> “第二次挥手”:女孩收到信之后,知道了男孩要和自己分手,立马给男孩写了一封回信:分手就分手,给我点时间,我要把你的东西整理好,全部还给你!男孩收到女孩的第一封信之后,明白了女孩知道自己要和她分手。随后等待女孩把自己的东西收拾好。
> “第三次挥手”:过了几天,女孩把男孩送的东西都整理好了,于是再次写信给男孩:你的东西我整理好了,快把它们拿走,从此你我恩断义绝!
> “第四次挥手”:男孩收到女孩第二封信之后,知道了女孩收拾好东西了,可以正式分手了,于是再次写信告诉女孩:我知道了,这就去拿回来!
>

4、为什么TCP建立连接要进行3次握手,而断开连接要进行4次?

35 张图解:被问千百遍的 TCP 三次握手和四次挥手面试题

详解 TCP 连接的“ 三次握手 ”与“ 四次挥手 ”/为什么“握手”是三次,“挥手”却要四次?

建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。

释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。

所以是“三次握手”,“四次挥手”。

tcp_三次握手tcp_四次挥手

四次握手与三次握手

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。

为什么需要四次挥手。

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

5、TCP两次握手会出现什么问题

《计算机网络》(谢希仁 译)中讲了原因:
1.采用两次握手,那么若Client向Server发起的包A1如果在传输链路上遇到的故障,导致传输到Server的时间相当滞后,在这个时间段由于Client没有收到Server的对于包A1的确认(如果有确认,即Server有收到的话,会发送回一个B1包),那么就会重传一个包A2(应该是等超时了),假设服务器正常收到了A2的包,然后返回确认B2包。由于没有第三次握手,这个时候Client和Server就被认为是已经建立连接,可以传输数据了。

接着假设Client发送的第一个A1包随后在链路中传到了Server,对于Server来说这是一个新连接请求,然后Server又为这个连接申请资源,返回B1包,但是由于Client现在用的是A2包,A1包是无效的,Client对于返回的B1包也不会去理会,即后面建立的这个连接其实是没用的(相当于“僵尸”的连接),Server一直为这个连接维持着资源,造成资源的浪费。

所以采用两次握手,有可能会浪费Server的网络资源。

TCP的三次握手最主要是防止已过期/失效的连接再次传到被连接的主机。

为什么要进行第三次握手?

为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

十、TCP与UDP

< 返回目录

1、TCP与UDP的区别

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!
UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。

TCP与UDP的区别:

1
2
3
4
5
1. 基于连接与无连接;
2. 对系统资源的要求(TCP较多,UDP少);
3. UDP程序结构较简单;
4. 流模式与数据报模式 ;
5. TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

2、为什么说UDP是不可靠的?

答:UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议
也正由于上面的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。

也正由于上面的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。

所以采用TCP传输协议的MSN比采用UDP的QQ传输文件慢,
但并不能说QQ的通信是不安全的,因为程序员可以手动对UDP的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,即使是这样,UDP因为在底层协议的封装上没有采用类似 TCP的“三次握手”而实现了TCP所无法达到的传输效率。

3、移动端IM系统的协议选型:UDP还是TCP?

移动端IM系统的协议选型:UDP还是TCP?

TCP还是UDP?长连接如何实现?如何实现心跳机制?心跳的间隔如何确定?这些问题都是讨论移动端IM消息推送等类似话题时,几乎一定被问到的问题。

个人认为,更恰当的方式应该是:两种通信协议同时使用,各有侧重。UDP用于保持大量终端的在线与控制,应用与业务则通过TCP去实现。这个和FTP服务控制与数据分离,采取不同的连接,有异曲同工之处。
事实上,这个也是即时通讯巨头QQ所采用的方式。早期的时候,QQ还是主要使用TCP协议,而后来就转向了采用UDP的方式来保持在线,TCP的方式来上传和下载数据。现在,UDP是QQ的默认工作方式,表现良好。相信这个也被沿用到了微信上。
简单的考证:登录PC版QQ,关闭多余的QQ窗口只留下主窗口,并将其最小化。几分钟过后,查看系统网络连接,会发现QQ进程已不保有任何TCP连接,但有UDP网络活动。这时在发送聊天信息,或者打开其他窗口和功能,将发现QQ进程会启用TCP连接。

十一、网络数据缓存

< 返回目录

iOS开发网络篇—数据缓存

RTMP VS RTSP

RTMP(Real-Time Messaging Protocol)和RTSP(Real-Time Streaming Protocol)都是用于流媒体数据传输的协议,但它们在设计和应用上有一些关键的区别。

  • RTMP是基于TCP的协议,RTSP是一种基于TCP或UDP的网络控制协议。
  1. 延迟
    • RTMP的延迟通常在3-30秒,适合于对实时性要求不是极高的应用。
    • RTSP的延迟通常在2-5秒,适合于对实时性要求较高的应用。
  2. 编解码器支持
    • RTMP通常支持H.264视频编码和AAC音频编码。
    • RTSP支持多种视频编码,包括H.265、H.264、VP9等。

在选择使用RTMP还是RTSP时,需要根据具体应用场景、对实时性的要求、客户端兼容性以及安全性需求来决定。例如,如果需要与HTML5兼容且对实时性要求不高,可能会选择基于HTTP的流媒体协议如HLS或DASH。如果需要低延迟和复杂的交互控制,则可能会选择RTSP。而RTMP可能更适合那些已经建立在Flash基础上的旧系统。

1、RTSP

RTSP(Real-Time Streaming Protocol)和TCP、UDP都是网络协议,它们在数据传输和流媒体领域中扮演着重要的角色。下面我将分别介绍它们的特点和用途:

  1. RTSP(Real-Time Streaming Protocol)
    • 定义:RTSP是一种网络控制协议,用于控制流媒体服务器上的媒体流。它允许客户端发出播放、暂停、停止等控制命令。
    • 用途:RTSP常用于视频监控系统中,通过它可以实现对视频流的控制,如请求视频流的开始、停止、快进、倒退等。
    • 特点:
      • 基于TCP或UDP,通常使用TCP来保证控制消息的可靠传输
      • 它不传输媒体本身,而是控制媒体流的传输,媒体数据通常通过RTP(Real-time Transport Protocol)传输。
      • 支持多种媒体类型,包括音频和视频。
  2. TCP(Transmission Control Protocol)
    • 定义:TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。
    • 用途:TCP广泛应用于需要可靠传输的应用中,如网页浏览(HTTP)、文件传输(FTP)、邮件传输(SMTP)等。
    • 特点:
      • 确保数据包按顺序到达,如果数据包丢失,会重新发送。
      • 建立连接需要三次握手,断开连接需要四次挥手,这增加了一定的延迟。
  3. UDP(User Datagram Protocol)
    • 定义:UDP是一种无连接的传输层协议,它在IP协议的基础上提供一种简单的数据传输服务。
    • 用途:UDP适用于对实时性要求高的应用,如视频会议、在线游戏、实时监控等。
    • 特点:
      • 不保证数据包的顺序、完整性或可靠性,如果数据包丢失,不会重发。
      • 没有建立连接的过程,发送数据前不需要握手,因此延迟较低。
      • 适用于那些可以容忍一定丢包率的应用,或者应用层自己实现数据的重传和顺序控制。

在实际应用中,RTSP通常与RTP(Real-time Transport Protocol)配合使用,RTSP用于控制媒体流,而RTP用于实际传输媒体数据。RTP可以选择在TCP或UDP上运行,但在流媒体传输中,UDP更常见,因为它的低延迟特性更适合实时媒体流的传输。而TCP则因其可靠性,适用于需要确保数据完整性的场景。

2、RTP

实时传输协议(RTP)是一种网络协议,专门设计用于在互联网上传输音频和视频数据。它通常与实时传输控制协议(RTCP)一起使用,后者用于监控数据传输的质量和提供反馈。RTP 被设计为一个基于数据包的协议,将媒体流分成数据包进行传输,每个数据包都有一个序列号,使得接收方能够以正确的顺序重新组合数据包。此外,RTP 还包括一个时间戳,允许接收器同步音频和视频流。

RTP 通常用于各种实时音频和视频传输的应用,如IP语音(VoIP)、视频会议、流媒体和广播电视等。它被广泛支持,并经常与其他协议如RTSP和SIP结合使用,以在互联网上传输音频和视频内容。

RTP 数据包的结构包括一个固定头部和可选的扩展头部,以及实际的媒体数据。

网络 | http常见错误码

  • HTTP状态码介绍,以及HTTP错误代码的故障修复?

    HTTP 状态码是一个客户发出请求时候,WEB服务器返回给客户的一个状态回应,常见HTTP状态有以下五类:

    1.信息代码:1xx,

    2.成功代码:2xx,

    3.重定向:3xx,

    4.客户端错误:4xx,

    5.服务器错误:5xx

    其中,我们需要关注的是客户端和服务器端错误代码,即4xx和5xx代码以及这些代码的常见解决方案。

    客户端错误,从400到499编码的客户端错误是由客户端引起的某些错误导致的,该客户端是Web浏览器、curl命令或postman等其他服务器测试软件发出的请求等。

    服务器错误,当服务器发生错误或服务器识别出无法处理请求时,将发送代码为500到599的服务器错误。

常见状态码:

一些常见的状态码为:

  • 200 – 服务器成功返回网页

  • 302

  • 304(未修改)

  • 404 – 请求的网页不存在

  • 503 – 服务器超时

1xx(临时响应)

表示临时响应并需要请求者继续执行操作的状态代码。

2xx (成功)

表示成功处理了请求的状态代码。

3xx (重定向)

表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。

4xx(请求错误)

这些状态代码表示请求可能出错,妨碍了服务器的处理。

5xx(服务器错误)

这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

AFNetworking 302 重定向

所有的AF请求都会在post get等请求前执行下面这个方法,所以只需重写这个方法就能拦截请求

1
2
3
4
5
6
@interface AFURLSessionManager ()
// 省略一堆代码
@property (readwrite, nonatomic, copy) AFURLSessionTaskWillPerformHTTPRedirectionBlock taskWillPerformHTTPRedirection;
// 省略一堆代码

@end

使用举例如下:

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
manager = [AFHTTPSessionManager manager];
[manager setTaskWillPerformHTTPRedirectionBlock:^NSURLRequest * _Nonnull(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSURLResponse * _Nonnull response, NSURLRequest * _Nonnull request) {
NSHTTPURLResponse *resPonse = (NSHTTPURLResponse *)response;
//1、如果要忽略重定向
/*
if (resPonse.statusCode == 302){
return nil;
}else {//正常请求
return request;
}
*/
// 2、通过重定向url去获取数据
if (resPonse.statusCode == 302){//如果响应code==302 就是重定向
NSMutableURLRequest *res = [NSMutableURLRequest requestWithURL:request.URL];
//通过抓包发现请求头不见了 所以在这里添加请求头,如果没有这个需求的可以忽略
NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:request.allHTTPHeaderFields];
NSString *valueStr = [NSString stringWithFormat:@" %@",localToken];
[headers setObject:valueStr forKey:@"Authorization"];
res.allHTTPHeaderFields = headers;

return res;
}else {//正常请求
return request;
}
}];

常见笔试/面试题

< 返回目录

1、简要说下Http通信协议的原理,与Socket协议的区别有哪些?

答:HTTP协议:简单对象访问协议,对应于应用层,HTTP协议是基于TCP连接的

tcp协议:对应于传输层

ip协议:对应于网络层

TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,才能使用TCP/IP协议。

http连接:http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉;

socket连接:socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该连接以释放网络资源。

2、cookie 和session 的区别:

1、cookie数据存放在客户的浏览器上,session数据放在服务器上。

2、安全性:
cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗。
考虑到安全应当使用session。

3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能
考虑到减轻服务器性能方面,应当使用COOKIE。

4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

5、所以个人建议:
将登陆信息等重要信息存放为SESSION
其他信息如果需要保留,可以放在COOKIE中

END

< 返回目录

第三方库SDWebImage①缓存-①NSCache

必备知识架构-第三方库SDWebImage①缓存-①NSCache

[toc]

一、NSCache的认识

1、为什么内存缓存要基于 NSCache?

NSCache和NSMutableDictionary,它们是iOS中常用的两个缓存类,基本上相同,都是健-值形式的内存缓存方式

1、NSCache的使用很方便,提供了类似可变字典的使用方式,但它比可变字典更适用于实现缓存,最重要的原因为NSCache是线程安全的,使用NSMutableDictionary自定义实现缓存时需要考虑加锁和释放锁NSCache已经帮我们做好了这一步,即在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问NSCache。

2、其次,在内存不足时NSCache会自动释放存储的对象,不需要手动干预,如果是自定义实现需要监听内存状态然后做进一步的删除对象的操作。

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
@interface NSCache <KeyType, ObjectType> : NSObject {
@private
id _delegate;
void *_private[5];
void *_reserved;
}

@property (copy) NSString *name;

@property (nullable, assign) id<NSCacheDelegate> delegate;

- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
- (void)removeObjectForKey:(KeyType)key;

- (void)removeAllObjects;

// NSCache可以指定缓存的限额,当缓存超出限额自动释放内存
// ①对象缓存可持有最大的数量 ,默认是0 没有限制),一旦超出限额,会自动删除之前添加的缓存数据
@property NSUInteger countLimit; // limits are imprecise/not strict
// ②缓存中可持有的最大空间 默认是0(没有限制)
@property NSUInteger totalCostLimit; // limits are imprecise/not strict

// 管理丢弃内容
// 是否可以自动缓存清除可丢弃的内容,默认是YES
@property BOOL evictsObjectsWithDiscardedContent;

@end

3、还有一点就是NSCachekey不需要实现NSCopying协议,因为NSCache的键key不会被复制/拷贝。在键key不支持拷贝操作的情况下,该类用起来比字典更方便。

2、NSCache什么时候会删除缓存中的对象

NSCache删除缓存中的对象会在以下情形中发生:

  • NSCache缓存对象自身被释放
  • 手动调用removeObjectForKey:removeAllObjects方法
  • 缓存中对象的个数大于countLimit,或缓存中对象的总cost值大于totalCostLimit
  • 程序进入后台后
  • 收到系统的内存警告

二、SDMemoryCache的认识

以上已说明内存缓存要基于 NSCache,所以SDMemoryCache要继承于NSCache,源码如下:

1
2
3
4
5
6
7
8
9
10
// SDMemoryCache.h

/**
A memory cache which auto purge the cache on memory warning and support weak cache.
*/
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType> <SDMemoryCache> // 请注意这里除继承 NSCache <KeyType, ObjectType> 外,还有一个 与 SDMemoryCache 类名同名的 <SDMemoryCache> 协议

@property (nonatomic, strong, nonnull, readonly) SDImageCacheConfig *config;

@end

可以看出类@interface SDMemoryCache还要遵守<SDMemoryCache>协议。虽然类@interface SDMemoryCache没提供什么方法,但与 SDMemoryCache 类名同名的<SDMemoryCache>协议提供了。

<SDMemoryCache>协议源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
A protocol to allow custom memory cache used in SDImageCache.
*/
@protocol SDMemoryCache <NSObject>

@required

- (nonnull instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;

- (nullable id)objectForKey:(nonnull id)key;

- (void)setObject:(nullable id)object forKey:(nonnull id)key;

- (void)setObject:(nullable id)object forKey:(nonnull id)key cost:(NSUInteger)cost;

- (void)removeObjectForKey:(nonnull id)key;
- (void)removeAllObjects;

@end

1、为什么另外建了个与类名SDMemoryCache同名的<SDMemoryCache>协议,并把方法提到了<SDMemoryCache>协议中?

答:为了当你想要要使用自定义的缓存类的时候,可以不用继承@interface SDMemoryCache,而只需遵循<SDMemoryCache>协议的方便。

设计模式-①概览.md

// 开闭原则:对扩展开放,对修改封闭.
// 里氏替换原则:应用程序中任何父类对象出现的地方,我们都可以用其子类的对象来替换,并且可以保证原有程序的逻辑行为和正确性。因为这里父类是抽象类,所以肯定遵守里氏替换原则。

2、内存缓存SDMemoryCache、磁盘缓存SDDiskCache的基类各是什么?

答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 内存类
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType> <SDMemoryCache>

@end


// 磁盘缓存类
@interface SDDiskCache : NSObject <SDDiskCache>

@property (nonatomic, strong, readonly, nonnull) SDImageCacheConfig *config;

- (void)moveCacheDirectoryFromPath:(nonnull NSString *)srcPath toPath:(nonnull NSString *)dstPath;

@end

三、内存缓存的设计

1、SDImageCache

id<SDMemoryCache> memoryCache;id<SDDiskCache> diskCache;

一个SDImageCache对象,由内存和磁盘共同控制缓存。控制的策略由SDImageCacheConfig类来定制。

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
@interface SDImageCache : NSObject

#pragma mark - Properties
@property (nonatomic, copy, nonnull, readonly) SDImageCacheConfig *config;

@property (nonatomic, strong, readonly, nonnull) id<SDMemoryCache> memoryCache;
@property (nonatomic, strong, readonly, nonnull) id<SDDiskCache> diskCache;

@property (nonatomic, copy, nonnull, readonly) NSString *diskCachePath;

@property (nonatomic, copy, nullable) SDImageCacheAdditionalCachePathBlock additionalCachePathBlock;

#pragma mark - Singleton and initialization

/**
* Returns global shared cache instance
*/
@property (nonatomic, class, readonly, nonnull) SDImageCache *sharedImageCache;

/**
* Init a new cache store with a specific namespace
*
* @param ns The namespace to use for this cache store
*/
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;

同时SDImageCache还有个类目

1
2
3
4
5
6
7
8
9
// SDImageCache.h
@interface SDImageCache (SDImageCache) <SDImageCache>

@end

// SDImageCacheDefine.h
@protocol SDImageCache <NSObject>

@end

内存和磁盘共同控制缓存策略定制类SDImageCacheConfig

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
// SDImageCacheConfig.h
@interface SDImageCacheConfig : NSObject <NSCopying>

// 是否使用内存做缓存,默认为YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

@property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground;

// 缓存图片的最长时间,单位是秒,默认是缓存一周
@property (assign, nonatomic) NSTimeInterval maxDiskAge;
// 缓存占用最大的空间,单位是字节
@property (assign, nonatomic) NSUInteger maxDiskSize;


@property (assign, nonatomic) NSUInteger maxMemoryCost;
@property (assign, nonatomic) NSUInteger maxMemoryCount;

/*
* The attribute which the clear cache will be checked against when clearing the disk cache
* Default is Modified Date
*/
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;



@end

2、SDImageCachesManager

管理NSArray<id> *caches;

一张图片就是一份SDImageCache元素对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
A caches manager to manage multiple caches.
*/
@interface SDImageCachesManager : NSObject <SDImageCache>

@property (nonatomic, class, readonly, nonnull) SDImageCachesManager *sharedManager;

@property (nonatomic, assign) SDImageCachesManagerOperationPolicy queryOperationPolicy;
@property (nonatomic, assign) SDImageCachesManagerOperationPolicy storeOperationPolicy;
@property (nonatomic, assign) SDImageCachesManagerOperationPolicy removeOperationPolicy;
@property (nonatomic, assign) SDImageCachesManagerOperationPolicy containsOperationPolicy;
@property (nonatomic, assign) SDImageCachesManagerOperationPolicy clearOperationPolicy;

@property (nonatomic, copy, nullable) NSArray<id<SDImageCache>> *caches; // 一张图片就是一份SDImageCache元素对象。

- (void)addCache:(nonnull id<SDImageCache>)cache;
- (void)removeCache:(nonnull id<SDImageCache>)cache;

@end

###

END

第三方库SDWebImage①缓存-①缓存不更新问题

必备知识架构-第三方库SDWebImage①缓存-①缓存不更新问题

[Toc]

一、背景

后台图片内容换了,但是url还是老的,手机就用了缓存,没有从后台更新图片。

主要问题表现在哪里呢?
很多app都有用户的概念,用户一般都会有头像,基本上都上传到服务器上,而服务器往往也支持在pc端更新头像(比如微博、QQ等)。
如果你的头像使用SDWebImage设置的,那么你会发现,pc端更新头像后,客户端可能(往往)不会自动更新!!!

二、解决

问:使用SDWebImage如何加载url不变,但图片已经变化的图片。

答:解决方法可以有如下几种:

  1. 让服务器更新url,也就是说服务器端如果更新了头像,那么就生成新的url(推荐)

    后台给的url中增加字段,表示图片是否更新,比如增加一个timestamp字段.图片更新了,就更新下这个字段;
    对客户端来说,只要这个timestamp字段变了,整个url就不一样了,就会从网络取图片。比如http://xxx/xx? timestamp=xxx
    也可以添加图片文件的md5来表示文件是否更新,比如http://xxx/xx? md5=xxx。并且md5比时间戳要好,这是强校验。时间戳在服务器回滚或者服务器重启的时候会有特殊的逻辑。不过大多数时候时间戳也够用了。
    ====这个方案客户端不用改,后台改动也不会太大。====强烈推荐

  2. 客户端只使用内存缓存,不使用磁盘缓存,那么下次启动时候就会重新下载,从而得到最新的了。(缺点:本次使用过程中没能看到最新图片。且即使之后下载到最新图片了,由于只使用内存缓存,不使用磁盘缓存。导致程序关闭又打开之后,缓存就没了,需要访问网络,重新加载图片)

    SDWebImageCacheMemoryOnly这个参数对解决这个问题有帮助,只用内存缓存,不用磁盘缓存,App关了再开,肯定会重新下载,不会出现服务器和手机缓存图片不一致的情况。

  3. 客户端使用SDWebImageRefreshCached,同时让服务器端支持cache-control。

    SDWebImageRefreshCached,这个参数就是为了解决url没变但是服务器图片改变的问题,很适合当前的场景。方案就是磁盘缓存不自己实现了,直接使用NSURLCache。记得AFNetworking的大神Matt就曾经嘲笑过SDWebImage的缓存是多此一举,还不如系统的NSURLCache好用。

    SDWebImageRefreshCached参数设置之后,会怎么样?

    • 不使用SDWebImage提供的内存缓存和硬盘缓存

    • 采用NSURLCache提供的缓存,默认情况下有效时间只有5秒

    • 图片不一致的问题是解决了,不过效果跟不使用缓存差别不大。个人建议这个参数还是不要用为好,为了一个小特性,丢掉了SDWebImage最核心的特色。

      1
      2
      3
      4
      >   [imageview sd_setImageWithURL:[NSURL URLWithString:url]	
      > placeholderImage:nil
      > options:SDWebImageRefreshCached];
      >
  4. 不使用SDWebImage,自己控制缓存,用系统API实现(NSURLCache)实现。(缺点还是和3一样,得服务端配合更改。)

    主要也是和使用SDWebImageRefreshCached时候一样,会涉及到Cache-Control(设定缓存有效时间,默认是5s)Last-Modified/If-Modified-Since(时间戳)Etag/If-None-Match(标签,一般用MD5值)

附:其他options

SDWebImageRetryFailed表示就算下载失败也会再次尝试(不把下载失败的的url加入黑名单)

参考文章:

SDWebImage①缓存-②缓存原理

[toc]

七、多线程–第三方库SDWebImage

参考资料:

这个类库提供一个UIImageView类别以支持加载来自网络的远程图片。具有缓存管理、异步下载、同一个URL下载次数控制和优化等特征。

基本思路

SDWebImage 基本思路如下:

SDWebImage基本思路

1 扩展(category) UIImageView,这样写出的代码更整洁

2 GCD 异步下载

3 重用 UITableViewCell 加异步下载会出现图片错位,所以每次 cell 渲染时都要预设一个图片 (placeholder),

以覆盖先前由于 cell 重用可能存在的图片, 同时要给 UIImageView 设置 tag 以防止错位。

4 内存 + 文件 二级缓存, 内存缓存基于 NSCache

暂时没有考虑 cell 划出屏幕的情况,一是没看明白 SDWebImage 是怎么判断滑出屏幕并 cancel 掉队列中对应的请求的

二是我觉得用户很多情况下滑下去一般还会滑回来,预加载一下也挺好。坏处是对当前页图片加载性能上有点小影响。

1、SDWebImage在图片下载及缓存的处理方法

SDWebImage加载网络图片的原理图分析如下:
SDWebImage加载网络图片的原理图分析

其中图片的获取与存储过程大概如下:
SDWebImage theory

大概描述为:
注:SDWebImage中的SDWebImageDownloader有使用到GCD相关dispatch_barrier_sync
1)当我门需要获取网络图片的时候,我们首先需要的便是URl没有URl什么都没有,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URl相关的图片,如果有则直接返回image,如果没有则进行下一步。

2)当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。

3)如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。

4)刷新UI(当然根据实际情况操作)将image存入沙盒缓存。

缓存策略的源码解析:SDWebImage 源码解析—缓存策略

END