String源码解析
一、Swift String 在内存中是如何存储的
今天我们一起来研究一下 String 这个类,我们先来看一下当我们创建一个空的字符串发生了什么?
var empty = ""
print(empty)
首先我们的思路是找到 String
的源码,然后找到对应的初始化方法,这里我们直接搜索源文件就可以看到如下代码:
/// Creates an empty string.
///
/// Using this initializer is equivalent to initializing a string with an
/// empty string literal.
///
/// let empty = ""
/// let alsoEmpty = String()
@inlinable @inline(__always)
@_semantics("string.init_empty")
public init() { self.init(_StringGuts()) }
当前的 init
方法调用了内部的 init
方法,该方法接收一个 _StringGuts
的对象作为参数。
public struct String {
public // @SPI(Foundation)
var _guts: _StringGuts
@inlinable @inline(__always)
internal init(_ _guts: _StringGuts) {
self._guts = _guts
_invariantCheck()
}
同样的,在上面的代码我们也可以看到,结构体 String
持有 _StringGuts
作为成员变量。
所以我们接下来关注的重点就是 _StringGuts
这个属性,我们直接来到 StringGuts.swift
这个
文件来看初始化方法
// Empty string
@inlinable @inline(__always)
init() {
self.init(_StringObject(empty: ()))
}
同样的 StringGuts
是一个结构体,该结构体持有 StringObject
作为成员变量
internal var _object: _StringObject
我们按照这个线索找下去,找到 StringObject.Swift
这个文件,定位到对应的方法
@inlinable @inline(__always)
internal init(empty:()) {
// Canonical empty pattern: small zero-length string
#if arch(i386) || arch(arm) || arch(arm64_32) || arch(wasm32)
self.init(
count: 0,
variant: .immortal(0),
discriminator: Nibbles.emptyString,
flags: 0)
#else
self._countAndFlagsBits = 0
self._object = Builtin.valueToBridgeObject(Nibbles.emptyString._value)
#endif
_internalInvariant(self.smallCount == 0)
_invariantCheck()
}
可以看到在判断条件的分支中,调用了 init(count: variant: discirminator: flags:)
这个方法, 同样的这几个都是结构体 StringObject
的成员变量
了解了上面 String
的基本数据结构之后,我们就来一起看一下当我们在创建一个字符串的过程中,都存储了些什么内容
@usableFromInline
internal var _count: Int
@usableFromInline
internal var _variant: Variant
@usableFromInline
internal var _discriminator: UInt8
@usableFromInline
internal var _flags: UInt16
那么也就意味着当前的 String
这个结构体在底层存储的内容就是上面的内容。
下面来看一下 Nibbles
是什么
// Namespace to hold magic numbers
@usableFromInline @frozen
enum Nibbles {}
可以看到也是一个枚举类型,但是这里只是定义,我们在源码里面稍微翻一翻就能够找到关于它
的定义:
extension _StringObject.Nibbles {
// The canonical empty string is an empty small string
@inlinable @inline(__always)
internal static var emptyString: UInt64 {
return _StringObject.Nibbles.small(isASCII: true)
}
}
extension _StringObject.Nibbles {
// Discriminator for small strings
@inlinable @inline(__always)
internal static func small(isASCII: Bool) -> UInt64 {
return isASCII ? 0xE000_0000_0000_0000 : 0xA000_0000_0000_0000
}
可以看到,这里调用的方法判断标准是如果当前是 ASCII
码,那么当前的 discriminator
(判别器的意思)就是 0xE000_0000_0000_0000
,如果不是就是 0xA000_0000_0000_0000
这里我们可以通过一个例子来理解一下:
对于一个空的字符串,打印输出的结果如下
![](https://img.haomeiwen.com/i2936157/e79adfd143c2e74b.png)
对于一个包含中文的字符串打印输入结果如下:
![](https://img.haomeiwen.com/i2936157/b853d724d31e018c.png)
看到这里我们已经明白了,A
、E
这里是用来标识当前是否是 ASCII
码,其中后面的数字是用来标志当前的的字符串的数量。
StringObject{
#if arch(i386) || arch(arm)
_count
_variant
_discriminator
#else
@usableFromInline
internal var _countAndFlagsBits: UInt64
@usableFromInline
internal var _object: Builtin.BridgeObject
}
其中 _discriminator
占据 4
位,每一位的标识如下:
┌─────────────────────╥─────┬─────┬─────┬─────┐
│ Form ║ b63 │ b62 │ b61 │ b60 │
╞═════════════════════╬═════╪═════╪═════╪═════╡
│ Immortal, Small ║ 1 │ASCII│ 1 │ 0 │
├─────────────────────╫─────┼─────┼─────┼─────┤
│ Immortal, Large ║ 1 │ 0 │ 0 │ 0 │
╞═════════════════════╬═════╪═════╪═════╪═════╡
│ Native ║ 0 │ 0 │ 0 │ 0 │
├─────────────────────╫─────┼─────┼─────┼─────┤
│ Shared ║ x │ 0 │ 0 │ 0 │
├─────────────────────╫─────┼─────┼─────┼─────┤
│ Shared, Bridged ║ 0 │ 1 │ 0 │ 0 │
╞═════════════════════╬═════╪═════╪═════╪═════╡
│ Foreign ║ x │ 0 │ 0 │ 1 │
├─────────────────────╫─────┼─────┼─────┼─────┤
│ Foreign, Bridged ║ 0 │ 1 │ 0 │ 1 │
└─────────────────────╨─────┴─────┴─────┴─────┘
其中 Nibbles
的布局结构如下:
┌────────────┐
│ nativeBias │
├────────────┤
│ 32 │
└────────────┘
┌───────────────┬────────────┐
│ b63:b60 │ b60:b0 │
├───────────────┼────────────┤
│ discriminator │ objectAddr │
└───────────────┴────────────┘
对于原生的 Swift
字符串来说,采取的是 tail-allocated
存储,也就是在当前实例分配有超出其最后存储属性的额外空间,额外的空间可用于直接在实例中存储任意数据,无需额外的堆分配。这里我们来验证一下:
![](https://img.haomeiwen.com/i2936157/419b4096aec10b83.png)
接下来我们需要关注的是 0x8000000100000f60
这个值,根据上面源码的阅读,我们知道当前 0x8
标识的是大字符串,这点我们在源代码里面也可以找到答案
![](https://img.haomeiwen.com/i2936157/61dfbc33d1764049.png)
同时结合 nibbles
在内存当中的布局我们知道其中 b60:b0
是存储字符串的地址,当然这个地址要加上偏移量,这个偏移量是 32
,这里我们通过计算器来验证一下
![](https://img.haomeiwen.com/i2936157/4ca1f3ba6b530e56.png)
那么前面的 8
个字节是什么呢呢?我们先从初始化的流程来看
![](https://img.haomeiwen.com/i2936157/e44a18d64fb881a1.png)
![](https://img.haomeiwen.com/i2936157/2badf773f7da6674.png)
所以看一看到,除了我们当前的地址和标识位之外,剩余的就是 countAndFlags
,这里我们可以看到布局如下:
┌─────────┬───────┬──────────────────┬─────────────────┬────────┬───────┐
│ b63 │ b62 │ b61 │ b60 │ b59:48 │ b47:0 │
├─────────┼───────┼──────────────────┼─────────────────┼────────┼───────┤
│ isASCII │ isNFC │ isNativelyStored │ isTailAllocated │ TBD │ count │
└─────────┴───────┴──────────────────┴─────────────────┴────────┴───────┘
![](https://img.haomeiwen.com/i2936157/ac4e67e96f3382f0.png)
![](https://img.haomeiwen.com/i2936157/b3343ec1c7f028e9.png)
第一个标志位是 isASCII
,如果我们修改成中文,这里就会改变
![](https://img.haomeiwen.com/i2936157/0bed7fb6ae992198.png)
二、Swift Index
我们先来回答第一个问题,聊到这个问题我们就必须要明白 Swift String
代表的是什么? 一系列的 characters
(字符),字符的表示方式有很多种,比如我们最熟悉的 ASCII
码, ASCII
码一共规定了 128
个字符的编码,对于英文字符来说 128
个字符已经够用了,但是相对于其他语言来说,这是远远不够用的。
这也就意味着不同国家不同语言都需要有自己的编码格式,这个时候同一个二进制文件就有可能 被翻译成不同的字符,有没有一种编码能够把所有的符号都纳入其中,这就是我们熟悉的
Unicode
,但是 Unicode
只是规定了符号对应的二进制代码,并没有详细明确这个二进制代码应该如何存储。
什么意思,这里我们举一个列子:假设我们有一个字符串 我是Kody
,其中对应的 Unicode
分别是
我 6212
是 662F
K 004B
O:006F
D: 0064
y: 0079
可以看到,上述的文字每一个对应一个十六进制的数,对于计算机来说能够识别的是二进制,所
以这个时候如果存储就会出现下面的情况
我 0110 0010 0001 0010
是 0110 0110 0010 1111
K 0000 0000 0100 1011
O 0000 0000 0110 1111
D 0000 0000 0110 0100
y 0000 0000 0111 1001
UTF-8
最大的一个特点,就是它是一种变⻓的编码方式。它可以使用 1~4
个字节表示一个符 号,根据不同的符号而变化字节⻓度。这里我们简单说一下 UTF-8
的规则:
- 单字节的字符,字节的第一位设为
0
,对于英语文本,UTF-8
码只占用一个字节,和ASCII
码 完全相同; -
n
个字节的字符(n>1)
,第一个字节的前n
位设为1,第n+1
位设为0
,后面字节的前两位都设为10
,这n
个字节的其余空位填充该字符unicode
码,高位用0
补足。
我 11100110 10001000 10010010
是 11100110 10011000 10101111
K 0100 1011
O 0110 1111
D 0110 0100
y 0111 1001
对于 Swift
来说,String
是一系列字符的集合,也就意味着 String
中的每一个元素是不等⻓的。那也就意味着我们在进行内存移动的时候步⻓是不一样的,什么意思? 比如我们有一个
Array
的数组(Int
类型),当我们遍历数组中的元素的时候,因为每个元素的内存大小是一致的,所以每次的偏移量就是 8
个字节。
但是对于字符串来说不一样,比如我要方位 str[1]
那么我是不是要把 我
这个字段遍历完成之后才能够确定 是
的偏移量?依次内推每一次都要重新遍历计算偏移量,这个时候无疑增加了很多的内存消耗。这就是为什么我们不能通过 Int
作为下标来去访问 String
这里我们可以很直观的看到 Index
的定义:
![](https://img.haomeiwen.com/i2936157/6a3be6e6c1e1c7c3.png)
从下面的注释我们大致明白了上述表示的意思:
position aka encodedffset
:一个 48bit
值,用来记录码位偏移量
transcoded offset
:一个 2bit
的值,用来记录字符使用的码位数量
grapheme cache
:一个 6bit
的值,用来记录下一个字符的边界
reserved
:7bit
的预留字段
scalar aligned
:一个 1bit
的值,用来记录标量是否已经对齐过
Moya 源码解析
这个问题我们直接借用 Moya
官网上的一张图,我们日常都会和网络打交道不管是使用 AFN
还是 Alamofire
,虽然这两者都封装了 URLSession
,不用让我们使用官方繁琐的 API
。
久而久之我们会发现我们的 APP
中到处都散落着和 AFN
、Alamofire
相关的代码,不便于统 一的管理,而且很多代码内容是重复的,于是我们就会新建一个中间层 Network layer
来统一 管理我们代码中 AFN
、Alamofire
的使用。
于此同时我们仅仅希望我们的 App
只和我们的 Network layer
打交道,不用关心底层使用的哪个 三方的网络库,即使进行迁移,也应该对我们的上层业务逻辑毫无变化,因为我们都是通过
Network layer
来耦合业务逻辑的。
但是因为抽象的颗粒度不够,我们往往写着写着就会出现越过 Network layer
,直接和我们的三方网络库打交道,这样就违背了我们设计的原则,而 Moya
就是对网络业务逻辑的抽象,我们只需要遵循相关协议,就可以发起网络请求,而不用关心底层细节。
![](https://img.haomeiwen.com/i2936157/59031643f510fd8b.png)
Moya 是如何一步步构建出来的?
在看 Moya
是如何一步步构建出来的,我们先来看一下 Moya
如何使用。首先我们新建一个文 件 TEST.swift
,这里用来存放我们网络层相关的逻辑。接下来我们新建一个 enum TEST
,当然 这这里面我们目前还没有那么多的逻辑分支,我们先空着,接下来使用对当前的 enum
就行,这里我们遵循协议 TargetType
,点击进入头文件可以看以下 TargetType
中定义的都是基础的网络请求数据。
Moya
的模块可以大致分成这几类:
![](https://img.haomeiwen.com/i2936157/0e60726c47daf79e.png)
其次 Moya
主要的数据处理流程可以用下面这张图来表示:Moya
流程图,对于这张图我们一点点来分析,我们先来看第一个阶段
![](https://img.haomeiwen.com/i2936157/6473e0cd1921d9ee.png)
第一步创建了一个遵守 TargetType
协议的枚举,这个过程中我们完成网络请求的基本配置;接下来通过 endpointClosure
的加工生成了一个 endPoint
,点击进入 EndPoint
的文件中,可以看到这里是对 TargetType
的一层再包装,其中 endpointClosure
的代码如下
public typealias EndpointClosure = (Target) -> Endpoint
public let endpointClosure: EndpointClosure
@escaping EndpointClosure = MoyaProvider.defaultEndpointMapping
final class func defaultEndpointMapping(for target: Target) -> Endpoint {
//这里就省略了 return
Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
let endpointClosure = { (target: GitHub) -> Endpoint in
Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
以上就是关于 TargetType
通过 endpointClosure
转化为 endPoint
的过程。
下一步就是把利用 requestClosure
,传入 endPoint
,然后生成 request
。 request
生成过程和 endPoint
很相似。我们一起来看一下
public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void
public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void
public let requestClosure: RequestClosure
final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
整体上使用 do-catch
语句来初始化一个 urlRequest
,根据不同结果向闭包传入不同的参数。一开始使用 try
来调用 endpoint.urlRequest()
,如果抛出错误,会切换到 catch
语句中去。至于 endpoint.urlRequest()
它其实做的事情很简单,就是根据前面说到的 endpoint
的那些属性来初始化一个 NSURLRequest
的对象。
生成了 Request
之后,就交给 Provider
来发起网络请求了
@discardableResult
open func request(_ target: Target,
callbackQueue: DispatchQueue? = .none,
progress: ProgressBlock? = .none,
completion: @escaping Completion) -> Cancellable {
let callbackQueue = callbackQueue ?? self.callbackQueue
return requestNormal(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}
其中 requestNormal
方法
let endpoint = self.endpoint(target)
let stubBehavior = self.stubClosure(target)
let cancellableToken = CancellableWrapper()
endPoint
这个我们再上面的代码分析中已经说过了,stub
是有关测试桩的代码这里我们都暂且忽略,cancellableToken
是取消的标识
internal class CancellableWrapper: Cancellable {
internal var innerCancellable: Cancellable = SimpleCancellable()
var isCancelled: Bool { innerCancellable.isCancelled }
internal func cancel() {
innerCancellable.cancel()
}
}
internal class SimpleCancellable: Cancellable {
var isCancelled = false
func cancel() {
isCancelled = true
}
}
CancellableWrapper
是对 SimpleCancellable
的又一层包装,都遵循了 Cancellable
的协议, 这里我们也可以遵循自己定义的协议,所以这里我们可以看到当前的 Class
都是 internal
。接下来就是 performNetworking
这个闭包表达式的分析,我们先一步步来看
if cancellableToken.isCancelled {
self.cancelCompletion(pluginsWithCompletion, target: target)
return
}
如果取消请求,则调用取消完成的回调, 直接 return
,不再执行闭包内下面的语句。
var request: URLRequest!
switch requestResult {
case .success(let urlRequest):
request = urlRequest
case .failure(let error):
pluginsWithCompletion(.failure(error))
return
}
cancellableToken.innerCancellable = self.performRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
执行 requestClosure
requestClosure(endpoint, performNetworking)
{(endpoint:Endpoint, closure:RequestResultClosure) in
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
高阶函数
高阶函数的本质也是函数,有两个特点
- 接受函数或者是闭包作为参数
- 返回值是一个函数或者是闭包
Map函数
Map
函数作用于 Collection
中的每一个元素,然后返回一个新的 Collection
。
![](https://img.haomeiwen.com/i2936157/75ea4218b335edb8.png)
flatMap函数
我们先来看一下 flatMap
的定义
public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throw
flatMap
中的闭包的参数同样是 Sequence
中的元素类型,但其返回类型为
SegmentOfResult
。在函数体的范型定义中, SegmentOfResult
的类型其实就是 Sequence
而 flatMap
函数返回的类型是: SegmentOfResult.Element
的数组。从函数的返回值来看,与
map
的区别在于 flatMap
会将 Sequence
中的元素进行 “压平”,返回的类型会是
Sequence
中元素类型的数组,而 map
返回的这是闭包返回类型的数组。
![](https://img.haomeiwen.com/i2936157/1f8d32fb4d5288b5.png)
相比较我们的 map
来说,flatMap
最主要的两个作用一个是压平,一个是过滤空值。
![](https://img.haomeiwen.com/i2936157/43050a41d78fafe4.png)
![](https://img.haomeiwen.com/i2936157/a240822ac1d6f039.png)
我们这里再看一个列子:
![](https://img.haomeiwen.com/i2936157/7e0f20ee2f94ae6e.png)
可以看到这里我们使用 map
做集合操作之后,得到的 reslut
是一个可选的可选,那么这里其实我们在使用 result
的过程中考虑的情况就比较多
通过 flatMap
我们就可以得到一个可选值而不是可选的可选
![](https://img.haomeiwen.com/i2936157/57800432ebe518a2.png)
我们来看一下源码
![](https://img.haomeiwen.com/i2936157/ec27762722026cb1.png)
flatMap
对于输入一个可选值时应用闭包返回一个可选值,之后这个结果会被压平, 也就是返回一个解包后的结果。本质上,相比 map
,flatMap
也就是在可选值层做了一 个解包。
![](https://img.haomeiwen.com/i2936157/8ce51dc17467c6c1.png)
使用 flatMap
就可以在链式调用时,不用做额外的解包工作,什么意思呢?我们先来看我们使用 map
来进行链式调用
![](https://img.haomeiwen.com/i2936157/dbc76d5b95356664.png)
这里我们得到的是一个可选的可选,而且在调用的过程中如果有必要我们依然需要进行解包的操作
![](https://img.haomeiwen.com/i2936157/fc82b45e47a68e24.png)
什么时候使用 compactMap
当转换闭包返回可选值并且你期望得到的结果为非可选值的序列时,使用 compactMap
。
let arr = [[1, 2, 3], [4, 5]]
let result = arr.map { $0 }
// [[1, 2, 3], [4, 5]]
let result = arr.flatMap { $0 }
// [1, 2, 3, 4, 5]
let arr = [1, 2, 3, nil, nil, 4, 5]
let result = arr.compactMap { $0 }
// [1, 2, 3, 4, 5]
什么时候使用 flatMap
当对于序列中元素,转换闭包返回的是序列或者集合时,而你期望得到的结果是一维数组时,使用 flatMap
。
let scoresByName = ["Hank": [0, 5, 8], "kody": [2, 5, 8]]
let mapped = scoresByName.map { $0.value }
// [[0, 5, 8], [2, 5, 8]] - An array of arrays
print(mapped)
let flatMapped = scoresByName.flatMap { $0.value }
// [0, 5, 8, 2, 5, 8] - flattened to only one array
CompactMap函数
什么时候使用 compactMap:
当转换闭包返回可选值并且你期望得到的结果为非可选值的序列 时,使用 compactMap
。
什么时候使用 flatMap:
当对于序列中元素,转换闭包返回的是序列或者集合时,而你期望得到的结果是一维数组时,使用 flatMap
Reduce 函数
![](https://img.haomeiwen.com/i2936157/6171872eecf99877.png)
为了更好的理解当前 reduce
的工作原理,我们来试着实现一下 map
、flatMap
、filter
函数
func customMap(collection: [Int], transform: (Int) -> Int) -> [Int] {
return collection.reduce([Int]()){
var arr: [Int] = $0
arr.append(transform($1))
return arr
}
}
let result = customMap(collection: [1, 2, 3, 4, 5]) {
$0 * 2
}
如何找出一个数组中的最大值
let result = [1, 2, 3, 4, 5].reduce(0) {
return $0 < $1 ? $1 : $0
}
print(result)
又或者我们如何通过 reduce
函数逆序
let result = [1, 2, 3, 4, 5].reduce([Int]()){
return [$1] + $0
}
print(result)
网友评论