⌈iOS⌋Swifty YYText II —— 事务与异步绘制
作为 YYText 的核心组件,YYAsyncLayer 与 YYTextTransaction 通过非常精简的代码定义了整个异步渲染流程。前者负责处理绘制逻辑并进行渲染,后者则是在适当的时机提交界面刷新事务(类似于 CATransaction)。为了提升渲染的效率,这两者内部都使用了一些优化技巧。原理相关的文章不胜枚举,本文旨在记录 Swift 化中遇到的问题以及一些思考。
OSAtomicIncrement:怎么说,我被弃用了
在获取渲染队列或者取消重复渲染都会用到一个原子自增的数字,但在 Swift 环境中该接口已经被标记为弃用:
1 | @available(iOS 7.1, *) |
如果我们根据提示按部就班,会发现 atomic_fetch_add_explicit 没法直接在 Swift 中使用。此时,需要为 Swift 与 C 之间的函数调用建立中间层,套路如下:
- 创建 .h 和 .c 文件声明并实现函数
- 在 Swift 声明函数并通过 @_silgen_name 指定其包装的函数名称(函数签名需要对齐)
- 在混编头文件中导入步骤 1 中创建的 .h 文件
1 | "atomic_increment_one") ( |
1 |
|
如果与原始代码进行对比,会发现我们这里用的是无符号整数。这是因为在获取渲染队列时需要指定下标,而当渲染次数足够多的时候,符号整数一定会出现溢出(变成负数),最终导致发生数组越界的崩溃。由于 Swift 静态成员变量初始化天然具备 dispatch_once 的特性,队列管理代码编写如下:
1 | private enum _QueueManager { |
基于闭包的事务去重可行性实验
YYTextTransaction 是基于 Target-Action 的方式进行事务管理,并根据这两者的哈希值完成无序列表的去重操作。但在 Swift 语境下,加上了 @objc 的函数为了能让 Objective-C 可用会被包装成一个 Thunk。为了能够提高执行的效率,会自然想到用闭包来代替: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
extension SYY.Transaction {
static func commit(_ work: @escaping () -> Void) {
__seeds.insert(.init(work))
}
}
extension SYY.Transaction {
private struct __Seed: Hashable {
let closure: () -> Void
private let _id: String
init(closure: @escaping () -> Void) {
self.closure = closure
self._id = "XXX" // TODO:
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs._id == rhs._id
}
func hash(into hasher: inout Hasher) {
hasher.combine(_id)
}
}
}
其中的 _id 的实现很容易这样来实现:
1 |
|
而如果我们查看 ObjectIdentifier 的注释会发现这种方式是错误的,因为In Swift, only class instances and metatypes have unique identities. There is no notion of identity for structs, enums, functions, or tuples.
穷则思变,如果使用指针是否可行呢?emmmm,很遗憾地说,此法亦行不通。Swift 为了安全性考虑,指针的操作相比于 C/C++ 而言异常的繁琐。并且它只是一个可以被用来临时访问变量的指针,而非其真实的内存地址。在文档上有如下说明:1
// The pointer argument to body is valid only during the execution of withUnsafeMutablePointer(to:_:)/其他 API. Do not store or return the pointer for later use.
踹一踹的例程如下:
1 |
|
总结
当我从头到尾阅读完 YYText 异步渲染流程相关代码之后,在某种程度上觉得自己行了。但在用 Swift 完成实际的编码时,会发现于胸的成竹变得慢慢变得模糊了。因为这其中涉及到不少的细节问题,处理是否得当将直接影响到 YYLabel/YYTextView 的实现复杂度。举个直观的🌰:YYAsyncLayerDelegate 从逻辑上肯定可以通过新的成员变量来指定,比如 yyDelegate。那么问题来了,UIView 的 layer 实际上是通过指定的 layerClass 隐式创建的,那么 yyDelegate 的赋值时机是什么呢?挨个在指定构造器里写上 (layer as? YYTextAsyncLayer).yyDelegate = self
这句代码吗?