Oniityann


  • 首页

  • 分类

  • 归档

  • 标签

TableViewCell 点击执行 presentViewController 的坑

发表于 2018-12-05 | 分类于 小技巧

场景

在开发中遇到了一个点击 TableViewCell 自定义模态出另一个控制器的坑:

  • 尝试普通模态一个控制器,偶尔会有延迟显示。
  • 自定义模态动画,必定会延迟显示,或者根本就不显示,必须二次点击视图才会显示出来。

解决

一开始以为是自定义模态动画出了问题,打断点发现都是即时执行的,流程也没有问题,就是会延迟 N 秒以上才模态出目标控制器。
然后用 Time Profiler 跑了一下,发现有以下两种情况:

  1. 延迟执行的时候,RunLoop 会延迟唤醒
  2. 压根不显示的情况,UIEventFetcher 压根就没有显示,Runloop 没有被唤醒

那既然是 RunLoop 没有被唤醒,那就好解决了,尝试在模态代码前面手动唤醒 RunLoop:

1
2
3
4
5
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
CFRunLoopWakeUp(CFRunLoopGetCurrent());
ToViewController *to = [[ToViewController alloc] init];
[self presentViewController:to animated:YES completion:NULL];
}

问题解决。

然后谷歌了一下为什么 didSelectRow 时存在 runloop 没有唤醒的情况,发现 Stack Overflow,还有一些别的论坛也有遇到这种坑的人,看了下,基本结论就是苹果的 didSelectRow 方法的 bug,其他开发者给出的解决方案是:

  • 包一层主线程异步:
1
2
3
4
5
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
ToViewController *to = [[ToViewController alloc] init];
[self presentViewController:to animated:YES completion:NULL];
});
  • 不唤醒的情况只有在 TableViewCell 的 selectionStyle 为 none 的时候才发生, 这种情况修改 selectionStyle 也可以解决。

以上就是三个这种 bug 的解决方案。

Metal 初窥

发表于 2018-09-22 | 分类于 Metal

渲染

顶点 (vertice, vertex)

一个顶点就是两个或多个的线, 曲线, 集合图形的焦点.

一个 3D 渲染器将会通过 model loader 代码去阅读这些顶点, 这些代码解析了顶点的列表. 然后渲染器会传递这些顶点到 GPU, shader (着色器, 流处理器) 方法会去处理这些顶点, 去创建最终的图片或者纹理, 然后会重新传递到 CPU, 以便在屏幕上展示.

渲染通道 (Rendering Pipeline) 是一个传送到 CPU 的命令表. 渲染通道包含了可编程和不可编程的方法, 前者, 一般被认为是顶点方法 (vertex functions) 和碎片方法 (fragment functions), 是你可以手动影响你最终你看见的渲染的模型的方法.

帧

每个静态图像都被认为是一个 frame (帧), 图片出现的速率叫做帧率.

Metal 处理序列

屏幕快照 2018-09-10 下午9.14.30

MetalKit

MetalKit 有一个自己的视图 — MTKView。
MTKView 在 MacOS 上是 NSView 的子类,在 iOS 上是 UIView 的子类。

检测是否有合适的 GPU:

1
2
3
guard let device = device else {
fatalError("GPU is not supported")
}

创建一个 MTKView:

1
2
3
let frame = CGRect(x: 75/2.0, y: 96, width: 300, height: 300)
let view = MTKView(frame: frame, device: device)
view.clearColor = MTLClearColor(red: 1, green: 1, blue: 0.8, alpha: 1)

MTKClearColor 代表了一个 RGBA 色值。色值被存储在 clearColor 中, 用这个值设置 MTKView。

创建一个 MTLCommandQueue 对象:

1
2
3
guard let commandQueue = device.makeCommandQueue() else { 
fatalError("Could not create a command queue")
}

Device 和 Command 需要在一开始就设置, 设置一次, 各处使用.

The Model - Load a model

Model I/O 是一个整合 Metal 和 SceneKit 的框架. 它的主要目的是加载 3D 模型, 设置 data buffer 以便渲染.

1
2
3
4
5
6
7
// 1. 创建一个网格数据内存管理器
let allocator = MTKMeshBufferAllocator(device: device)
// 2. Model I/O 创建一个有着特定大小的球体
// 并且返回一个 MDLMesh 对象, buffers 中有着所有的顶点信息
let mdlMesh = MDLMesh(sphereWithExtent: [0.75, 0.75, 0.75], segments: [100, 100], inwardNormals: false, geometryType: .triangles, allocator: allocator)
// 3. 为了让 Metal 可以使用这些网格, 从一个 Model I/O mesh 转换成一个 MetalKit mesh
let mesh = try! MTKMesh(mesh: mdlMesh, device: device)

Shader Functions

Shader Functions 是跑在 GPU 上的一小段代码, 用 Metal Shading Language (着色语言) 来写它们, 是一段 C++ 的代码子集.

通常情况下, 你会为 shader functions 创建一个独立的文件, 后缀为 .metal

The vertex function is where you usually manipulate vertex positions and the fragment function is where you specify the pixel color.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let shader = """
#include <metal_stdlib> \n
using namespace metal;

struct VertexIn {
float4 position [[ attribute(0) ]];
};

vertex float4 vertex_main(const VertexIn vertex_in [[ stage_in ]]) {
return vertex_in.position;
}

fragment float4 fragment_main() {
return float4(1, 0, 0, 1);
}
"""

// 创建一个包含 shader 中两个方法的 Metal 库
let library = try device.makeLibrary(source: shader, options: nil)
let vertexFunction = library.makeFunction(name: "vertex_main")
let fragmentFunction = library.makeFunction(name: "fragment_main")

The compiler will check that these functions exist and make them available to a pipeline descriptor (描述符号).

The Pipeline State - Setup the pipeline

在 Metal 中, 你为 GPU 设置一个通道状态, 通过这是这种状态, 你告诉 GPU, 除非状态改变, 什么都不会改变, 并且 GPU 可以运行更高效。通道状态包含了所有 CPU 需要的信息, 例如, 它需要用到的像素格式化和它是否需要渲染深度, 此外, 它也持有你创建的顶点和碎片方法。

然而, 你不直接创建通道状态, 而是通过创建一个 descriptor 来创建它。descriptor 持有了所有通道需要知道的东西, 并且, 为了那些特殊的渲染场景, 你仅仅是改变那些必要的属性即可:

1
2
3
4
5
6
7
8
9
10
// 创建一个 pipeline descriptor
let descriptor = MTLRenderPipelineDescriptor()
// 设置像素格式化
// .bgra8Unorm: Ordinary format with four 8-bit normalized unsigned integer components in BGRA order.
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
descriptor.vertexFunction = vertexFunction
descriptor.fragmentFunction = fragmentFunction
// MTKMetalVertexDescriptorFromModelIO(_:)
// 返回一个部分被转换的 Metal 顶点描述符号
descriptor.vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(mesh.vertexDescriptor)

vertex descriptor 描述了那些顶点如何在 Metal buffer 中被布局的。
当 Model I/O 加载球体网格的时候, 会自动创建顶点描述符号。
通过 descriptor 创建 pipeline state:

1
let pipelineState = try device.makeRenderPipelineState(descriptor: descriptor)

Creating a pipeline state takes valuable processing time, so all of the above should be a one-time setup. In a real app, you might create several pipeline states to call different shading functions or use different vertex layouts.

Rendering

MTKView 有一个代理方法, 可以运行每一帧。

MTKView provides a render pass descriptor and a drawable.

1
2
3
4
5
6
7
8

guard let commandBuffer = commandQueue.makeCommandBuffer(),

let descriptor = view.currentRenderPassDescriptor,

let renderEncoder =
commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
else { fatalError() }

Realm 多线程通信

发表于 2018-08-26 | 分类于 Thread

公司的 App IM 模块要求用 Realm 做数据库,不可避免的在接收消息和发送消息时,读写数据要放在后台线程去做。这样不会影响聊天页面的 UI 显示。一开始涉及到多线程的时候 Realm 挺多坑的。

Realm 多线程特性

Realm 是基于零拷贝架构,所有对象是鲜活的而且自动更新。如果 Realm 允许对象可在线程间共享,Realm 会无法确保数据的一致性,因为不同的线程会在不确定的什么时间点同时改变对象的数据。这样数据很快就不一致了。一个线程可能需要写入一个数据而另一个线程也打算读取它,反过来也可能。这很快就会变得有问题了,而且你不能够在相信哪个线程能有正确的数据了。

Realm 通过确保每个线程始终拥有 Realm 的一个快照,以便让并发运行变得十分轻松。你可以同时有任意数目的线程访问同一个 Realm 文件,并且由于每个线程都有对应的快照,因此线程之间绝不会产生影响。

您唯一需要注意的一件事情就是不能让多个线程都持有同一个 Realm 对象的实例。如果多个线程需要访问同一个对象,那么它们分别会获取自己所需要的实例(否则在一个线程上发生的更改就会造成其他线程得到不完整或者不一致的数据)。

RLMObject 的未管理实例(unmanaged) 表现的和正常的 NSObject 子类相同,可以安全地跨线程传递。

RLMRealm、RLMObject、RLMResults 或者 RLMArray 受管理实例皆受到线程的限制,这意味着它们只能够在被创建的线程上使用,否则就会抛出异常*。这是 Realm 强制事务版本隔离的一种方法。否则,在不同事务版本中的线程间,通过潜在泛关系图 (potentially extensive relationship graph) 来确定何时传递对象将不可能实现。

在多线程中使用 Realm

在同一个线程中访问 Realm 实例

上文说到 RLMRealm、RLMObject、RLMResults 或者 RLMArray 受管理实例皆受到线程的限制。如果在同一个线程中访问这些,那就不需要担心线程带来的数据错乱等问题。在实际操作中,可以开启一个常驻线程来操作数据库。

在不同的线程中重新获取 Realm 实例

无论是全局的RLMRealm实例,还是RLMObject,在不同的线程访问时都需要重新获取一个实例(每个线程都有一个 Realm 的快照),否则会报错。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在线程 1 中写入一个 dog
thread1 {
Dogs *dog = [[Dogs alloc] init];
dog.name = @"Chubby";

RLMRealm *r = [RLMRealm defaultRealm];
[r transactionWithBlock:^{
[Dogs createOr....:dog];
}];

thread2 {
RLMResults *dogs = [Dogs allObjects];
// 模拟取出刚才写入的 dog
Dog *dog = dogs.firstObject;
[[RLMRealm defaultRealm] transactionWithBlock:^{
// 修改 dog 的名字
dog.name = @"Chub"
}];
}
}

在线程 1 中,dog 已经被 managed,同时与当前线程绑定,如果此时想在另一个线程,必须重新在另一个线程中获取 RLMObject 的快照。

通过 RLMThreadSafeReference 来传递实例

  1. 通过受到线程限制的对象来构造一个 RLMThreadSafeReference;
  2. 将此 RLMThreadSafeReference 传递给目标线程或者队列;
  3. 通过在目标 Realm 上调用 -[RLMRealm resolveThreadSafeReference:] 来解析此引用。

例如:

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
thread1 {
Dog *dog = [Dog new];
dog.name = @"Chubby";
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[RLMRealm defaultRealm] add:dog];
}];

// 这时如果要在线程中传递这只 dog
// 1. 构造 RLMThreadSafeReference 实例
RLMThreadSafeReference *dogRef = [RLMThreadSafeReference
referenceWithThreadConfined:dog];

// 2. 将此 RLMThreadSafeReference 传递给目标线程或者队列
thread2 {
@autoreleasepool {
RLMRealm *r = [RLMRealm defaultRealm];

// 3. 通过 resolve 方法来获取 thread 1 中的 dog
Dog *dog = [r resolveThreadSafeReference:dogRef];

[r transactionWithBlock:^ {
dog.name = @"Chub";
}];
}
}
}

RLMThreadSafeReference 对象最多只能够解析一次。如果 RLMThreadSafeReference 解析失败的话,将会导致 Realm 的原始版本被锁死,直到引用被释放为止。因此,RLMThreadSafeReference 的生命周期应该很短。

基于 WebSocket 的 IM App 实现

发表于 2018-08-22 | 分类于 Socket

前言

