Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析
1.String在内存中是如何存储的
创建一个空的字符串发生了什么?
var empty = ""
print(empty) // 断点在这里调试看
这里并不能看出String的内存结构。那么接下来就借助Swift源码的方式看看String在内存中到底是如何存储的。
打开swift源码 -> stdib里的String.swift
@frozen
public struct String {
public // @SPI(Foundation)
var _guts: _StringGuts
@inlinable @inline(__always)
internal init(_ _guts: _StringGuts) {
self._guts = _guts
_invariantCheck()
}
// This is intentionally a static function and not an initializer, because
// an initializer would conflict with the Int-parsing initializer, when used
// as function name, e.g.
// [1, 2, 3].map(String.init)
@_alwaysEmitIntoClient
@_semantics("string.init_empty_with_capacity")
@_semantics("inline_late")
@inlinable
internal static func _createEmpty(withInitialCapacity: Int) -> String {
return String(_StringGuts(_initialCapacity: withInitialCapacity))
}
/// 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
其中最后有一个创建空字符串初始化方式 self.init(_StringGuts())
:
接下来看看这个_StringGuts
到底是什么东西?
同样找到swift源码 -> stdib里的StringGuts.swift
@frozen
public // SPI(corelibs-foundation)
struct _StringGuts: @unchecked Sendable {
@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: ()))
}
}
_StringGuts
也是一个结构体,它有一个成员变量是_StringObject
类型的实例;
并且在最后是通过初始化出一个_StringObject
类型的实例来初始化_StringGuts
的。
所以真正swift的String
的实质就是_StringObject
。接下来看看_StringObject
到底是什么玩意儿?
找到swift源码 -> stdib里的StringObject.swift
,可以看到_StringObject
是一个结构体,再找到空字符串的初始化函数:
ps: 注意这里初始化时的传参,下面会说到这几个成员
最终找到字符串最终初始化函数,该函数是对成员的初始化赋值,那么只要搞懂这几个成员是代表什么意思,那就能搞清楚字符串的底层实质了。
_StringObject
存储着一些成员变量,文章最开始使用x/8g格式化输出一个空字符串对象empty的时候,那我猜测:输出的内容应该就是_StringObject
里的_count、_variant、_discriminator、_flags。
@usableFromInline
internal var _count: Int // 字符串大小
@usableFromInline
internal var _variant: Variant // 枚举值 默认0
@usableFromInline
internal var _discriminator: UInt8 // 在初始化的时候传递了一个Nibbles.emptyString
@usableFromInline
internal var _flags: UInt16
internal var _variant: Variant
是一个枚举值,默认是immortal 0:
@usableFromInline @frozen
internal enum Variant {
case immortal(UInt) // 原始字符串
case native(AnyObject) // AnyObject
case bridged(_CocoaString) // NSString
@inlinable @inline(__always)
internal static func immortal(start: UnsafePointer) -> Variant {
let biased = UInt(bitPattern: start) &- _StringObject.nativeBias
return .immortal(biased)
}
@inlinable @inline(__always)
internal var isImmortal: Bool {
if case .immortal = self { return true }
return false
}
}
internal var _discriminator: UInt8
在初始化的时候传递了一个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)
}
}
// Discriminator for small strings
@inlinable @inline(__always)
internal static func small(isASCII: Bool) -> UInt64 {
// 是ASCII就用0xE000_0000_0000_0000否则用0xA000_0000_0000_0000
return isASCII ? 0xE000_0000_0000_0000 : 0xA000_0000_0000_0000
}
0xE000_0000_0000_0000 与文章最上面截图相对应起来了:
那接下来我们就能测试一下字符串了:
字符a的ASCII编码是97,97的16进制是61,注意那个2的字节位的输出
小于等于15个字符串时,会记录字符串的位数。
对于小字符串(小于等于15个字符串)来说,是优先直接存到内存当中,无需另外分配内存空间的。(和NSString差不多类似)
接下来看看中文字符
中文字符不是ASCII编码,一个中文字符占据3个字节(24位),也是我们上面通过源码分析得出的使用了 0xA000_0000_0000_0000
所以_StringObject.Nibbles
是一个识别器,去识别字符串是不是ASCII编码。
对于大字符串(大于15个字符串)来说,原本的小字符串占据的15个字节已经不足以存储字符串了,那就会发生改变:
来看看0x8000000000000000在源码中出现的定义是一个大原始字符串:
那剩下的 0x000000010000b860 到底是什么东西呢?它是字符串的内存相对地址;
那应该偏移多少呢?来看源码里的注解
意思是0x10000b860需要加上偏移量nativeBias
即32,32的16进制是0x20:
0x10000b860 + 0x20 = 0x10000b880
在源码注解里找到大字符串标志位
大字符串前8位就记录着这些标志位信息,0xd000000000000012就是大字符串前8位,拿到科学计算器里看看标志位:
所以count是0x12,转换成10进制就是18,正好对应18个字符。
总结:
小字符串
:大小在15字节以内(包含15),直接存到内存当中,无需另外分配内存空间的(和NSString差不多类似)。
大字符串
:大小在15字节以上,会另外开辟内存去存储字符串内容。字符串对象的第一个8字节存储的是标志位信息;第二个最高位标识大字符串,剩下的是相对偏移地址信息,需要偏移0x20才是真实另外开辟的内存地址。
二、String.Index
对于String
来说,它并不支持通过下标的方式获取字符
只能通过String.Index
的方式来访问
var str = "hello world"
// str.index(str.startIndex, offsetBy: 1) 从开始位置向后偏移1,返回结果是String.Index类型
print(str[str.index(str.startIndex, offsetBy: 1)]) // e
对于 Swift
来说,String
是一系列字符的集合,也就意味着 String
中的每一个元素是不等长的。那也就意味着我们在进行内存移动的时候步长是不一样的,什么意思?
比如我们有一个 Array
的数组(Int 类型),当我们遍历数组中的元素的时候,因为每个元素的内存大小是一致的,所以每次的偏移量就是 8 个字节。
但是对于字符串来说不一样,比如我要方位 str[1]
那么我是不是要把我这个字段遍历完成之后才能够确定是的偏移量?
依次内推每一次都要重新遍历计算偏移量,这个时候无疑增加了很多的内存消耗。这就是为什么我们不能通过 Int
作为下标来去访问 String
。
可以很直观的看到 Index
的定义:
position aka encodedOffset
一个 48 bit 值,用来记录码位偏移量;
transcoded offset
: 一个 2 bit 的值,用来记录字符使用的码位数量;
grapheme cache
: 一个 6 bit 的值,用来记录下一个字符的边界;
reserved
: 7 bit 的预留字段;
scalar aligned
: 一个 1 bit 的值,用来记录标量是否已经对齐过。
String.Index
的本质就是一个64位的位域信息,这个位域信息展示的就是上面的解释。
创建String.Index
实际上就是通过encodedOffset 或者 transcoded offset
,encodedOffset
就是方便我们从内存中通过下标访问到字符串。