Swift 更高效地使用集合(Using Collections
目录
- 基本原理
- 索引和切片
- 懒加载
- 修改和多线程
- Foundation框架和桥接(Bridging)
基本原理
Collection协议
protocol Collection : Sequence {
associatedtype Element
associatedtype Index : Comparable
subscript(position: Index) -> Element { get }
var startIndex: Index { get }
var endIndex: Index { get }
func index(after i: Index) -> Index
}
startIndex, endIndex, index(after: )以及subscript下标的使用示例:
extension Collection {
func everyOther(_ body: (Element) -> Void) {
let start = self.startIndex
let end = self.endIndex
var iter = start
while iter != end {
body(self[iter])
let next = index(after: iter)
if next == end { break }
iter = index(after: next)
}
}
}
(1...10).everyOther { print($0) }
各种集合协议之间的关系
Collections Protocol Hierarchy索引和切片
首先介绍这两种集合协议
Bidirectional Collections:
func index(before: Self.Index) -> Self.Index
Random Access Collections:
// constant time
func index(_ idx: Index, offsetBy n: Int) -> Index
func distance(from start: Index, to end: Index) -> Int
Swift中常见的集合类型
Array, Set, Dictionary
以获取集合中的第二个元素为例,讲解索引和切片的用法:
// 多次判定,然后通过索引获取第二个元素
extension Collection {
var second: Element? {
// Is the collection empty?
guard self.startIndex != self.endIndex else { return nil }
// Get the second index
let index = self.index(after: self.startIndex)
// Is that index valid?
guard index != self.endIndex else { return nil }
// Return the second element
return self[index]
}
}
使用切片更高效地获取第二个元素:
// 使用dropFirst获取去除第一个元素之后的切片,然后获取切片的第一个元素
var second: Element? {
return self.dropFirst().first
}
切片会引用原始的集合,如果切片不释放引用,原始的集合依然存在于内存中:
extension Array {
var firstHalf: ArraySlice<Element> {}
return self.dropLast(self.count / 2)
}
var array = [1, 2, 3, 4, 5, 6, 7, 8]
var firstHalf = array.firstHalf // [1, 2, 3, 4]
array = []
print(firstHalf.first!) // 1
// 复制切片到另一个集合中,释放原始的集合
let copy = Array(firstHalf) // [1, 2, 3, 4]
firstHalf = []
print(copy.first!)
懒加载(lazy方法)
避免低效地去迭代集合中的所有值来获取部分值:
// 返回值为[Int]类型;
// 迭代所有的值,获取最后结果;
let items = (1...4000).map { $0 * 2 }.filter { $0 < 10 }
print(items.first) // 迭代1...4000后获取第一个值
// 返回值为LazyFilterCollection<LazyMapCollection<(ClosedRange<Int>), Int>>类型;
// 在需要获取部分值时,可以避免迭代所有的值;
// 但是每次从items获取值,都会进行新一次的计算;
let items = (1...4000).lazy.map { $0 * 2 }.filter { $0 < 10 }
print(items.first) // 仅迭代一次
懒加载会重复计算,可以将结果保存到另一个集合中:
let bears = ["Grizzly", "Panda", "Spectacled", "Gummy Bears", "Chicago"]
let redundantBears = bears.lazy.filter {
print("Checking '\($0)'")
return $0.contains("Bear")
}
// 输出"Grizzly", "Panda", "Spectacled", "Gummy Bears"这三个元素
// 每次调用redundantBears.first都会输出这3个元素
print(redundantBears.first!)
// 将结果赋值给另一个集合,以防止每次获取过滤结果都进行计算
let filteredBears = Array(redundantBears)
print(filteredBears.first!)
何时使用lazy方法?
- 链式计算
- 只需要集合中的一小部分数据
- 没有副作用
- 避免API边界 (在返回结果给API的调用方时,返回一个实际的集合而不是懒加载对象)
修改集合
接下来介绍另外两种集合协议:
Mutable Collection
// constant time
subscript(_: Self.Index) -> Element { get set }
Range Replaceable Collections
replaceSubrange(_:, with:)
访问集合时发生了崩溃?思考以下几个问题:
- 是否修改了集合?
- 是否是多线程操作集合?
在数组中使用无效的索引,会发生崩溃:
var array = ["A", "B", "C", "D", "E"]
let index = array.firstIndex(of: "E")!
array.remove(at: array.startIndex)
// index已失效
print(array[index]) // Fatal Error: Index out of range.
// 重新获取有效的索引可以避免这种问题发生
if let idx = array.firstIndex(of: "E") {
print(array[idx])
}
在字典中依然要面对这种问题:
var favorites: [String : String] = [
"dessert" : "honey ice cream",
"sleep" : "hibernation",
"food" : "salmon"
]
let foodIndex = favorites.index(forKey: "food")!
print(favorites[foodIndex]) // (key: "food", value: "salmon")
favorites["accessory"] = "tie"
favorites["hobby"] = "stealing picnic supplies"
print(favorites[foodIndex]) // Fatal error: Attempting to access Dictionary elements using an invalid Index
// 重新获取有效的索引可以避免这种问题发生
if let foodIndex = favorites.index(forKey: "food") {
print(favorites[foodIndex])
}
使用索引和切片的建议:
- 谨慎使用索引和切片;
- 修改会导致失效;
- 只在需要时才去进行获取;
你的集合可以多线程访问吗?
运行以下代码,会发生什么事?
var sleepingBears = [String]()
let queue = DispatchQueue.global() // 并发队列,会有多个线程
queue.async { sleepingBears.append("Grandpa") }
queue.async { sleepingBears.append("Cub") }
sleepingBears的值可能为 ["Grandpa", "Cub"], ["Cub", "Grandpa"], ["Grandpa"], ["Cub"]
甚至出现类似的错误:malloc: *** error for object 0x100586238: pointer being freed was not allocated
使用ThreadSanitizer(TSAN)进行检测,可以看到类似如下信息:
WARNING: ThreadSanitizer: Swift access race
Modifying access of Swift variable at 0x7b0800023cf0 by thread Thread 3:
...
Previous modifying access of Swift variable at 0x7b0800023cf0 by thread Thread 2:
...
Location is heap block of size 24 at 0x7b0800023ce0 allocated by main thread:
...
SUMMARY: ThreadSanitizer: Swift access race main.swift:515 in closure #1 in gotoSleep()
使用串行队列替换global()并发队列,即可使集合的操作在单个线程中进行。
var sleepingBears = [String]()
let queue = DispatchQueue(label: "Bear-Cave") // 串行队列,单一线程调度
queue.async { sleepingBears.append("Grandpa") }
queue.async { sleepingBears.append("Cub") }
queue.async { print(sleepingBears) } // ["Grandpa", "Cub"]
多线程操作集合的建议:
- 尽可能从单一线程进行操作;
- 如果必须进行多线程操作:
- 1.使用互斥(加锁);
- 2.使用TSAN(ThreadSanitizer)来检测错误;
建议使用不可变的集合:
- 更容易获取不变的数据;
- 缩小BUG的区域;
- 用切片和懒加载来模拟修改;
- 编译器会帮助你;
在使用集合时,提前预留空间,减少内存分配操作:
Array.reserveCapacity(_:)
Set(minimumCapacity:)
Dictionary(minimumCapacity:)
桥接(Bridging)
Foundation框架中的集合
Foundation框架中的集合都是引用类型,而Swift的集合都是值类型;
在操作Foundation框架中的集合时,要谨慎!
转换Objc中的类型为Swift类型时,桥接都会有消耗,虽然看似代价不大,但是如果桥接的量很大,这个过程就很值得关注了。
识别桥接问题:
- 用Instruments可以帮助检测桥接的代价;
- 在循环中,检测桥接的代价;
- 找出以下常见问题:
- _unconditionallyBridgeFromObjectiveC;
- bridgeEverything;
观察以下代码:
let story = NSString(string: """
Once upon time there lived a family of Brown Bears. They had long brown hair.
...
They were happy with their new hair cuts. The end.
""")
let text = NSMutableAttributedString(string: story)
// text.string由NSString转为String,用Instruments检测到这个操作需要耗费大量时间
let range = text.string.range(of: "Brown")! // Range<String.Index>
let nsrange = NSRange(range, in: text.string) // NSRange
text.addAttribute(.foregroundColor, value: NSColor.brown, range: nsrange)
桥接操作发生在哪里?
更改耗时严重的桥接操作代码,优化性能:
let string = text.string as NSString // NSString
let nsrange = string.range(of: "Brown") // 依然有桥接操作发生,"Brown"由String转为NSString
何时使用Foundation框架中的集合?
- 需要使用引用类型;
- 操作已知的代用品:如: NSAttributedString.string、Core Data中的Managed Objects;
- 你已经识别并且测试了桥接的代价;
接下来怎么办?
- 查看你已经使用的集合;
- 测试你的集合相关代码 (TSAN, Instruments);
- 审查集合的修改状态;
- 在Playground中操练,然后掌握这些知识要点;
如有错误,欢迎指出!😀