三月份入职了一家做社交的公司,进公司以后,让我负责 IM 模块的开发,当时问为什么不用第三方,第一是因为价格问题,第二是因为前一批人没做好,技术主管一怒之下就说自己做了,使用 WebSocket 实现。这也得以让我有机会去研究一下如何去做一个 IM app。

移动端 IM/ 推送协议选型

在 www.52im.net 上有一篇帖子很好,直接贴出来。
链接:移动端IM/推送系统的协议选型:UDP还是TCP?

iOS Socket 编程

深入浅出Cocoa - iOS网络编程之Socket

WebSocket

在 WebSocket 的处理上,我选择了 Facebook 开源的 SocketRocket,因为它在网上的 demo 是最多的,而且使用起来确实很方便。

SocketRocket 源码解读

Subject

发表于 2018-07-27 | 分类于 RxSwift

什么是 Subjects

Subjects 的行为看起来就像一个 Observable 和一个 Observer。它可以接受事件并且也可以被订阅。Subject 接受 .next 事件并且每当它接受一个事件,他就会发送这个事件给它的订阅者。

RxSwift 中有四种事件:

  • PublishSubject:以 empty 的形式创建并且只发给 subscriber 新元素。
  • BehaviorSubject:以初始值的方式创建,重播或发出最新的元素给它的 subscriber。
  • ReplaySubject:以一个缓存尺寸创建并且会确保元素以缓存尺寸进入缓存区,并且重播给它的 subscriber。
  • Variable:封装了一个 BehaviorSubject,保护当前值作为状态,仅仅给订阅者重播最新或者初始化的值。

下面以例子分析每种 Subject 的使用。

PublishSubject

1
2
3
4
5
6
7
let subject = PublishSubject<String>()

subject.onNext("Test")

let subscription1 = subject.subscribe(onNext: { (string) in
print(string)
}, onError: nil, onCompleted: nil, onDisposed: nil)

此时,控制台并不会输出任何值。在上面的代码中加上以下代码:

1
2
subject.on(.next("1"))
subject.on(.next("2"))

此时控制台输出:
1 2

由此可见,在 PublishSubject 被订阅之前,subject 发出的 .onNext("Test") 事件并没有被订阅。

把代码修改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let subject = PublishSubject<String>()

subject.onNext("Test")

let subscription1 = subject.subscribe(onNext: { (string) in
print(string)
}, onError: nil, onCompleted: nil, onDisposed: nil)

subject.on(.next("1"))
subject.on(.next("2"))

let subscription2 = subject.subscribe({ (event) in
print("Subscription 2: ", event.element ?? event)
})

subject.on(.next("3"))

subscription1.dispose()

subject.on(.next("4"))

运行后控制台会输出:

1
2
3
4
5
1
2
3
Subscription 2: 3
Subscription 2: 4

由此可知,当一个订阅被 dispose 之后,它就不会在处理 subject 发出的新值。

当一个 PublishSubject 收到 .completed 或者 .error 事件后,它将会给自己的新订阅者发送停止事件,并且不会再发送 .next 事件。然而,它会重新发送它的停止事件给未来的订阅者:

1
2
3
4
5
6
7
8
9
10
11
12
13
subject.onCompleted()

subject.onNext("5")

subscription2.dispose()

let disposeBag = DisposeBag()
let subscription3 = subject.subscribe({
print("subscription 3:", $0.element ?? $0)
})
.disposed(by: disposeBag)

subject.onNext("6")

控制台输出:

1
2
Subscription 2: completed
subscription 3: completed

在收到 .completed 事件后,控制台并没有打印 5 和 6,但是打印了 subscription 3: completed。在收到停止事件后,重新给自己未来的订阅者 subscription3 发送了完成事件。

实际上,每种类型的 subject 在停止时,都会重新发送它的停止事件给未来的订阅者。

BehaviorSubject

BehaviorSubject 的工作原理和 PublishSubject 比较相似,他们的区别在于,BehaviorSubject 会重播最新的 .next 事件给它的订阅者:

1
2
3
4
5
6
7
8
9
10
// 提供一个初始值
let subject = BehaviorSubject(value: "Init")

let disposeBag = DisposeBag()

subject.onNext("1")

subject.subscribe({ (event) in
print("Subscription1:", (event.element ?? event.error ?? event) ?? "")
}).disposed(by: disposeBag)

控制台会输出:

1
Subscription1: 1

subject 重播了最新的值 1 给它的订阅者。

在上面的代码后面添加以下代码:

1
2
3
4
5
6
7
8
9
10

enum MyError: Error {
case anError
case anotherError
}

subject.onError(MyError.anError)
subject.subscribe({ (event) in
print("Subscription2:", (event.element ?? event.error ?? event) ?? "")
}).disposed(by: disposeBag)

输出:

1
2
Subscription1: anError
Subscription2: anError

最新值分别发送给了 subscription 1 和 2,anError。

BehaviorSubject 在空状态占位上很有用,例如,在一个 user profile 页面中,你可以给控件绑定一个 behaviorSubject。当 app 获取新数据的时候,可以先用最新值来占位。或者,在一个搜索页面,你可以展示最新的五个搜索数据。

ReplaySubject

当使用一个 ReplaySubject,它锁创建的缓存是被内存所持有的。如果你为摸个类型的 ReplySubject 设置一个很大的缓存尺寸,每个实例都会占据很大的内存。另一个需要注意的点是,创建一个数组的 ReplaySubject,每次发出的 element 是一个数组,因此,缓存区会缓存许多数组,这会引起内存压力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let subject = ReplaySubject<String>.create(bufferSize: 2)
subject.onNext("1")
subject.onNext("2")
subject.onNext("3")

let disposeBag = DisposeBag()

subject.subscribe({
print("Subscription1:", ($0.element ?? $0.error ?? $0) ?? "")
}).disposed(by: disposeBag)

subject.subscribe({
print("Subscription2:", ($0.element ?? $0.error ?? $0) ?? "")
}).disposed(by: disposeBag)

控制台会输出:

1
2
3
4
Subscription1: 2
Subscription1: 3
Subscription2: 2
Subscription2: 3

1 没有被发出,而 2、3 被发出了。因为上面代码创建的缓存区大小是 2,2 和 3 挤掉了 1。

继续添加下面的代码:

1
2
3
4
subject.onNext("4")
subject.subscribe({
print("Subscription3:", ($0.element ?? $0.error ?? $0) ?? "")
}).disposed(by: disposeBag)

输出:

1
2
3
4
Subscription1: 4
Subscription2: 4
Subscription3: 3
Subscription3: 4

因为输出的内容是按缓存区区分的,所以后面添加的第三个订阅者输出了 3 和 4,最新的两个事件。而订阅者 1 和订阅者 2 在上一个 buffer 中已经有了值,所以发出最新值 4。

下面来测试下 Error 情况,在第三个订阅之前加上:

1
2
3
4
5
6
subject.onNext("4")
// 新添加
subject.onError(MyError.anError)
subject.subscribe({
print("Subscription3:", ($0.element ?? $0.error ?? $0) ?? "")
}).disposed(by: disposeBag)

输出:

1
2
3
4
5
6
7
Subscription1: 4
Subscription2: 4
Subscription1: anError
Subscription2: anError
Subscription3: 3
Subscription3: 4
Subscription3: anError

这是因为上文提到过:每种类型的 subject 在停止时,都会重新发送它的停止事件给未来的订阅者。

在 onError 事件后添加:

1
2
3
4
5
subject.onError(MyError.anError)
subject.dispose()
subject.subscribe({
print("Subscription3:", ($0.element ?? $0.error ?? $0) ?? "")
}).disposed(by: disposeBag)

输出:

1
2
3
4
5
Subscription1: 4
Subscription2: 4
Subscription1: anError
Subscription2: anError
Subscription3: Object `RxSwift.(unknown context at 0x128dcdbc8).ReplayMany<Swift.String>` was already disposed.

在第三个订阅前手动调用 dipose,第三个订阅只会收到一个错误事件,说,subject 已经被销毁了。

通常情况下,不需要手动调用 dispose(),如果你给一个 subscription 添加了一个 disposeBag,那么当这个 subject 的持有者被销毁的时候,它也会一并销毁。

Variables

上文提到过,Variables 封装了一个 BehaviorSubject 并且存储了当前值。你可以通过 value property 去获取当前值,和其他 subjects 与通常的 observable 不一样的是,你也可以用那个 value property 去给一个 Variable 设置新的元素,换句话说,不用调用 onNext 方法。Variables 特殊的地方在于,它确保了不会发出一个 error 事件,尽管你可以去监听 .error 事件,但你不能手动添加一个 .error 事件给 Variable。Variable 同样会在它要被释放的时候发送 complete 事件,所以不需要手动添加 .completed 事件。

创建一个 Variable:

1
2
3
4
5
6
7
8
let variable = Variable("Init")
variable.value = "Re-init"

let disposeBag = DisposeBag()

variable.asObservable().subscribe({
print("Subscription:", ($0.element ?? $0.error ?? $0) ?? "")
}).disposed(by: disposeBag)

输出:

1
Subscription: Re-init

由控制台输出可知,订阅获得了最新值。这点和 BehaviorSubject 没什么区别,但是写法上,Variable 是需要先变成 Observable。

将刚才 Subscription 改为 Subscription1,继续测试新值:

1
2
3
4
5
6
variable.value = "1"
variable.asObservable().subscribe({
print("Subscription2:", ($0.element ?? $0.error ?? $0) ?? "")
}).disposed(by: disposeBag)

variable.value = "2"

输出:

1
2
3
4
Subscription1: 1
Subscription2: 1
Subscription1: 2
Subscription2: 2

之前的 subscription1 在 variable 设置新值 1 的时候收到了新值,当 subscription2 被订阅的时候也收到了同样的值,因为在 variable.value = "1" 时,1 就是最新值。然后 value 被更新为 2,同样的,两次订阅都收到最新值。

Variable 是非常有用的,主要有以下两点:

  1. 和其他的 subject 一样,无论什么时候,一个 next 事件被发出,它都会做出响应。
  2. 它可以提供“一次性”的需求,例如,当你仅仅需要检查当前值而不用去订阅获取更新。

以上就是四种 Subjects 的主要用法。后续会更新 Observable 和 Operator 的实战内容。

Observable

发表于 2018-07-21 | 分类于 RxSwift

什么是 Observable

Observables 是 Rx 的核心。

在 RxSwift 中,所有事情都是 sequence。Observable 也是一个 sequence,可被监听的 sequence。
Observable 最重要的功能就是异步操作,它在一定事件内会发出事件。

– 1 – 2 – 3 –>

从左向右的箭头代表了时间,数字代表了 sequence 元素。元素 1 发送后,接着 2 和 3 也被发出,这些事件就伴随着 Observable 的生命周期。

同样的,RACSignal 也是 RAC 的核心,如果用过 RAC,可以暂且把 Observables 当做 RACSignal 来学习。

Observable 生命周期

  • 当一个 Observable 发出一元素,就是说发出了一个 next event。
  • 当其结束分发事件后,就会发出一个 completed event,然后这个 Observable 就会被终止。
  • 如果分发事件的时候出现错误,就会发出一个 error event,同时它也会被终止。
  • 一旦一个 Observable 被终止,它也会停止分发事件。

创建 Observables

just - of - from

  • just 创建了一个只包含一个元素的 observable sequence。
  • of 创建的 sequence 类型由传入的元素类型所决定。
  • from 创建的 sequence,是从一个数组中获取的元素。
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
let one = 1
let two = 2
let three = 3

let observable: Observable<Int> = Observable<Int>.just(one)
// An observable of Int with 3 elements
let observable2 = Observable.of(one, two, three)
// An observable of [Int] with a single element - Array
let observable3 = Observable.of([one, two, three])
// An observable of Int
let observable4 = Observable.from([one, two, three])

observable.subscribe(onNext: { (int) in
print("just: \(int)")
// just: 1
})

observable2.subscribe(onNext: { (int) in
print("of: \(int)")
// of: 1
// of: 2
// of: 3
})

observable3.subscribe(onNext: { (int) in
print("of array: \(int)")
// of array: [1, 2, 3]
})

observable4.subscribe(onNext: { (int) in
print("from: \(int)")
// from: 1
// from: 2
// from: 3
})

订阅 Observables

