作为 YYText 的核心组件,YYAsyncLayer 与 YYTextTransaction 通过非常精简的代码定义了整个异步渲染流程。前者负责处理绘制逻辑并进行渲染,后者则是在适当的时机提交界面刷新事务(类似于 CATransaction)。为了提升渲染的效率,这两者内部都使用了一些优化技巧。原理相关的文章不胜枚举,本文旨在记录 Swift 化中遇到的问题以及一些思考。

OSAtomicIncrement:怎么说,我被弃用了

在获取渲染队列或者取消重复渲染都会用到一个原子自增的数字,但在 Swift 环境中该接口已经被标记为弃用:

1
2
3
@available(iOS 7.1, *)
@available(iOS, deprecated: 10.0, message: "Use atomic_fetch_add_explicit(memory_order_relaxed) from <stdatomic.h> instead")
public func OSAtomicIncrement64(_ __theValue: UnsafeMutablePointer<OSAtomic_int64_aligned64_t>!) -> Int64

如果我们根据提示按部就班,会发现 atomic_fetch_add_explicit 没法直接在 Swift 中使用。此时,需要为 Swift 与 C 之间的函数调用建立中间层,套路如下:

  1. 创建 .h 和 .c 文件声明并实现函数
  2. 在 Swift 声明函数并通过 @_silgen_name 指定其包装的函数名称(函数签名需要对齐)
  3. 在混编头文件中导入步骤 1 中创建的 .h 文件
1
2
@_silgen_name("atomic_increment_one")
func atomicIncrementOne(_ value: UnsafeMutablePointer<UInt64>) -> UInt64
1
2
3
4
5
6
7
8
9
10
#include <stdatomic.h>

// c_bridge.h
atomic_uint_fast64_t atomic_increment(atomic_uint_fast64_t *value);

// c_bridge.c
atomic_uint_fast64_t atomic_increment_one(atomic_uint_fast64_t *value) {
atomic_fetch_add_explicit(value, 1, memory_order_relaxed);
return atomic_load(value);
}

如果与原始代码进行对比,会发现我们这里用的是无符号整数。这是因为在获取渲染队列时需要指定下标,而当渲染次数足够多的时候,符号整数一定会出现溢出(变成负数),最终导致发生数组越界的崩溃。由于 Swift 静态成员变量初始化天然具备 dispatch_once 的特性,队列管理代码编写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private enum _QueueManager {
static var display: DispatchQueue {
let idx = Int(atomicIncrementOne(&_counter) % _queueCount)
return _queues[idx]
}

private static var _counter = UInt64.zero

// Static members of class or struct are thread-safe for initializing.
private static let _queueCount: UInt64 = {
let maxQueueCount: UInt64 = 16
let apc = UInt64(ProcessInfo.processInfo.activeProcessorCount)
return min(max(1, apc), maxQueueCount)
}()

private static let _queues: [DispatchQueue] = {
var result: [DispatchQueue] = []
for idx in 0..<_queueCount {
result.append(.init(label: "com.swiftyyytext.display.\(idx)", qos: .userInitiated))
}
return result
}()
}

基于闭包的事务去重可行性实验

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
2
3

self._id = "\(Int(bitPattern: ObjectIdentifier(closure as AnyObject)))"

而如果我们查看 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
2
3
4
5
6
7
8
9
10
11
12
13
14

func add() {
let blockA = {}
let blockB = {}

SYY.Transaction.commit(_renderA)
SYY.Transaction.commit(_renderA)
SYY.Transaction.commit(_renderB)
SYY.Transaction.commit(_renderB)
}

private func _renderA() {}
private func _renderB() {}

总结

当我从头到尾阅读完 YYText 异步渲染流程相关代码之后,在某种程度上觉得自己行了。但在用 Swift 完成实际的编码时,会发现于胸的成竹变得慢慢变得模糊了。因为这其中涉及到不少的细节问题,处理是否得当将直接影响到 YYLabel/YYTextView 的实现复杂度。举个直观的🌰:YYAsyncLayerDelegate 从逻辑上肯定可以通过新的成员变量来指定,比如 yyDelegate。那么问题来了,UIView 的 layer 实际上是通过指定的 layerClass 隐式创建的,那么 yyDelegate 的赋值时机是什么呢?挨个在指定构造器里写上 (layer as? YYTextAsyncLayer).yyDelegate = self 这句代码吗?