1、String
1.1、String源码分析
- 首先我们来看一下空字符串的
var empty = "" print(empty)
- 查找源码我们需要从初始化入手,其中的
init
方法调用了内部的init
方法,该方法接收一个_StringGuts
的对象作为参数/// 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()) }
- String结构体结构 持有
_StringGuts
作为成员变量,所以我们接下来关注的重点就是 _StringGuts 这个属性,我们直接来到StringGuts.swift
这个文件来找到它的定义:// StringGuts is a parameterization over String's representations. It provides // functionality and guidance for efficiently working with Strings. // @frozen public // SPI(corelibs-foundation) struct _StringGuts: UnsafeSendable { @usableFromInline internal var _object: _StringObject @inlinable @inline(__always) internal init(_ object: _StringObject) { self._object = object _invariantCheck() } // Empty string @inlinable @inline(__always) init() { self.init(_StringObject(empty: ())) } }
- 初始化方法中需要一个
StringObject
作为成员变量internal var _object: _StringObject
- _StringObject 也是一个结构体,我们找到
_StringObject(empty: ())
这个方法:@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 的成员变量@inlinable @inline(__always) init(count: Int, variant: Variant, discriminator: UInt64, flags: UInt16) { _internalInvariant(discriminator & 0xFF00_0000_0000_0000 == discriminator, "only the top byte can carry the discriminator and small count") self._count = count self._variant = variant self._discriminator = UInt8(truncatingIfNeeded: discriminator &>> 56) self._flags = flags self._invariantCheck() }
_count
应该是代表当前字符串的大小_variant
的类型是 Variant,它是一个枚举,这个枚举代表字符串的三种情况,分别为immortal
、native
以及bridged
;通过刚才初始化方法的传值,此时的 _variant 类型是Variant.immortal(0)
类型的,这个代表 Swift 原生的字符串类型;其结构如下:internal enum Variant { case immortal(UInt) case native(AnyObject) case bridged(_CocoaString) // function ...... }
_discriminator
外部传进来的是一个 Nibbles.emptyString 下面来看一下Nibbles
是什么
它是一个什么都没有的枚举,这里只进行了定义,那它可能在其他位置进行了实现,最后找到了它的 extension 中// Namespace to hold magic numbers @usableFromInline @frozen enum Nibbles {}
emptyString 返回的是一个 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 }
samll(isASCII:)
(判断是否为小字符串
) 方法,true 代表是ASCII
,此时返回是0xE000_0000_0000_0000
,false 代表不是 ASCII,此时返回0xA000_0000_0000_0000
- 初始化方法中需要一个
1.2、小字符串
-
一个空的字符串,打印输出的结果如下:
-
对于一个英文字符串"abc",打印如下:
-
对于一个包含中文的字符串打印输入结果如下:(中文单字符占据位数要比英文多)
-
看到这里我们已经明白了,
0xa
、0xe
这里是用来标识当前小字符串是否是ASCII
码(大字符串用0x8
表示)- 中文字符不属于ASCII
- 其中后面的数字是用来标志当前的的 字符串的大小
- 前面的 636261 是 abc 的编码,也就是 636261 代表的就是 abc
-
我们都知道一个字符串的大小为 16 个字节,那这些字符是否都是存储在这 16 个字节中呢?我们查看"abcdefghijklmno"这15个字母长的字符串怎么存储;选15个是因为上边说了,有1位用来存储了字符串大小,所以选了这个临界值长度
- 原来前边存满了后,接着字符串大小标志接着存到满
- 那么问题又来了,如果再比这个字符串长呢?这里就不能使用 小字符串 了,而是变成了 大字符串
1.3、大字符串
-
我们来看一下"abcdefghijklmnoabc"的内存打印结果
-
此时这 16 个字节存储的内容就变了,变成什么了呢?在源码中有这么一段注释:
- 当字符串的大小小于 15 的时候属于小字符串,当字符串的大小大于 15 的时候属于大字符串;。通过官方的注释得知 大字符串可以是原生的、共享的或者是外来的(这里主要探究原生的字符串)。这个原生的字符串具有尾部分配的存储空间,它从
存储对象地址的 nativeBias 偏移量
开始 - 也就是意味着当字符串的大小大于 15 的时候,会分配额外的存储空间,用这个额外的存储空间存储字符串的值
-
首先
nativeBias
的值为 32。接下来在 64 位中,discriminator
(鉴别器) 和objectAddr
,根据官方给的注释,这个 discriminator 占据的位置是高 63 位到高 60 位,objectAddr 占据的位置是高 60 位到低 0 位,存储的就是这个额外的存储空间的内存地址- 其中
_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 │ └─────────────────────╨─────┴─────┴─────┴─────┘
- 其中
-
这个 objectAddr 存储的是这个额外的存储空间的 相对内存地址,因为它需要加上 nativeBias,得到的才是这个额外的存储空间的地址值
-
- 当字符串的大小小于 15 的时候属于小字符串,当字符串的大小大于 15 的时候属于大字符串;。通过官方的注释得知 大字符串可以是原生的、共享的或者是外来的(这里主要探究原生的字符串)。这个原生的字符串具有尾部分配的存储空间,它从
-
此时回到 abcdefghijklmnoabc 的内存布局,前面的 8 个字节(0xd000000000000012)我们先不管,看后面的 8 个字节(0x8000000100003f60)。
0x8
就是这个鉴别器 discriminator 的值,后面的 100003f60 就是额外存储空间 objectAddr 的相对地址。所以我们来试着加一下 nativeBias 的到的值后是否就是字符串的存储空间100003f60 + 32 = 100003f60 + 0x2 = 100003f80
- 我们格式化输出一下 0x100003f80:
果然,0x100003f80 就是字符串的内存地址,那我怎么知道这个 0x8 就是鉴别器的值呢?因为在源码中直接规定了,代码如下:
// Discriminator for large, immortal, swift-native strings @inlinable @inline(__always) internal static func largeImmortal() -> UInt64 { return 0x8000_0000_0000_0000 }
- 对于原生的
Swift
字符串来说,采取的是tail-allocated
存储,也就是在当前实例分配有超出其最后存储属性的额外空间,额外的空间可用于直接在实例中存储任意数据
- 我们格式化输出一下 0x100003f80:
果然,0x100003f80 就是字符串的内存地址,那我怎么知道这个 0x8 就是鉴别器的值呢?因为在源码中直接规定了,代码如下:
-
现在我们再来看看刚才搁置的8个字节,官方有对于大字符串标志位的描述:
- isASCII:用来判断当前字符串是否是 ASCII,在高 63 位。
- isNFC:这个默认为 1,在高 62 位。
- isNativelyStored:是否是原生存储,在 高 61 位。
- isTailAllocated:是否是尾部分配的,在 高 60 位。
- TBD:留作将来使用,在高 59 位到高 48 位。
- count:当前字符串的大小,在高 47位到低 0 位。
0x12 的十进制是 18,所以当前字符串的大小为 18。所以当字符串为大字符串的时候,前 8 个字节存储的就是
countsAndFlags
总结
-
一个 String 变量/常量的大小为 16 个字节
-
当字符串的大小 不超过 15 的时候为小字符串,超过 15 的时候为大字符串
-
当为小字符串的时候,在 16 字节的大小中,前 15 个字节用来存储字符串的值,最后一个字节用来记录当前字符串是否是 ASCII 和字符串的大小
-
当为大字符串的时候,在 16 字节的大小中,前 8 个字节用来记录字符串的大小和其它的一些信息,比如是否是 ASCII。剩余的 8 个字节中,高 63 位到高 60 位存储的是鉴别器的值,60 位到 0 位 存储字符串的存储空间的地址值
2、Swift Index
-
在Swift中,获取字符串中某一个字符的值我们可以通过
Index
去获取-
index(i: offsetBy: )
:获取字符串偏移多少位字符的String.index?
- 我们首先准备一段字符串"Hello World"
let str = "Hello World"
- 我们取出这段字符串中的第7个字符
let index = str.index(str.startIndex, offsetBy: 6 , limitedBy:str.endIndex) print(str[index!]) //打印结果 W
-
-
我们想要获取字符串中的第 n 位字符,Swift通过把 n 转化成一个 String.index,再通过其定位到该字符,但是为什么不能通过角标直接获取呢?
-
Swift String 代表的是什么? 一系列的 characters (字符),字符的表示方式有很多种,比如我们最熟悉的 ASCII码,ASCII 码一共规定了128个字符的编码,对于英文字符来说 128 个字符已经够用了,但是相对于其他语言来说,这是远远不够用的。这也就意味着不同国家不同语言都需要有自己的编码格式,这个时候同一个二进制文件就有可能被翻译成不同的字符,有没有一种编码能够把所有的符号都纳入其中---这就是我们熟悉的
Unicode
,但是 Unicode 只是规定了符号对应的二进制代码,并没有详细明确这个二进制代码应该如何存储。-
什么意思呢,假设这里有一个字符串:"我是Coder",那这个字符串对应的 Unicode 如下:
我:6211 是:662f C:0043 o:006f d:0064 e:0065 r:0072
可以看到,上述的文字每一个对应一个十六进制的数,对于计算机来说能够识别的是二进制,那它们的二进制表示如下:
我:0110 0010 0001 0001 是:0110 0110 0010 1111 C:0000 0000 0100 0011 o:0000 0000 0110 1111 d:0000 0000 0110 0100 e:0000 0000 0110 0101 r:0000 0000 0111 0010
从上边的二进制表示中我们一下就看出,中英文统一步长的情况下,对于中文字符得到了充分利用,但是对于英文字符来说,存在大量的 0000 这种未利用空间,造成了内存浪费
-
为了解决上边内存浪费的问题,就使用了
UTF-8
,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 C:0100 0011 o:0110 1111 d:0110 0100 e:0110 0101 r:0111 0010
-
-
对于 Swift 来说,String 是一系列字符的集合,也就意味着 String 中的每一个元素是不等长的。那也就意味着我们在进行内存移动的时候步长是不一样的,什么意思?比如我们有一个 Array 的数组(Int 类型),当我们遍历数组中的元素的时候,因为每个元素的内存大小是一致的,所以每次的偏移量就是 8 个字节
但是对于字符串来说不一样,比如访问
str[1]
,那么是不是要把 "我" 这个字段遍历完成之后才能够确定 "是" 的偏移量?依次内推每一次都要重新遍历计算偏移量,这个时候无疑增加了很多的内存消耗。这就是为什么不能通过 Int 作为下标来去访问 String 的原因。 -
源码中有关于 String 的 Index 布局描述:
-
position aka encodedOffset:一个 48 bit 值,用来记录码位偏移量。
-
transcoded offset:一个 2 bit 的值,用来记录字符使用的码位数量。
-
grapheme cache:一个 6 bit 的值,用来记录下一个字符的边界。
-
reserved:7 bit 的预留字段 scalar aligned: 一个 1 bit 的值,用来记录标量是否已经对齐过。
-
-
对于 String 的 Index 的本质是存储了 encodedOffset 和 transcoded offset。当我们构建 String 的 Index 的时候,其实是把 encodedOffset 和 transcoded offset 计算出来存放到 Index 的内存信息里面。而这个 Index 本身就是一个 64 位的位域信息
-
3、Array
3.1、Array源码分析
-
初始化数组时会调用
_allocateUninitializedArray
方法 -
这个方法返回一个元组,通过 count 判断当前初始化的数组是否需要分配 缓冲区。我们来看当 count 大于 0 的情况,当大于 0 的时候,会调用一个
allocWithTailElems_1
方法,分配堆空间来存储数组当中的元素
。接着通过_adoptStorage
来创建数组 此时返回的这个元组里返回了Array的buffer
和firstElementAddress
,这个 firstElementAddress 是数组第一个元素的地址;为什么要返回第一个元素的地址呢?是因为在第一个元素的地址之前,应该还有其它的信息 所以, Array 里存储的其实是一个名为 _ContiguousArrayStorage 类的内存地址,而这个类存储这个存储着 元类型,引用计数,元素的个数,容量的大小和标志位,以及元素的内存地址
3.2、Array追加数据
-
当你向数组添加元素并且该数组开始超出其保留容量时,该数组会分配更大的内存区域并将其元素复制到新存储中;新存储是旧存储大小的倍数,触发重新分配的追加操作有性能成本,但随着阵列变大,它们发生的频率越来越低
-
当数组 append 数据时
@inlinable @_semantics("array.append_element") public mutating func append(_ newElement: __owned Element) { // Separating uniqueness check and capacity check allows hoisting the // uniqueness check out of a loop. _makeUniqueAndReserveCapacityIfNotUnique() let oldCount = _buffer.mutableCount _reserveCapacityAssumingUniqueBuffer(oldCount: oldCount) _appendElementAssumeUniqueAndCapacity(oldCount, newElement: newElement) _endMutation() }
-
首先它调用了一个
_makeUniqueAndReserveCapacityIfNotUnique
方法:@inlinable @_semantics("array.make_mutable") internal mutating func _makeUniqueAndReserveCapacityIfNotUnique() { if _slowPath(!_buffer.beginCOWMutation()) { _createNewBuffer(bufferIsUnique: false, minimumCapacity: count + 1, growForAppend: true) } }
-
如果缓冲区的存储是唯一引用的,将缓冲区置于可变状态,然后去创建新的 buffer。我们接着看这个
_createNewBuffer(bufferIsUnique:minimumCapacity:growForAppend)
的实现:@_alwaysEmitIntoClient @inline(never) @_semantics("optimize.sil.specialize.owned2guarantee.never") internal __consuming func _consumeAndCreateNew(bufferIsUnique: Bool, minimumCapacity: Int, growForAppend: Bool ) -> _ArrayBuffer { let newCapacity = _growArrayCapacity(oldCapacity: capacity, minimumCapacity: minimumCapacity, growForAppend: growForAppend) ... }
-
在这个方法当中它会去调用
_growArrayCapacity
方法来进行扩容: -
所以,一个数组在进行扩容的时候,本质上是在旧的 capacity 上进行 2 倍扩容
-