在 RxSwift 中,订阅,subscribe(),就像 KVO 中的 addObserver()。但是不同的是,只有一个 subscriber 订阅了 observable,它才会发出 event。当 observable 调用 subscribe 操作时,在闭包中,它为每个元素发送了 .next event,然后发送 .completed event,然后终止。当然,也可能发出 .error event。

在 subscribe 操作中使用 iflet 语法可以获取 element。

subscribe(onNext:) 处理 .next 事件,不会去理会其他事件。onNext 闭包接收 next 事件的元素作为参数,所以可以不必在 subscribe 操作中手动获取元素了。

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
let one = 1
let two = 2
let three = 3

let observable = Observable.of(one, two, three)
observable.subscribe { (event) in
print("subscrib event: \(event)")
if let element = event.element {
print("subscrib: \(element)")
}
}

observable.subscribe(onNext: { (element) in
print("subscribe next on: \(element)")
})

// 控制台输出:
/**
subscrib event: next(1)
subscrib: 1
subscrib event: next(2)
subscrib: 2
subscrib event: next(3)
subscrib: 3
subscrib event: completed
subscribe next on: 1
subscribe next on: 2
subscribe next on: 3
*/

empty

empty 操作创建了一个空的 observable sequence,不包含任何元素,它只发送一个 completed 事件。
如果一个 observable 不能被推断类型,则它必须指定类型:Observable<Type>。

1
2
3
4
5
6
7
8
let observable = Observable<Void>.empty()
observable
.subscribe(onNext: { (element) in
print("empty: \(element)")
}) {
print("Completed")
// Completed
}

empty 一般在你想返回一个直接终止的或者没有值的 observable 时使用。

never

和 empty 相反,字面意思也可以看出,never 创建的 observable 不发出任何值。
并且这个 observable 也不会终止,它可以用来代表一个无限的时间间隔。

1
2
3
4
5
6
let observable = Observable<Any>.never()
observable
.subscribe( onNext: { element in print(element) },
onCompleted: { print("Completed") } )

// 控制台什么都不会打印

range

1
2
3
4
5
6
7
8
9
10
11
12
let observable = Observable.range(start: 1, count: 3)

var n = 0

observable.subscribe(onNext: { (i) in
n += i
print(n)
})

// 1 (0+1)
// 3 (1+2)
// 6 (3+3)

range 操作会自动发出 completed 事件并终止。

销毁和终止 Observable

订阅操作触发了一个 observable 去发出事件,当它发出 error 事件或 completed 事件后就会被终止。但是也可以手动通过取消订阅去终止。

dispose

通过 dispose() 操作可以手动取消一个 observable 的订阅。

1
2
3
4
5
6
7
8
9
10
11
let observable = Observable.of("a", "b", "c")
let subscription = observable.subscribe { (event) in
print(event)
}

subscription.dispose()

// next(a)
// next(b)
// next(c)
// completed

DisposeBag

手动管理每个订阅的销毁是非常蛋疼的,RxSwift 提供了一个 DisposeBag 类去管理每个订阅的取消(通过 disposed(by:) 操作实现)。
一个 disposeBag 持有 disposables,当 disposeBag 要被取消配置的时候,它会对每个 observable 调用 dispose() 操作。

1
2
3
4
5
6
7
let disposeBag = DisposeBag()

Observable.of("a", "b", "c")
.subscribe {
print($0)
}
.disposed(by: disposeBag)

DisposeBag 类也是最常用的,创建并订阅一个 observable,然后直接在后面添加 disposeBy 操作。

为什么要 dispose 操作?

当你忘记给一个订阅操作添加 disposeBag,或者当一个订阅结束手动调用 dispose() 操作,或者在某个时间点引起了 observable 的终止,这些行为都可能造成内存泄露,而 disposeBag 可以去自动管理 observables 的终止操作。

创建 Observable

create 操作:static func create(_ subscribe: @escaping (AnyObserver<Int>) -> Disposable) -> Observable<Int>。

通过 create 操作可以创建一个 observable,create 操作包含了一个 subscribe 的闭包参数,这个闭包提供了 observer,用来实现 observable 的订阅。AnyObserver 是一个泛型类型,给一个 observable sequence 添加值,这个值被发送给订阅者。

Observer 可以发出 next、completed、error 事件。

至此可以看出 RxSwift 的 Observable.create 操作和 RAC 的 [RACSignal create] 操作基本上是一个意思。ReactiveCocoa 也是借用了 Rx 的思想在 OC 上实现了响应式框架。

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

enum MyError: Error {
case anError
}

let disposeBag = DisposeBag()

Observable<String>.create { (observer) -> Disposable in
observer.on(.next("1"))
observer.on(.error(MyError.anError))
observer.on(.completed)
observer.onNext("?")

// Disposables.create() 操作返回了一个空的 disposable
return Disposables.create()
}
.subscribe(
onNext: { print($0) },
onError: { print($0) },
onCompleted: { print("Completed") },
onDisposed: { print("Disposed") })
.disposed(by: disposeBag)

// 1
// anError
// Disposed
// observer 发出的 error 操作终止了订阅行为,completed 操作和 next("?") 操作并没有被发出

在上面的这段代码中,如果你注释掉 error、completed 和 disposedBy 操作会发生什么?
造成内存泄漏,控制台会打印:1 和 ?,并没有任何 error 或 completed 或 disposed 信息被打印。

创建 observable 工厂

deferred

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
let disposeBag = DisposeBag()

var flip = false

let factory: Observable<Int> = Observable.deferred {

flip = !flip

if flip {
return Observable.of(1, 2, 3)
} else {
return Observable.of(4, 5, 6)
}
}

for _ in 0...3 {
factory
.subscribe(onNext: {
print($0, terminator:"")
})
.disposed(by: disposeBag)
print()
}

// 123
// 456
// 123
// 456

Singles

Singles 会发送两个事件:

  • .success(value) 事件是 .next 和 .completed 的组合
  • .error

Single 非常适合处理一次性操作,要么成功,要么失败,例如:

  • 下载数据
  • 从磁盘加载数据

Single 使用示范:
将一个文件拖入工程中,例如我拖入了一个 Repo 的 README.md 文件:

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
func exampleOfSingle() {

print("--- Example of: single --- ")

let disposeBag = DisposeBag()
loadText(from: "README", type: "md")
.subscribe {
// single in
//
// print(single)
// switch single {
// case.success(let string):
// print(string)
// case.error(let error):
// print(error)
switch $0 {
case .success(let string):
print(string)
case .error(let error):
print(error)
}
}
.disposed(by: disposeBag)
}

func loadText(from name: String, type: String = "txt") -> Single<String> {
return Single.create { single in
let disposable = Disposables.create()

guard let path = Bundle.main.path(forResource: name, ofType: type) else {
single(.error(FileReadError.fileNotFound))
return disposable
}

guard let data = FileManager.default.contents(atPath: path) else {
single(.error(FileReadError.unreadable))
return disposable
}

guard let contents = String(data: data, encoding: .utf8) else {
single(.error(FileReadError.encodingFailed))
return disposable
}

single(.success(contents))

return disposable
}
}

enum FileReadError: Error {
case fileNotFound, unreadable, encodingFailed
}

以上代码调用 exampleOfSingle() 后,控制台会打印 README.md 的内容:

1
2
3
# LearnRxSwift

#### 项目介绍

Completable

Completable 仅仅会发送一个 completed 事件或者一个 error 事件,它不会发送任何 value。

当你仅仅考虑一个操作是否成功或者失败的时候,可以使用 Completable。

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
let bag = DisposeBag()

let completable = Completable.create { (event) -> Disposable in

// 模拟操作
let flag = Int.random(in: 0..<2)

var success: Bool {
return flag == 1 ? true : false
}

guard success else {
event(.error(FileReadError.fileNotFound))
return Disposables.create()
}

event(.completed)
return Disposables.create()
}

completable.subscribe(onCompleted: {
print("Completed")
}) { (error) in
print("An error has occurred")
}.disposed(by: bag)

// An error has occurred

Maybe

Maybe 是 Single 和 Completable 的混搭。它可以发送 success、completed 或 error 事件,但只能发送一个事件。

如果你需要去事件一个可以发送成功或者失败,并且当成功时,可以选择性的返回一个值的操作,可以使用 Maybe 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let maybe = Maybe<Int>.create { (event) -> Disposable in
event(.success(1))
event(.completed)
event(.error(FileReadError.encodingFailed))
return Disposables.create()
}

let bag = DisposeBag()

maybe.subscribe(onSuccess: {
print($0)
}, onError: { (errpr) in
print("An error has occurred")
}) {
print("Completed")
}.disposed(by: bag)

// 1

数据结构与算法基础

发表于 2018-04-20 | 分类于 DataStructure&Algorithm

算法数据结构

数组

1
// Todo

字典

1
// Todo

集合

1
// Todo

链表

参考文献

链表是一个集合,它的值是线性排列的序列。链表相比一些连续存储策略,例如 Swift Array,有这么几个理论上的优势:

  • 固定时间插入和顶端移除
  • 可靠的特征表现

由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

参考文献

双向链表,又称为双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

Node 节点

链表是一个节点的链条,节点拥有两个职责:

  • 持有一个值
  • 持有一个对下一节点的引用,一个 nil 代表了链表的结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Node<Value> {
public var value: Value
public var next: Node?

public init(value: Value, next: Node? = nil) {
self.value = value
self.next = next
}
}

extension Node: CustomStringConvertible {
public var description: String {
guard let next = next else { return "\(value)" }
return "\(value) -> " + String(describing: next) + " "
}
}

链表增加值

链表有一个头尾的改变,头指代了第一个节点,尾指代第二个节点。

有三种方式给一个链表添加值:

  1. push:在链表前端添加值
  2. append:在链表末尾添加值
  3. insert(after:):在一个特定的节点后添加值
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
public struct LinkedList<Value> {
public var head: Node<Value>?
public var tail: Node<Value>?

public init() {}

public var isEmpty: Bool {
return head == nil
}

// Push
public mutating func push(_ value: Value) {
head = Node(value: value, next: head)
if tail == nil {
tail = head
}
}

// Append
public mutating func append(_ value: Value) {
guard !isEmpty else {
push(value)
return
}

tail!.next = Node(value: value)

tail = tail!.next
}

public func node(at index: Int) -> Node<Value>? {
var currentNode = head
var currentIndex = 0

// 向下遍历直到找到相应的 index, 返回 Node
while currentNode != nil && currentIndex < index {
currentNode = currentNode!.next
currentIndex += 1
}

return currentNode
}

@discardableResult
public mutating func insert(_ value: Value,
after node: Node<Value>) -> Node<Value> {

// 如果尾部节点和 node 不是同一个节点
guard tail !== node else {
// 是同一个节点
return tail!
}

node.next = Node(value: value, next: node.next)
return node.next!
}
}

extension LinkedList: CustomStringConvertible {
public var description: String {
guard let head = head else { return "Empty list" }
return String(describing: head)
}
}

性能

push append insert(after:) node(at:)
Behavior insert at head insert at tail insert after a node returns a node at given index
Time complexity O(1) O(1) O(1) O(i) where i is the given index

移除值

有三种操作移除链表中的节点:

  1. pop:移除最前端节点
  2. removeLast:移除尾部节点
  3. remove(after:):移除链表中节点
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
extension LinkedList {
/// Removes the value at the front of the list
public mutating func pop() -> Value? {
defer {
head = head?.next
print("defer head: \(head!)")
if isEmpty {
tail = nil
}
}
print("head: \(head!)")
return head?.value
}

public mutating func removeLast() -> Value? {

guard let head = head else { return nil }

// 如果这个链表只有一个节点, removeLast 就等价于 pop
guard head.next != nil else {
return pop()
}

// 一直搜索, 直到 current.next == nil
// 这表明 current 是链表最后一个节点
var prev = head
var current = head

while let next = current.next {
prev = current
// 第一次循环 prev === head -> true
// 第二次循环 prev === head -> false
// 此时 prev 的引用切换到 head.next
current = next
}

// 这时 current 是最后一个节点
// prev.next === current
// 移除 prev.next 相当于移除 head.next 的 next, 更新 tail
prev.next = nil
tail = prev

return current.value
}

@discardableResult
public mutating func remove(after node: Node<Value>) -> Value? {
defer {
if node.next === tail {
tail = node
}
node.next = node.next?.next
}
return node.next?.value
}
}

