Swift String、Moya 源码解析及高阶函数

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

这里我们可以通过一个例子来理解一下:

对于一个空的字符串,打印输出的结果如下

对于一个包含中文的字符串打印输入结果如下:

看到这里我们已经明白了,AE 这里是用来标识当前是否是 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 存储,也就是在当前实例分配有超出其最后存储属性的额外空间,额外的空间可用于直接在实例中存储任意数据,无需额外的堆分配。这里我们来验证一下:

接下来我们需要关注的是 0x8000000100000f60 这个值,根据上面源码的阅读,我们知道当前 0x8 标识的是大字符串,这点我们在源代码里面也可以找到答案

同时结合 nibbles 在内存当中的布局我们知道其中 b60:b0 是存储字符串的地址,当然这个地址要加上偏移量,这个偏移量是 32,这里我们通过计算器来验证一下

那么前面的 8 个字节是什么呢呢?我们先从初始化的流程来看

所以看一看到,除了我们当前的地址和标识位之外,剩余的就是 countAndFlags,这里我们可以看到布局如下:

┌─────────┬───────┬──────────────────┬─────────────────┬────────┬───────┐
│   b63   │  b62  │       b61        │       b60       │ b59:48 │ b47:0 │
├─────────┼───────┼──────────────────┼─────────────────┼────────┼───────┤
│ isASCII │ isNFC │ isNativelyStored │ isTailAllocated │  TBD   │ count │
└─────────┴───────┴──────────────────┴─────────────────┴────────┴───────┘

第一个标志位是 isASCII,如果我们修改成中文,这里就会改变

二、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 的规则:

  1. 单字节的字符,字节的第一位设为 0,对于英语文本,UTF-8 码只占用一个字节,和 ASCII 码 完全相同;
  2. 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 的定义:

从下面的注释我们大致明白了上述表示的意思:

position aka encodedffset:一个 48bit 值,用来记录码位偏移量
transcoded offset:一个 2bit 的值,用来记录字符使用的码位数量
grapheme cache:一个 6bit 的值,用来记录下一个字符的边界
reserved7bit 的预留字段
scalar aligned:一个 1bit 的值,用来记录标量是否已经对齐过

Moya 源码解析

这个问题我们直接借用 Moya 官网上的一张图,我们日常都会和网络打交道不管是使用 AFN 还是 Alamofire,虽然这两者都封装了 URLSession,不用让我们使用官方繁琐的 API

久而久之我们会发现我们的 APP 中到处都散落着和 AFNAlamofire 相关的代码,不便于统 一的管理,而且很多代码内容是重复的,于是我们就会新建一个中间层 Network layer 来统一 管理我们代码中 AFNAlamofire 的使用。

于此同时我们仅仅希望我们的 App 只和我们的 Network layer 打交道,不用关心底层使用的哪个 三方的网络库,即使进行迁移,也应该对我们的上层业务逻辑毫无变化,因为我们都是通过
Network layer 来耦合业务逻辑的。

但是因为抽象的颗粒度不够,我们往往写着写着就会出现越过 Network layer,直接和我们的三方网络库打交道,这样就违背了我们设计的原则,而 Moya 就是对网络业务逻辑的抽象,我们只需要遵循相关协议,就可以发起网络请求,而不用关心底层细节。

Moya 是如何一步步构建出来的?

在看 Moya 是如何一步步构建出来的,我们先来看一下 Moya 如何使用。首先我们新建一个文 件 TEST.swift,这里用来存放我们网络层相关的逻辑。接下来我们新建一个 enum TEST,当然 这这里面我们目前还没有那么多的逻辑分支,我们先空着,接下来使用对当前的 enum 就行,这里我们遵循协议 TargetType,点击进入头文件可以看以下 TargetType 中定义的都是基础的网络请求数据。

Moya 的模块可以大致分成这几类:

其次 Moya 主要的数据处理流程可以用下面这张图来表示:Moya 流程图,对于这张图我们一点点来分析,我们先来看第一个阶段

第一步创建了一个遵守 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,然后生成 requestrequest 生成过程和 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

flatMap函数

我们先来看一下 flatMap 的定义

public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throw

flatMap 中的闭包的参数同样是 Sequence 中的元素类型,但其返回类型为
SegmentOfResult。在函数体的范型定义中, SegmentOfResult 的类型其实就是 Sequence
flatMap 函数返回的类型是: SegmentOfResult.Element 的数组。从函数的返回值来看,与
map 的区别在于 flatMap 会将 Sequence 中的元素进行 “压平”,返回的类型会是
Sequence 中元素类型的数组,而 map 返回的这是闭包返回类型的数组。

相比较我们的 map 来说,flatMap 最主要的两个作用一个是压平,一个是过滤空值。

我们这里再看一个列子:

可以看到这里我们使用 map 做集合操作之后,得到的 reslut 是一个可选的可选,那么这里其实我们在使用 result 的过程中考虑的情况就比较多

通过 flatMap 我们就可以得到一个可选值而不是可选的可选

我们来看一下源码

flatMap 对于输入一个可选值时应用闭包返回一个可选值,之后这个结果会被压平, 也就是返回一个解包后的结果。本质上,相比 mapflatMap 也就是在可选值层做了一 个解包。

使用 flatMap 就可以在链式调用时,不用做额外的解包工作,什么意思呢?我们先来看我们使用 map 来进行链式调用

这里我们得到的是一个可选的可选,而且在调用的过程中如果有必要我们依然需要进行解包的操作

什么时候使用 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 函数

为了更好的理解当前 reduce 的工作原理,我们来试着实现一下 mapflatMapfilter 函数

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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容