Perform analysis

pop removeLast Remove(after:)
Time complexity O(1) O(n) O(1)

栈

参考文献

栈(英语:stack)又称为栈或堆叠,是计算机科学中一种特殊的串列形式的抽象数据类型,其特殊之处在于只能允许在链表或数组的一端(称为堆栈顶端指针,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。另外栈也可以用一维数组或链表的形式来完成。堆栈的另外一个相对的操作方式称为队列。

由于堆栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。

对于栈,只有两个必要的操作:

  1. push:在栈顶添加一个元素
  2. pop:移除栈顶元素
  • 在 iOS 开发中,如果你要在你的 App 中添加撤销操作(比如删除图片,恢复删除图片),那么栈是首选数据结构
  • 无论在面试还是写 App 中,只关注栈的这几个基本操作:push, pop, isEmpty, peek, size。
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
public struct Stack<Element> {
private var storage: [Element] = []
public init() {}

public init(_ elements: [Element]) {
storage = elements
}

public mutating func push(_ element: Element) {
storage.append(element)
}

// 忽略警告
@discardableResult
public mutating func pop() -> Element? {
return storage.popLast()
}

public func peek() -> Element? {
return storage.last
}

public var isEmpty: Bool {
return peek() == nil
}
}

extension Stack: CustomStringConvertible {
public var description: String {
let topDivider = "----top----\n"
let bottomDivider = "\n----Bot----"

let stackElements = storage
.map { "\($0)" }
.reversed()
.joined(separator: "\n")

return topDivider + stackElements + bottomDivider
}
}

/// 用数组字面量初始化
extension Stack: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Element...) {
storage = elements
}
}

队列

参考文献

队列,又称为伫列(queue),是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。

队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。

队列所关心的操作:

  • enqueue:在队列末尾插入一个元素
  • dequeue:移除队列头部元素
  • isEmpty:队列是否为空
  • peek:返回队列最开头的元素,不移除
1
2
3
4
5
6
7
8
public protocol Queue {

associatedtype Element
mutating func enqueue(_ element: Element) -> Bool
mutating func dequeue() -> Element?
var isEmpty: Bool { get }
var peek: Element? { get }
}

基于数组的队列

操作 最好情况 最差情况
enqueue O(1) O(1)
dequeue O(n) O(n)
空间复杂度 O(n) O(n)

基于双向链表队列

操作 最好情况 最差情况
enqueue O(1) O(1)
dequeue O(1) O(1)
空间复杂度 O(n) O(n)

环形缓冲区队列

操作 最好情况 最差情况
enqueue O(1) O(1)
dequeue O(1) O(1)
空间复杂度 O(n) O(n)

双栈队列

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

public struct QueueStack<T> : Queue {

private var leftStack: [T] = []
private var rightStack: [T] = []
public init() {}

public var isEmpty: Bool {
return leftStack.isEmpty && rightStack.isEmpty
}

public var peek: T? {
return !leftStack.isEmpty ? leftStack.last : rightStack.first
}

/// 右栈用来插入, 左栈用来放右栈拿出来的数据
public mutating func enqueue(_ element: T) -> Bool {
rightStack.append(element)
return true
}

public mutating func dequeue() -> T? {
if leftStack.isEmpty {

// 如果此时左栈是空的, 右栈是 [3, 2, 1]
// 进栈顺序为 3 -> 2 -> 1
// 把右栈反向填充到左栈, 此时左栈为 [1, 2, 3]
leftStack = rightStack.reversed()
rightStack.removeAll()
}
// 出队列操作, 左栈移除最后一个元素, 3, 先进先出
return leftStack.popLast()
}
}
操作 最好情况 最差情况
enqueue O(1) O(1)
dequeue O(1) O(1)
空间复杂度 O(n) O(n)

树

参考文献)

树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • 每个节点有零个或多个子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树;

树结构主要用来:

  • 代表层级关系
  • 管理排序数据
  • 促进快速查找操作

术语

节点

树是由节点构成,每个节点封装一些数据并跟踪其子节点。

每个节点负责一个值,并且用数组持有对子节点的引用。

父节点和子节点

每个节点,除了根节点,上面都连接了一个节点,这个节点叫父节点。直接连接在父节点下方的节点叫子节点。
每个子节点只有一个父节点。

根节点

树最顶端的叫根节点

叶节点

没有子节点的节点叫叶节点或者终端节点。

实现

1
2
3
4
5
6
7
8
9
10
11
12
public class TreeNode<T> {
public var value: T
public var children: [TreeNode] = []

public init(_ value: T) {
self.value = value
}

public func add(_ child: TreeNode) {
children.append(child)
}
}

二叉树

参考文献

二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

与普通树不同,普通树的节点个数至少为1,而二叉树的节点个数可以为0;普通树节点的最大分支度没有限制,而二叉树节点的最大分支度为2;普通树的节点无左、右次序之分,而二叉树的节点有左、右次序之分。

二叉树通常作为数据结构应用,典型用法是对节点定义一个标记函数,将一些值与每个节点相关系。这样标记的二叉树就可以实现二叉搜索树和二叉堆,并应用于高效率的搜索和排序。

二叉树的子节点,通常被称为左右子节点。

实现

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
public class BinaryNode<Element> {
public var value: Element
public var leftChild: BinaryNode? = nil
public var rightChild: BinaryNode? = nil

public init(value: Element) {
self.value = value
}
}

extension BinaryNode: CustomStringConvertible {

public var description: String {
return diagram(for: self)
}

private func diagram(for node: BinaryNode?,
_ top: String = "",
_ root: String = "",
_ bottom: String = "") -> String {
guard let node = node else {
return root + "nil\n"
}
if node.leftChild == nil && node.rightChild == nil {
return root + "\(node.value)\n"
}
return diagram(for: node.rightChild, top + " ", top + "┌──", top + "│ ")
+ root + "\(node.value)\n"
+ diagram(for: node.leftChild, bottom + "│ ", bottom + "└──", bottom + " ")
}
}

遍历算法

In-order traversal 中序遍历

从根节点开始:

  1. 如果当前节点有左子节点,先递归访问这个子节点
  2. 然后访问当前节点本身
  3. 如果当前节点有一个右子节点,递归访问这个子节点
1
2
3
4
5
6
7
8
9
10
11
extension BinaryNode {

public func traverseInOrder(visit: (Element) -> Void) {
// 如果当前节点有左子节点,先递归访问这个子节点
leftChild?.traverseInOrder(visit: visit)
// 然后访问当前节点本身
visit(value)
// 如果当前节点有一个右子节点,递归访问这个子节点
rightChild?.traverseInOrder(visit: visit)
}
}

Pre-order traversal 前序遍历

  1. 首先访问当前节点自身值
  2. 如果当前节点有左子节点,递归访问这个子节点
  3. 如果当前节点有右子节点,递归访问这个子节点
1
2
3
4
5
6
7
extension BinaryNode {
public func traversePreOrder(visit: (Element) -> ()) {
visit(value)
leftChild?.traversePreOrder(visit: visit)
rightChild?.traversePreOrder(visit: visit)
}
}

Post-order traversal 后序遍历

  1. 如果当前节点有左子节点,递归访问这个子节点
  2. 如果当前节点有右子节点,递归访问这个子节点
  3. 最后访问当前节点自身值
1
2
3
4
5
6
7
extension BinaryNode {
public func traversePostOrder(visit: (Element) -> ()) {
leftChild?.traversePostOrder(visit: visit)
rightChild?.traversePostOrder(visit: visit)
visit(value)
}
}

二叉搜索树

参考文献

堆

一个堆是一个完整的二叉树, 也叫做二叉堆, 可以用一个数组构成

堆有两种形式:

  • Max heap - 高权重的元素有更大的值
  • Min heap - 高权重的元素有更小的值

堆属性

  • 在 Max heap 中, 父节点的值必须大于等于子节点的值, 根节点一定是最大值
  • 在 Min heap 中, 父节点的值必须小于等于子节点的值, 根节点一定是最小值
  • 一个堆一定是一个完整的二叉树, 这意味着, 每一层级必须被填上, 除了最后一级

堆的应用

  • 计算一个集合的最小或最大元素
  • 堆排
  • 生成一个优先队列 - priority queue
  • 生成图标算法, 例如 Prim's 或 Dijkstra's

不要混淆这些堆和内存堆
术语堆有时候在计算机科学中被用来只带一个内存池
内存堆是另一个不同的概念

常用操作

Basic Heap

1
2
3
4
5
6
7
8
struct Heap<Element: Equatable> {
var elements: [Element] = []
let sort: (Element, Element) -> Bool

init(sort: @escaping (Element, Element) -> Bool) {
self.sort = sort
}
}

Heap Representation

用数组代表堆:

  • 左节点下标: 2i + 1
  • 右节点下标: 2i + 2
  • 父节点下标: (i - 1) / 2

向下筛选去获取左右子节点是 O(log n) 操作, 在数组中, 同样的操作为 O(1)

基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var isEmpty: Bool {
return elements.isEmpty
}

var count: Int {
return elements.count
}

func peek() -> Element? {
return elements.first
}

func leftChildIndex(ofParentAt index: Int) -> Int {
return (2 * index) + 1
}

func rightChildIndex(ofParentAt index: Int) -> Int {
return (2 * index) + 2
}

func parentIndex(ofChildAt index: Int) -> Int {
return (index - 1) / 2
}

Removing from a heap

最基本的移除操作就是移除根节点.

移除最大根节点, 你必须先交换根节点和最后一个元素

注意: max heap, 每个父节点必须必子节点的值要大, 在交换后必须向下筛选. 如果两个子节点的值, 都更大, 那就取最大的那个

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
/// 移除根节点
mutating func remove() -> Element? {

// 如果 heap 是空的就返回 空
guard !isEmpty else {
return nil
}

// 交换根元素和最后一个元素
elements.swapAt(0, count - 1)

/**
在错误处理方面,guard 和新的 throw 语法之间,Swift 2.0 也鼓励用尽早返回错误(这也是 NSHipster 最喜欢的方式)来代替嵌套 if 的处理方式。尽早返回让处理更清晰了,但是已经被初始化(可能也正在被使用)的资源必须在返回前被处理干净。

新的 defer 关键字为此提供了安全又简单的处理方式:声明一个 block,当前代码执行的闭包退出时会执行该 block
*/
defer {
// 向下筛选以确保是一个 max heap 或者 min heap
siftDown(from: 0)
}

// 移除最后一个元素, 并返回
return elements.removeLast()
}

/// 向下筛选
mutating func siftDown(from index: Int) {

// 记录父节点 index
var parent = index

// 不断向下筛选, 直到 return
while true {

// 得到左节点和右节点的 index
let left = leftChildIndex(ofParentAt: parent)
let right = rightChildIndex(ofParentAt: parent)

// 记录候选节点 index
var candidate = parent

// 如果有左节点, 并且左节点的值高于父节点的值, 那么替换候选节点为左节点
if left < count && sort(elements[left], elements[candidate]) {
candidate = left
}

// 如果有右节点, 并且右节点比左节点的值更高, 那么替换候选节点为右节点
if right < count && sort(elements[right], elements[candidate]) {
candidate = right
}

// 如果候选节点和父节点是一样的, 直接 return 结束循环
if candidate == parent {
return
}

// 替换父节点和子节点的值
elements.swapAt(parent, candidate)

// 重新赋值父节点, 进入下一次向下筛选
parent = candidate
}
}

时间复杂度: remove()方法总体时间复杂度是 O(log n) - 数组中交换元素的复杂度是 O(1), 向下筛选的操作复杂度是 O(log n)

插入操作

首先, 在最后一个节点插入值, 然后检查这个完整二叉树还是不是一个最大堆或最小堆, 如果不是, 则要通过和父节点比较, 进行向上筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mutating func insert(_ element: Element) {
elements.append(element)
siftUp(from: elements.count - 1)
}

mutating func siftUp(from index: Int) {
var child = index
var parent = parentIndex(ofChildAt: index)

// 如果子节点不是根节点, 且子节点的值大于或小于父节点
while child > 0 && sort(elements[child], elements[parent]) {

// 交换两个节点的值
elements.swapAt(child, parent)
// 更换子节点下标
child = parent
// 重新查找父节点
parent = parentIndex(ofChildAt: child)
}
}

时间复杂度: 总共的 insert(_:) 复杂度是 O(log n), 给数组添加元素的操作是 O(1), 向上筛选的复杂度是 O(log n)

O(n2) 算法

Bubble sort

1
2
3
4
5
6
7
8
9
10
// Bubble sort
func bubbleSort(_ array: inout [Int]) {
for i in (1 ..< array.count).reversed() {
for j in 0 ..< i {
if array[j] > array[j+1] {
array.swapAt(j, j + 1)
}
}
}
}
1
2
3
4
5
6
7
8
9
for (NSInteger i = 0; i < mutableArray.count; ++i) {
for (NSInteger j = 0; j < mutableArray.count - 1; ++j) {
if (mutableArray[j].integerValue > mutableArray[j+1].integerValue) {
temp = [mutableArray[j] integerValue];
mutableArray[j] = mutableArray[j+1];
mutableArray[j + 1] = @(temp);
}
}
}

Selection sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Selection sort
func selectionSort(_ array: inout [Int]) {
for current in 0 ..< array.count - 1 {
var lowest = current
for other in (current + 1) ..< array.count {
if array[lowest] > array[other] {
lowest = other
}
}
if lowest != current {
array.swapAt(lowest, current)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
for (NSInteger current = 0; current < mutableArray.count - 1; current++) {
NSInteger lowest = current;
for (NSInteger other = current + 1; other < mutableArray.count; other++) {
if (mutableArray[lowest].integerValue > mutableArray[other].integerValue) {
lowest = other;
}
}

if (lowest != current) {
[mutableArray exchangeObjectAtIndex:current withObjectAtIndex:lowest];
}
}

Insertion sort

1
2
3
4
5
6
7
8
9
10
11
12
// Insertion sort
func insertionSort(_ array: inout [Int]) {
for current in 1 ..< array.count {
for shifting in (1...current).reversed() {
if array[shifting] < array[shifting - 1] {
array.swapAt(shifting, shifting - 1)
} else {
break
}
}
}
}
1
2
3
4
5
6
7
8
9
for (NSInteger i = 1; i < mutableArray.count; i++) {
for (NSInteger j = i; j > 0; j--) {
if (mutableArray[j].integerValue < mutableArray[j - 1].integerValue) {
[mutableArray exchangeObjectAtIndex:j withObjectAtIndex:j - 1];
} else {
break;
}
}
}

Merge sort (归并排序)

参考文献

归并排序是创建在归并操作上的一种有效的排序算法, 效率为 O(nlogn)
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

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
func mergeSort<Element>(_ array: [Element]) -> [Element] where Element: Comparable {

guard array.count > 1 else {
return array
}
let middle = array.count / 2
let left = mergeSort(Array(array[..<middle]))
let right = mergeSort(Array(array[middle...]))
let merged = merge(left, right)
return merged
}

func merge<Element>(_ left: [Element], _ right: [Element]) -> [Element] where Element: Comparable {

var leftIndex = 0
var rightIndex = 0

var result: [Element] = []

while leftIndex < left.count && rightIndex < right.count {
let leftElement = left[leftIndex]
let rightElement = right[rightIndex]

if leftElement < rightElement {
result.append(leftElement)
leftIndex += 1
} else if rightElement < leftElement {
result.append(rightElement)
rightIndex += 1
} else {
result.append(leftElement)
leftIndex += 1
result.append(rightElement)
rightIndex += 1
}
}

if leftIndex < left.count {
result.append(contentsOf: left[leftIndex...])
}
if rightIndex < right.count {
result.append(contentsOf: right[rightIndex...])
}

return result
}

Quicksort (快速排序)

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
// Naive
func quicksortNaive<T: Comparable>(_ a: [T]) -> [T] {

guard a.count > 1 else {
return a
}

let pivot = a[a.count / 2]
let less = a.filter { $0 < pivot }
let equal = a.filter { $0 == pivot }
let greater = a.filter { (num) -> Bool in
return num > pivot
}
return quicksortNaive(less) + equal + quicksortNaive(greater)
}

// Lomuto
func partitionLomuto(_ a: inout [Int], low: Int, high: Int) -> Int {
let pivot = a[high]
var i = low
for j in low..<high {
if a[j] <= pivot {
a.swapAt(i, j)
i += 1
}
}
a.swapAt(i, high)

return i
}

func quicksort(_ a: inout [Int], low: Int, high: Int) {
if low < high {
let pivot = partitionLomuto(&a, low: low, high: high)
quicksort(&a, low: low, high: pivot - 1)
quicksort(&a, low: pivot + 1, high: high)
}
}

Heap sort (堆排序)

参考文献

匹配虚线填充动画分析实现

发表于 2018-04-18 | 分类于 Core Animation

设计师给了一个交互动画需要实现,是项目房间模块匹配中的一个交互动效:圆球随着实线转,实线填充虚线,圆球转到顶端开始减速,越过顶端开始加速,如图:

circle1

circle2

一开始会想如何去让一个圆球绕着圆心去转,难道是需要套什么数学公式么?思考了下,iOS 动画库中并没有这种操作,然后思考了一下上面的部件层级。

部件层级

  1. 一个外环圆,很好实现
  2. 外环圆包着内环虚线
  3. 一个做圆环动效的实线
  4. 一个固定的圆球
  5. 一个随圆环滚动的圆球

过程拆分

外环圆和内环虚线都非常好实现,第一反应就是 CAShapeLayer 去画各种图形,并且 ShapeLayer 在性能上是优化的。

查了很久没看见有圆球绕着锚点做圆周运动的 API,于是我拿 Sketch 尝试分解了一下,如图所示:

circleLevel

如图所示,换了个思路,虽然没有圆球绕着锚点做圆周运动的 API,但是可以把它放在父 layer 上,然父 layer 围绕自己的圆心自转,那么这个圆球也就绕着圆心运动了。

实现

首先从最简单的开始

外圈大圆和内圈虚线圆环

确定它们的贝塞尔曲线 path 直接绘制:

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
CGFloat bigLayerWidth = self.bounds.size.width;
CGFloat bigLayerHeight = self.bounds.size.height;

CGPoint position = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);

UIBezierPath *bigLayerPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, bigLayerWidth, bigLayerHeight)];


_bigLayer = [CAShapeLayer layer];
_bigLayer.frame = CGRectMake(0, 0, bigLayerWidth, bigLayerHeight);
_bigLayer.path = bigLayerPath.CGPath;
_bigLayer.lineWidth = 1.f;
_bigLayer.strokeColor = Color(whiteColor).CGColor;
_bigLayer.fillColor = Color(clearColor).CGColor;
_bigLayer.position = position;
[self.layer addSublayer:_bigLayer];

CGFloat contentLayerWidth = bigLayerWidth - 32;
CGFloat contentLayerHeight = bigLayerHeight - 32;
CGRect contentLayerRect = CGRectMake(0, 0, contentLayerWidth, contentLayerHeight);

UIBezierPath *centralCirclePath = [UIBezierPath bezierPathWithOvalInRect:contentLayerRect];

_dashCircleLayer = [CAShapeLayer layer];
_dashCircleLayer.bounds = contentLayerRect;
_dashCircleLayer.path = centralCirclePath.CGPath;
_dashCircleLayer.fillColor = Color(clearColor).CGColor;
_dashCircleLayer.strokeColor =Color(whiteColor).CGColor;
_dashCircleLayer.lineDashPattern = @[@1, @1];
_dashCircleLayer.lineWidth = 0.5f;
_dashCircleLayer.position = position;
[self.layer addSublayer:_dashCircleLayer];

此时的效果如图所示:

CircleLevel2

内圈动画层的绘制

要画的有三个部分:

  • 需要旋转的圆环
  • 需要旋转的圆球父 layer
  • 需要旋转的圆球
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

// 要做动画的圆环
_dynamicCircleLayer = [CAShapeLayer layer];
_dynamicCircleLayer.bounds = contentLayerRect;
_dynamicCircleLayer.path = centralCirclePath.CGPath;
_dynamicCircleLayer.fillColor = Color(clearColor).CGColor;
_dynamicCircleLayer.lineWidth = 1.0f;
_dynamicCircleLayer.strokeColor = Color(whiteColor).CGColor;
_dynamicCircleLayer.strokeStart = 0;
_dynamicCircleLayer.strokeEnd = 1;
_dynamicCircleLayer.affineTransform = CGAffineTransformMakeRotation(M_PI + M_PI_2);
_dynamicCircleLayer.position = position;
[self.layer addSublayer:_dynamicCircleLayer];

// 要做动画的父 layer
UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:contentLayerRect];
_dynamicContentLayer = [CAShapeLayer layer];
_dynamicContentLayer.bounds = contentLayerRect;
_dynamicContentLayer.path = rectPath.CGPath;
_dynamicContentLayer.fillColor = Color(clearColor).CGColor;
_dynamicContentLayer.strokeColor= Color(redColor).CGColor;
_dynamicContentLayer.position = position;
[self.layer addSublayer:_dynamicContentLayer];

// 父 layer 上的圆球
CGFloat ballLayerRadius = 4;

_dynamicBallLayer = [CAShapeLayer layer];
_dynamicBallLayer.frame = CGRectMake(0, 0, 2 * ballLayerRadius, 2 * ballLayerRadius);
_dynamicBallLayer.cornerRadius = ballLayerRadius;
_dynamicBallLayer.backgroundColor = Color(whiteColor).CGColor;
_dynamicBallLayer.position = CGPointMake(contentLayerWidth / 2.0, 0);
[_dynamicContentLayer addSublayer:_dynamicBallLayer];

此时效果如图所示:

CircleLevel3

不动圆球的绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_staticContentLayer = [CAShapeLayer layer];
_staticContentLayer.bounds = contentLayerRect;
_staticContentLayer.fillColor = Color(clearColor).CGColor;
_staticContentLayer.strokeColor= Color(whiteColor).CGColor;
_staticContentLayer.position = position;


[self.layer addSublayer:_staticContentLayer];

_staticBallLayer = [CAShapeLayer layer];
_staticBallLayer.frame = CGRectMake(0, 0, 2 * ballLayerRadius, 2 * ballLayerRadius);
_staticBallLayer.cornerRadius = ballLayerRadius;
_staticBallLayer.backgroundColor = Color(whiteColor).CGColor;
_staticBallLayer.position = CGPointMake(contentLayerWidth / 2.0, 0);
[_staticContentLayer addSublayer:_staticBallLayer];

隐藏圆球父试图开始动画

重力加速度动画,如果使用系统的 dynamic 感应系统就太麻烦了,我选择用动画选项淡入淡出模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)makeLayersAnimated {

// 圆环动画
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 3.0f;
// 模拟重力加速度动画
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.00f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.repeatCount = INFINITY;
[_dynamicCircleLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];

// 圆球动画
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 3.0f;
animation.repeatCount = INFINITY;
animation.byValue = @(M_PI * 2);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];;
[_dynamicContentLayer addAnimation:animation forKey:animation.keyPath];
}

最后在父控制器的 View 上添加这个 matching view:

1
2
CPMatchingView *matchingView = [[CPMatchingView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[self.view addSubview:matchingView];

本文 Demo

Demo 地址

浅谈 iOS 多线程

发表于 2017-12-21 | 分类于 Thread

前言

在移动端开发中不可避免的会接触到多线程。从用户使用体验角度来讲,也不可避免的会接触到多线程的操作。
本文代码已更新到 Swift 4.2。

多线程基础

什么是线程

线程,也被称为轻量级进程,是程序执行的最小单元。一个标准的线程由线程 ID、当前指令指针、寄存器和堆栈组成。一个进程由一到多个线程组成,线程之间又共享程序的内存空间和一些进程级资源。

线程和进程的关系:

Progress _ Thread

线程调度优先级

当线程数小于处理器核心数量时,是真正的并发,当大于的时候,线程的并发会受到一定阻碍。这可能也是为什么 Intel 即将要推出的 i7 - 9700k 是 8 核心 8 线程的原因,而不是 i7 - 8700k 那样拥有超线程技术的 6 核心 12 线程的 CPU。

在单核处理多线程的情况下,并发操作是模拟出来的一种状态,操作系统会让这些线程轮流执行一段时间,时间短到足以看起来这些线程是在同步执行的。这种行为称为线程调度。在线程调度中是有优先级调度的,高优先级的先执行,低优先级的线程通常要等到系统已经没有高优先级的可执行线程存在时才会开始执行,这也是为什么 GCD 会提供 Background、utility 等优先级选项。

除了用户手动控制线程的优先级,操作系统还会自动调整线程优先级。频繁进入等待状态的线程被称为 IO 密集型线程,很少等待,处理耗时操作长时间占用时间片的线程一般称为 CPU 密集型线程,IO 密集型线程比 CPU 密集型线程在线程优先级的调整中,更容易获得优先级的提升。

在线程调度中存在一种饿死现象。饿死现象是说,这个线程的优先级较低,而在它之前又有一个耗时的线程执行,导致它无法执行,最后饿死。为了避免这种情况,调度系统通常会提升那些等待时间过长线程的优先级,提升到足够让它执行的程度。

线程安全

数据竞争

举个例子,线程 1 有一个变量 i,并且在做 i += 1 的操作,线程 2 同时对这个变量做 i -= 1 的操作,线程 1、2 是并发执行的,这时就会发生竞争关系。

同步和锁

同步,指在一个线程操作一个数据未结束时,其他线程不得对同一个数据进行访问。为了避免多个线程同事读写一个数据而产生不可预知的结果,我们要将各个线程对这个数据的访问进行同步。

同步最常见的方法是使用锁。每个线程在访问数据之前会先获取锁,并在访问之后释放锁。在锁已经被占用时,试图获取锁,线程会等待到锁重新可用。

信号量(Semaphore)

在 iOS 中,信号量主要表现方式为 dispatch_semaphore_t,最终会调用 sem_wait 方法。
和 dispatch_semaphore 相关的函数有三个,创建信号,等待信号,发送信号。
信号量是允许并发访问的,可以由一个线程获取,另一个线程释放。

互斥量(Mutex)

互斥量仅允许一个线程访问。互斥量和信号量不同的是,互斥量要求哪个线程获取了,哪个线程就要负责去释放。
在 iOS 中,pthread_mutex 可以作为互斥锁。pthread_mutex 不是使用忙等,会阻塞线程并进行等待。它本身拥有设置协议的功能,通过设置协议来解决优先级反转的问题:

1
pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol)

NSLock 也是互斥锁,只不过是用 OC 的方式暴露出来,内部封装了一个 pthread_mutex。在 YYKit 源码中,ibireme 大佬频繁使用 pthread_mutex 而不是 NSLock,是应为 NSLock 是 OC 类,在使用时会经过消息转发,方法调用等操作,比 pthread 略慢。

1
2
3
4
let lock = NSLock()
lock.lock()
// Todo
lock.unlock()

@synchronized(Obj) 也是一种便捷的互斥锁创建方式,同事它也是一个递归锁。

读写锁(Read-Write Lock)

读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的。

  1. 当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行
  2. 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞

在 iOS 中,读写锁主要变现为 pthread_rwlock_t。

条件变量(Condition Variable)

条件变量,作用类似于一个栅栏。

  1. 线程可以等待条件变量,一个条件变量可以被多个线程等待。
  2. 线程可以唤醒条件变量,此时所有等待此变量的线程都会被唤醒。

使用条件变量,可以让许多线程一起等待某个事件的发生,当事件发生时,所有线程可以恢复执行。

在 iOS 中,NSCondition 表现为条件变量。

介绍条件变量的文章非常多,但大多都对一个一个基本问题避而不谈:“为什么要用条件变量?它仅仅是控制了线程的执行顺序,用信号量或者互斥锁能不能模拟出类似效果?”

网上的相关资料比较少,我简单说一下个人看法。信号量可以一定程度上替代 condition,但是互斥锁不行。在以上给出的生产者-消费者模式的代码中, pthread_cond_wait 方法的本质是锁的转移,消费者放弃锁,然后生产者获得锁,同理,pthread_cond_signal 则是一个锁从生产者到消费者转移的过程。

参考链接:bestswifter iOS锁的博文。

自旋锁(Spin lock)

关于自旋锁,可以查阅 ibirme 大佬的《不再安全的 OSSpinLock》

Thread

创建

Thread 创建有三种方式:

1
2
3
4
5
6
7
8
9
10
11
12
// 第一种,手动调用 start
// convenience init(target: Any, selector: Selector, object argument: Any?)
let thread = Thread(target: self, selector: #selector(thread1Action(_:)), object: "Thread1")
thread.name = "Background 1"
thread.start()

// 第二种,类方法
// class func detachNewThreadSelector(_ selector: Selector, toTarget target: Any, with argument: Any?)
Thread.detachNewThreadSelector(#selector(thread2Action(_:)), toTarget: self, with: "Thread2")

// 第三种 performSelector
performSelector(inBackground: #selector(thread3Action(_:)), with: "Thread3")

线程安全

在 OC 中可以添加 @synchronized() 方法方便的给线程加锁,但是 Swift 中,这个方法已经不存在。@synchronized 实际上在底层是调用了 objc_sync_enter 和 objc_sync_exit 方法以及一些异常处理。所以忽略异常问题可以简单实现一个 synchronized 方法:

1
2
3
4
5
func synchronized(_ lock: AnyObject, closure:() -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}

经典的售票系统简单模拟:

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
@IBAction func saleTicket(_ sender: Any) {

firstTicketWindow = Thread(target: self, selector: #selector(saleTicketAction), object: "Ticket Window 1")
firstTicketWindow.name = "Ticket Window 1"

secondTicketWindow = Thread(target: self, selector: #selector(saleTicketAction), object: "Ticket Window 2")
secondTicketWindow.name = "Ticket Window 2"

thirdTicketWindow = Thread(target: self, selector: #selector(saleTicketAction), object: "Ticket Window 3")
thirdTicketWindow.name = "Ticket Window 3"

firstTicketWindow.start()
secondTicketWindow.start()
thirdTicketWindow.start()
}

@objc func saleTicketAction() {

while ticketCount > 0 {
synchronized(self) {
Thread.sleep(forTimeInterval: 0.1)
if ticketCount > 0 {
ticketCount -= 1
print("\(Thread.current.name!) sold 1 ticket, \(self.ticketCount) remains.")
} else {
print("Tickets have been sold out.")
}
}
}
}

此时如果不加自己定义的 synchronized 方法,控制台会输出以下信息:
Thread unlock
很明显的,票务系统已经错乱。

如果加上 synchronized 方法,则会输出正确的信息:
Thread locked

线程间通信

在主线程上显示余票:

1
2
3
4
5
6
7
8
9
10
11
if ticketCount > 0 {
ticketCount -= 1
print("\(Thread.current.name!) sold 1 ticket, \(self.ticketCount) remains.")

// 主线程显示余票
self.performSelector(onMainThread: #selector(showTicketNum), with: nil, waitUntilDone: true)
}

@objc func showTicketNum() {
remainingLabel.text = "Ticket remains: \(ticketCount)"
}

Operation

Operation 是 Apple 对于 GCD 的封装,但是并不局限于 GCD 的先进先出队列。API 更加面向对象化,操作起来十分方便。

Operation 和 OperationQueue

Operation 相当于 GCD 的任务, OperationQueue 相当于 GCD 的队列。
使用 Operation 实现多线程的具体步骤:

  • 将需要执行的操作封装到 Operation 对象中
  • 将 Operation 添加到 OperationQueue

创建

一般情况下有三种使用方法:

  • NSInvocaionOperation

NSInvocation 在 Swift 中已被废除,因为它不是类型安全和 ARC 安全的。

下面是 OC 实现:

1
2
3
4
5
6
7
8
- (void)testNSInvocationOperation {
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation) object:nil];
[invocationOperation start];
}

- (void)invocationOperation {
NSLog(@"NSInvocationOperation: %@", [NSThread currentThread]);
}
  • BlockOperation
1
2
3
4
let operation = BlockOperation {
print("An block operation without being added in a queue, the thread is: \(Thread.current)")
}
operation.start()

Block Operation 添加执行闭包:

1
2
3
4
5
6
7
8
9
10
let operation = BlockOperation {
print("Create a block operation in \(Thread.current).")
}
operation.addExecutionBlock {
print("The block operation has add an execution block in \(Thread.current).")
}
operation.addExecutionBlock {
print("The block operation has add an execution block in \(Thread.current).")
}
operation.start()
  • Operation 子类

Operation 子类需要创建一个继承于 Operation 的类,需要重写 main() 方法:

1
2
3
4
5
6
7
8
9
class CustomOperation: Operation {
override func main() {

// Things to do
for _ in 0 ..< 2 {
print("Cunstom operation in thread: \(Thread.current)")
}
}
}

使用:

1
2
let operation = CustomOperation()
operation.start()

OperationQueue

  • OperationQueue 直接创建为子线程:let queue = OperationQueue()。
  • OperationQueue 获取主线程方法:OperationQueue.main。

将 Operation 添加到 Queue 中 会自动异步执行 Operation 中封装的操作,不需要再调用 Operation 的 start() 方法。

使用 addOperation(_:) 方法把 Operation 添加到队列

1
2
3
4
5
6
7
8
9
10
11
12
13
let queue = OperationQueue()

let operation1 = BlockOperation {
print("Operation 1 has beed added in a queue, in \(Thread.current).")
}

let operation2 = BlockOperation {
print("Operation 2 has beed added in a queue, in \(Thread.current).")
}

// Operation1 和 Operation2 执行顺序是不固定的
queue.addOperation(operation1)
queue.addOperation(operation2)

使用 addOperation {} 方法添加 Operation

1
2
3
4
5
6
let queue = OperationQueue()
queue.addOperation {
for _ in 0 ..< 2 {
print("A queue add operation with block in \(Thread.current).")
}
}

OperationQueue 线程间通信

下面以一个伪下载图片的代码来模拟 Operation 线程间通信:

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
let downloadQueue = OperationQueue()

indicator.startAnimating()

downloadQueue.addOperation {

Thread.sleep(forTimeInterval: 1)

let imageURLString = "https://clutchpoints.com/wp-content/uploads/2018/09/lebron-james.png"
let imageURL = URL(string: imageURLString)
let data = try? Data(contentsOf: imageURL!)

guard let theData = data else {
// 如果没有图片数据,回到主线程停止 indicator
OperationQueue.main.addOperation {
self.indicator.stopAnimating()
}
print("Download failed.")
return
}
let image = UIImage(data: theData)

// 下载完图片回到主线程更新 UI
OperationQueue.main.addOperation {
if let image = image {
self.imageView.image = image
self.hideButton.isHidden = false
self.imageView.isHidden = false
self.indicator.stopAnimating()
}
}
}

控制 OperationQueue 最大并发数

可以通过 maxConcurrentOperationCount 来控制并发数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.addOperation {
print("First operation - max concurrent number in \(Thread.current).")
}
queue.addOperation {
print("Second operation - max concurrent number in \(Thread.current).")
}
queue.addOperation {
print("Third operation - max concurrent number in \(Thread.current).")
}
queue.addOperation {
print("Fourth operation - max concurrent number in \(Thread.current).")
}

依赖和完成监听

你可以通过 Operation 的 addDependency(_ op: Operation) 方法来添加操作间的依赖关系:
例如 operation2.addDependency(operation1) 就是说 Operation1 执行完毕后 Operation2 才会执行。

你也可以通过 completionBlock 属性来监听某个操作已经完成。

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
let queue = OperationQueue()

var flag = false
let operation1 = BlockOperation {
// 模拟一个操作是否成功
flag = true
print("Operation 1 in \(Thread.current).")
Thread.sleep(forTimeInterval: 2)
}

// 监听 Operation 1 是否完成
operation1.completionBlock = {
print("Operation 1 is completed.")
}

let operation2 = BlockOperation {
if flag {
print("Operation 2 in \(Thread.current).")
} else {
print("Something went wrong.")
}
}

operation2.addDependency(operation1)

// 过两秒之后控制台才会打印 Operation1 完成和 Operation2 的执行信息
queue.addOperation(operation1)
queue.addOperation(operation2)

取消 Operation

可以通过 Operation 的 cancel() 方法 或 Queue 的 cancelAllOperations() 来取消 Operation。

但,值得注意的是,cancel() 方法,它做的唯一做的就是将 Operation 的 isCancelled 属性从 false 改为 true。由于它并不会真正去深入代码将具体执行的工作暂停,所以我们必须利用 isCancelled 属性的变化来暂停 main() 方法中的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let queue = OperationQueue()
queue.addOperation {
for i in 0 ... 100000000 {
print("i: \(i) in \(Thread.current)")
}
}
queue.cancelAllOperations()
queue.addOperation {
print("Second operation in \(Thread.current)")
}

let operation = CustomOperation()
// 将 isCancelled 属性更改为 true
operation.cancel()

// 控制台只会输出第二个 Operation 的执行信息。

GCD

GCD(Grand Central Dispatch) 是 Apple 推荐的方式,它将线程管理推给了系统,用的是名为 dispatch queue 的队列。开发者只要定义每个线程需要执行的工作即可。所有的工作都是先进先出,每一个 block 运转速度极快(纳秒级别)。使用场景主要是为了追求高效处理大量并发数据,如图片异步加载、网络请求等。

Dispatch 在 Swift 3 中的改变

任务和队列

  • Async:异步任务
  • Sync:同步任务

DispatchQueue 是一个类似线程的概念,这里称作对列队列是一个FIFO数据结构,意味着先提交到队列的任务会先开始执行)。DispatchQueue 背后是一个由系统管理的线程池。

DispatchQueue 又分为串行队列和并发队列。

串行队列使用同步操作容易造成死锁,例如主线程进行同步操作 DispatchQueue.main.sync {}。

创建队列

创建串行队列

如果不设置 DispatchQueue 的 Attributes,那么默认就会创建串行队列。

  • 串行队列的同步操作:
1
2
3
4
5
let queue = DispatchQueue(label: "com.demo.Serial1")
// 串行队列做同步操作, 容易造成死锁, 不建议这样使用
queue.sync {
print("Sync operation in a serial queue.")
}
  • 串行队列的异步操作:
1
2
3
4
5
6
7
8
9
10
11
12
let queue = DispatchQueue(label: "com.demo.Serial2")
// 串行队列做异步操作是顺序执行
queue.async {
for i in 0 ..< 2 {
print("First i: \(i)")
}
}
queue.async {
for i in 0 ..< 2 {
print("Second i: \(i)")
}
}

创建并发队列

  • 并发队列同步操作是顺序执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let label = "com.demo.Concurrent1"
let qos = DispatchQoS.default
let attributes = DispatchQueue.Attributes.concurrent
let autoreleaseFrequency = DispatchQueue.AutoreleaseFrequency.never
let queue = DispatchQueue(label: label, qos: qos, attributes: attributes, autoreleaseFrequency: autoreleaseFrequency, target: nil)

// 并发队列同步操作是顺序执行
queue.sync {
for i in 0 ..< 2 {
print("First sync i: \(i)")
}
}
queue.sync {
for i in 0 ..< 2 {
print("Second sync i: \(i)")
}
}
  • 并发队列异步操作执行顺序不定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let label = "com.demo.Concurrent2"
let attributes = DispatchQueue.Attributes.concurrent
let queue = DispatchQueue(label: label, attributes: attributes)

// 并发队列做异步操作执行顺序不固定
queue.async {
for i in 0 ..< 2 {
print("First async i: \(i)")
}
}
queue.async {
for i in 0 ..< 2 {
print("Second async i: \(i)")
}
}

创建主队列和全局队列

1
2
3
let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global()
let globalQueueWithQos = DispatchQueue.global(qos: .userInitiated)

QoS

QoS 全称 Quality of Service,在 Swift 中是一个结构体,用来指定队列或任务的优先级。

全局队列肯定是并发队列。如果不指定优先级,就是默认(default)优先级。另外还有 background,utility,user-Initiated,unspecified,user-Interactive。下面按照优先级顺序从低到高来排列:

  • Background:用来处理特别耗时的后台操作,例如同步、备份数据。
  • Utility:用来处理需要一点时间而又不需要立刻返回结果的操作。特别适用于异步操作,例如下载、导入数据。
  • Default:默认优先级。一般来说开发者应该指定优先级。属于特殊情况。
  • User-Initiated:用来处理用户触发的、需要立刻返回结果的操作。比如打开用户点击的文件。
  • User-Interactive:用来处理用户交互的操作。一般用于主线程,如果不及时响应就可能阻塞主线程的操作。
  • Unspecified:未确定优先级,由系统根据不同环境推断。比如使用过时的 API 不支持优先级,此时就可以设定为未确定优先级。属于特殊情况。

After 延迟

Swift 写法如下:

1
2
3
4
5
6
7
8
9
override func viewDidLoad() {
super.viewDidLoad()

print("View did load.")
let dispatchTime = DispatchTime.now() + 0.5
DispatchQueue.main.asyncAfter(deadline: dispatchTime) {
print("After 0.5 seconds.")
}
}

线程间通信

模拟下载单张图片并在 imageView 上展示:

使用 DispatchQueue.global().async {} 和 DispatchQueue.main.async {}。

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
@IBAction func downloadImage(_ sender: Any) {
indicator1.startAnimating()
// let queue = DispatchQueue.global(qos: .default)
DispatchQueue.global().async {
sleep(1)
let imageURL = URL(string: self.imageURLString1)
let data = try? Data(contentsOf: imageURL!)

guard let theData = data else {
OperationQueue.main.addOperation {
self.indicator1.stopAnimating()
}
print("Download failed.")
return
}
let image = UIImage(data: theData)

DispatchQueue.main.async {
if let image = image {
self.imageView1.image = image
self.hideButton.isHidden = false
self.imageView1.isHidden = false
self.indicator1.stopAnimating()
}
}
}
}

DispatchGroup

组操作,用来管理一组任务的执行,然后监听任务都完成的事件。比如,多个网络请求同时发出去,等网络请求都完成后 reload UI。

步骤:

  1. 创建一个 DispatchGroup
  2. 在并发队列中进行异步组操作
  3. 通过 group.notify {} 来组合那些单个的组操作

模拟多图下载操作:

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
85
86
87
88
89
@IBAction func downloadImagesInGroup(_ sender: Any) {

indicator1.startAnimating()
indicator2.startAnimating()

let group = DispatchGroup()

let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
var fileURL1 = URL(fileURLWithPath: documentsPath!)
fileURL1 = fileURL1.appendingPathComponent("LBJ1")
fileURL1 = fileURL1.appendingPathExtension("png")

var fileURL2 = URL(fileURLWithPath: documentsPath!)
fileURL2 = fileURL2.appendingPathComponent("LBJ2")
fileURL2 = fileURL2.appendingPathExtension("jpg")

// 下载图片1
group.enter()
DispatchQueue.global().async {

print("Begin to download image1.")

let imageURL = URL(string: self.imageURLString1)
let data = try? Data(contentsOf: imageURL!)

guard let theData = data else {
DispatchQueue.main.async {
self.indicator1.stopAnimating()
}
print("Image 1 download failed.")
return
}

try! theData.write(to: fileURL1, options: .atomic)

print("Image1 downloaded.")
sleep(1)
group.leave()
}

// 下载图片2
group.enter()
DispatchQueue.global().async {

print("Begin to download image2.")

let imageURL = URL(string: self.imageURLString2)
let data = try? Data(contentsOf: imageURL!)

guard let theData = data else {
DispatchQueue.main.async {
self.indicator2.stopAnimating()
}
print("Image 2 Download failed.")
return
}

try! theData.write(to: fileURL2, options: .atomic)

sleep(1)
print("Image2 downloaded.")
group.leave()
}

// 在主线程展示
group.notify(queue: .main) {

let imageData1 = try? Data(contentsOf: fileURL1)
let imageData2 = try? Data(contentsOf: fileURL2)

guard let theData1 = imageData1 else {
return
}
guard let theData2 = imageData2 else {
return
}

let image1 = UIImage(data: theData1)
let image2 = UIImage(data: theData2)

self.imageView1.image = image1
self.imageView2.image = image2
self.imageView1.isHidden = false
self.imageView2.isHidden = false
self.indicator1.stopAnimating()
self.indicator2.stopAnimating()
self.hideButton.isHidden = false
}
}

DispatchBarrier

栅栏函数,函数之前的任务提交完了才会执行后续的任务:

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
let label = "com.demo.Concurrent3"
let queue = DispatchQueue(label: label, attributes: .concurrent)

queue.async {
for i in 0 ..< 2 {
print("First i: \(i)")
}
}
queue.async {
for i in 0 ..< 2 {
print("Second i: \(i)")
}
}

queue.async(flags: .barrier) {
print("This is a barrier.")
}

queue.async {
for i in 0 ..< 2 {
print("Third i: \(i)")
}
}
queue.async {
for i in 0 ..< 2 {
print("Fourth i: \(i)")
}
}

控制台输出:

GCDBarrier

由此可见,只有当 First 和 Second 执行完毕才会执行 Third 和 Fourth,并且 First 和 Second 执行顺序是不确定的,Third 和 Fourth 也是如此。

Semaphore

信号量,是锁机制。

DispatchSemaphore 是传统计数信号量的封装,用来控制资源被多任务访问的情况。

举个例子,一共有两个停车位,现在 A、B、C 都需要停车,A 和 B 先挺的情况下,C 过来了,这时 C 就要等待 A 或 B 其中有一个出来,才会继续停进去。

注意:在串行队列上使用信号量要注意死锁的问题。

模拟停车操作:

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
let semaphore = DispatchSemaphore(value: 2)

// semaphore 在串行队列需要注意死锁问题
let queue = DispatchQueue(label: "com.demo.Concurrent4", qos: .default, attributes: .concurrent)

queue.async {
semaphore.wait()
print("First car in.")
sleep(2)
print("First car out.")
semaphore.signal()
}

queue.async {
semaphore.wait()
print("Second car in.")
sleep(3)
print("Second car out.")
semaphore.signal()
}

queue.async {
semaphore.wait()
print("Third car in.")
sleep(4)
print("Third car out.")
semaphore.signal()
}

控制台输出:

GCDSemaphore

由此可见,第一辆车出来了,第三辆车才能进去。

本文 Demo

本文 Demo 已更新到 Swift 4.2

SDWebImage 源码阅读

发表于 2017-10-25 | 分类于 Thread

SDWebImage 可以说是用途最广的库了,看了源码之后来说说自己的理解。

SD 架构

首先看 SDWebImage 自己提供的架构图:

SDWebImage类图表

仔细观察发现,SDWebImage 按功能,主要分为两块结构。

  • UIKit + SD 分类:提供了外部设置图片的接口
  • WebImageManager:内部逻辑类

按照平时的使用习惯,可以整理出常用类,如下图:

SDWebImage类

UIKit + SD 分类

ImageView 分类提供的接口有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)sd_setImageWithURL:(nullable NSURL *)url;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options;

...

- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;

- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray<NSURL *> *)arrayOfURLs;

- (void)sd_cancelCurrentAnimationImagesLoad;

Button 分类提供的接口:

1
2
3
4
5
6
7
8
9
10
- (void)sd_setImageWithURL:(nullable NSURL *)url
forState:(UIControlState)state;

...

- (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url
forState:(UIControlState)state
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock;

不难看出,设置图片的接口都是通过一个“全能初始化方法”去实现。而设置动图的方法是 UIImageView+WebCache 分类自己实现的。

溯源可知,设置图片的方法在 UIView+WebCache 中实现:

1
2
3
4
5
6
7
8
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context;

下面是这个方法的实现,关键点的注释在代码块中:

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context {
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);

// 1. 一些加锁操作,保证没有当前正在进行的异步操作
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// 加载之前先添加临时占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}

// URL 不为空的情况下
if (url) {
#if SD_UIKIT
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
#endif

// reset the progress
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;

// 创建 SDWebImageManager 下载图片
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}

// 创建下载进度回调
__weak __typeof(self)wself = self;
SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
wself.sd_imageProgress.totalUnitCount = expectedSize;
wself.sd_imageProgress.completedUnitCount = receivedSize;
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};

// 加载图片操作
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
#if SD_UIKIT
[sself sd_removeActivityIndicator];
#endif
// if the progress not been updated, mark it to complete state
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};

// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}

UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}

#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
// 3. 如果需要变形,设置图片
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
// 不需要变形,设置图片,是基于 3 的实现
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
// 完成回调
callCompletedBlockClojure();
});
}];

// 在操作缓存字典里添加operation,表示当前的操作正在进行
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {

// 如果不存在 URL,则完成回调设置 error
dispatch_main_async_safe(^{
#if SD_UIKIT
[self sd_removeActivityIndicator];
#endif
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

上面代码中 1. 的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
// 通过关联对象设置操作字典,如果关联对象存的有,则返回,如果没有则创建一个并且设置关联对象返回
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;

// 加锁,取出 operation,确保取操作的线程安全,以防出现数据竞争的问题
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
// 取消当前 operation
[operation cancel];
}
// 已经取消的操作,加锁,删除
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}

2. 的设置图片操作:

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
#if SD_UIKIT || SD_MAC
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
UIView *view = self;
SDSetImageBlock finalSetImageBlock;

// 如果设置了 setImageBlock
if (setImageBlock) {
finalSetImageBlock = setImageBlock;
}

// 如果是通过 UIImageView+WebCache 设置图片
else if ([view isKindOfClass:[UIImageView class]]) {
UIImageView *imageView = (UIImageView *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData) {
imageView.image = setImage;
};
}
#if SD_UIKIT
// 如果是通过 UIButton+WebCache 设置图片
else if ([view isKindOfClass:[UIButton class]]) {
UIButton *button = (UIButton *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData){
[button setImage:setImage forState:UIControlStateNormal];
};
}
#endif
...
}
#endif

值得一提的是,SDOperationDictionary 是由 UIView+WebCacheOperation 分类管理的,履行了面向对象单一职责原则,通俗说就是各干各的事,不越权,而且对代码的可读性有很好的帮助。

SDImageCache

SDImageCache 主要职责是管理下载完的图片缓存。在设置图片的时候,SDWebImageManager 首先会访问缓存,如果有缓存,则直接以缓存设置图片。

SDImageCache 是通过 NSCache 来缓存图片的,NSCache 的一个简单的优势就是它是线程安全的。

在 SDImageCache.m 中:

1
2
3
4
5
6
7
8
9
10
@interface SDImageCache ()

#pragma mark - Properties
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
@property (strong, nonatomic, nonnull) NSFileManager *fileManager;

@end

SDMemoryCache 是继承自 NSCache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// A memory cache which auto purge the cache on memory warning and support weak cache.
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>

@end

// Private
@interface SDMemoryCache <KeyType, ObjectType> ()

@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe

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

@end

缓存类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
* The image wasn't available the SDWebImage caches, but was downloaded from the web.
*/
// 不缓存(网络下载)
SDImageCacheTypeNone,
/**
* The image was obtained from the disk cache.
*/
// 磁盘缓存
SDImageCacheTypeDisk,
/**
* The image was obtained from the memory cache.
*/
// 内存缓存
SDImageCacheTypeMemory
};

存储缓存

通过一个串行队列去执行,一个任务执行完毕才执行下一个,不会出现文件同时被读取和写入的情况:

1
2
// Create IO serial queue
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

当下载完图片后,会先将图片保存到 NSCache 中,并把图片像素(image.width × image.height × image.scale2)大小作为该对象的 cost 值,同时如果需要保存到硬盘,会先判断图片的格式,PNG 或者 JPEG,并保存对应的 NSData 到缓存路径中,文件名为 URL 的 MD5 值:

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
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
// 首先缓存缓存图片到 NSCache 子类
if (self.config.shouldCacheImagesInMemory) {
// image.width × image.height × image.scale2 设置 cost
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}

// 如果选择保存到磁盘
if (toDisk) {
// 启用创建的 io 串行队列异步操作,确保文件写入安全性。
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
// 通过 alpha 通道确定是否图片是 PNG 格式
SDImageFormat format;
if (SDCGImageRefContainsAlpha(image.CGImage)) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
// 编码 image 到 NSData
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
// 写入磁盘
[self _storeImageDataToDisk:data forKey:key];
}

if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}

缓存删除

缓存删除的原理是首先移除内存缓存中的图片,如果需要从磁盘中移除,通过 io 串行队列异步操作移除磁盘中的图片 data。

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
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
if (key == nil) {
return;
}

// 如果有内存缓存,删除内存缓存
if (self.config.shouldCacheImagesInMemory) {
[self.memCache removeObjectForKey:key];
}

if (fromDisk) {
// 串行队列异步移除
dispatch_async(self.ioQueue, ^{
// 直接删除文件
[self.fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];

if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
} else if (completion){
completion();
}

}

缓存查询

缓存的查询是通过 NSOperation 操作的,SD 定义了一些缓存选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
/**
* By default, we do not query disk data when the image is cached in memory. This mask can force to query disk data at the same time.
*/
// 查询内存缓存
SDImageCacheQueryDataWhenInMemory = 1 << 0,
/**
* By default, we query the memory cache synchronously, disk cache asynchronously. This mask can force to query disk cache synchronously.
*/
// 强制同步查询磁盘缓存
SDImageCacheQueryDiskSync = 1 << 1,
/**
* By default, images are decoded respecting their original size. On iOS, this flag will scale down the
* images to a size compatible with the constrained memory of devices.
*/
// 这个操作会解码内存尺寸合适的图片。
SDImageCacheScaleDownLargeImages = 1 << 2
};

通过 Query 可查到缓存机制如下,首先查询内存中图片,如果只需要从内存中取出图片,则查询完毕立即返回空值。如果需要从磁盘中查询,为了防止数据的错乱,使用了一个 NSOperation 操作,如果有磁盘数据而没有内存数据,则将磁盘数据取出转换成内存数据,并返回。这样做是为了提高查询效率:

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
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}

// First check the in-memory cache...
// 首先查询内存中图片,如果只需要从内存中取出图片,则直接返回。
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}

NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
// 如果 Operation 被取消了,直接返回。
return;
}

@autoreleasepool {
// 通过路径查询磁盘文件
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
if (image) { // 此处的 image 是上面再内存中查询的图片
// the image is from in-memory cache
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) { // 如果有磁盘数据,将磁盘数据转换成 UIImage 对象
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// 将磁盘中的对象存储到内存中,优化下次查询的效率
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}

// 查询完毕的回调
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};

// 查询回调
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}

return operation;
}

SDWebImageDownloader

SDWebImageDownloader 管理图片的下载和缓存。SDWebImageDownloaderOperation 是继承自 NSOperation 的类。

一些关键的实例变量

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
/**
* Decompressing images that are downloaded and cached can improve performance but can consume lot of memory.
* Defaults to YES. Set this to NO if you are experiencing a crash due to excessive memory consumption.
*/
// 是否需要解压图片
@property (assign, nonatomic) BOOL shouldDecompressImages;

/**
* The maximum number of concurrent downloads
*/
// 最大并发下载数,默认为 6
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;

/**
* Shows the current amount of downloads that still need to be downloaded
*/
@property (readonly, nonatomic) NSUInteger currentDownloadCount;

/**
* The timeout value (in seconds) for the download operation. Default: 15.0.
*/
@property (assign, nonatomic) NSTimeInterval downloadTimeout;


/// .m 文件中
@interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

// 下载队列
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
// 上一条被添加的操作
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;
@property (assign, nonatomic, nullable) Class operationClass;
// 以 URL 为 key,装有 SDWebImageDownloaderOperation 的字典
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;
@property (strong, nonatomic, nonnull) dispatch_semaphore_t operationsLock; // a lock to keep the access to `URLOperations` thread-safe
@property (strong, nonatomic, nonnull) dispatch_semaphore_t headersLock; // a lock to keep the access to `HTTPHeaders` thread-safe

// The session in which data tasks will run
@property (strong, nonatomic) NSURLSession *session;

@end

配置 downloadToken 和 downloadOperation

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
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
// 确保 URL 不为空,如果为空则立马返回 nil
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}

// 通过信号量加锁
LOCK(self.operationsLock);
// 取出下载 operation
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
// 如果没有 operation 或者 operation 已经完成,配置 Operation
if (!operation || operation.isFinished) {
operation = createCallback();
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
// 根据 URL 为 key,将 operation 添加到字典中
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
// 下载队列添加下载 operation
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);

id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

// 配置下载 token
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;

return token;
}

下载

downloadImageWithURL: options: progress: completed: 方法是 SDWebImageDownloader 的核心,调用了上面的代码,同时在创建回调的 block 中创建新的操作,配置之后将其放入 downloadQueue 操作队列中,最后方法返回新创建的操作,返回一个遵循 SDWebImageOperation 协议的对象:

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
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;

return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;

// 重设下载超时时间
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
// 配置缓存策略
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];

// 是否处理 cookies
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
// 配置 header
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}

// 创建下载对象
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;

if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}

// 设置操作的队列优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}

// 如果是后入先出操作,则将该操作设置为最后一个操作
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}

return operation;
}];
}

SDWebImageManager

SDWebImageManager 管理着 SDImageCache 和 SDWebImageDownloader。在下载任务开始的时候,manager 先访问 cache 查询是否有可用的缓存,如果有,则直接使用缓存图片。如果没有缓存,就使用 downloader 来下载图片,下载成功后,存入缓存,显示图片。

SDWebImageManager 的核心方法:

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}

// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}

// 继承 NSObject,遵循 <SDWebImageOperation>,主要处理 cacheOperation
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;

BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}

if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}

LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
NSString *key = [self cacheKeyForURL:url];

SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;

__weak SDWebImageCombinedOperation *weakOperation = operation;

// 在 SDImageCache 中 查询是否有图片缓存
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// 如果没有图片处理操作,则移除
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}

// Check whether we should download image from network
// 没有缓存图片 || 即使有缓存图片,也需要更新缓存图片 || 代理没有响应imageManager:shouldDownloadImageForURL:消息,默认返回yes,需要下载图片 || imageManager:shouldDownloadImageForURL:返回yes,需要下载图片
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);


if (shouldDownload) {

// 有缓存图片的情况
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
// 如果缓存图片呗找到但是刷新缓存被提供,重新从服务器下载刷新缓存
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}

// 无缓存的情况
// download if no image or requested to refresh anyway, and download allowed by delegate
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;

if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
__weak typeof(strongOperation) weakSubOperation = strongOperation;
// 通过上文中 downloader 的下载方法下载图片
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
if (!strongSubOperation || strongSubOperation.isCancelled) {
// Do nothing if the operation was cancelled
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
} else if (error) {

// 如果有 error,在 completeBlock 里传入 error
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL;
// Check whether we should block failed url
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else {
shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost);
}

// 下载失败加锁添加失败 URL
if (shouldBlockFailedURL) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
else { // 下载成功
// 加锁移除失败的 URL
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}

// 判断是否进行缓存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}

if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { //(即使缓存存在,也要刷新图片) && 缓存图片 && 不存在下载后的图片:不做操作
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
// 缓存图片
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}

// 调用上文中的 imageCache 方法缓存图片
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}

// 将图片传入 completeBlock
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {

// 图片下载成功并结束
if (downloadedImage && finished) {
if (self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
});
} else {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}

// 如果完成,从当前的运行操作列表里移除操作
if (finished) {
// 这个方法也是一个加锁方法,通过加锁对 operation 的 Set 进行移除
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}];
} else if (cachedImage) {

// 存在缓存图片,调用完成的 block
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 删除当前的下载操作
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
// 没有缓存的图片,并且下载代理被终止
// Image not in cache and download disallowed by delegate
// 调用完成的 block
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
// 删除当前下载操作
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];

return operation;
}

总结

以上就是 SDWebImage 设置图片的主要流程。

基本上可以总结为:

  1. 外部调用 UIKit+SD 分类中的设置图片方法
  2. UIKit+SD 的方法调用 SDWebImageManager 的 loadImage 方法去加载图片
  3. SDWebImage 的 loadImage 方法会先查询是否有缓存,如果有缓存则直接使用缓存图片
  4. 如果没有缓存,或者缓存需要刷新,则下载图片,重新缓存
  5. SDImageCache 中的增删查操作都是通过队列加锁来确保操作的安全性
123

Yinan Zheng

21 日志
11 分类
15 标签
GitHub E-Mail
© 2018 Yinan Zheng
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4