本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。
本节将会描述Swift 2.0Beta2的String结构体的内部结构,仅供参考与更深入的理解。读者不应该把它当成文档使用,因为随着Swift的演变,String的内部结构随时会发生变化。
在Swift 2.0中,调用sizeof(String)的结果是24,至少在64位的机器上结果如此。如果是在32位的机器上,结果是12。String的内部存储结构由下面这样的结构体组成:
struct Stringternals {
let buffer: UnsafePointer<Void>
let data: UInt
let copyTracker: UnsafePointer<Void>
}
你可以自己创建字符串变量,然后通过unsafeBitCast方法将它装换成Stringternals结构体:
let hello = "hello"
let bits = unsafeBitCast(hello, Stringternals.self)
print(bits)
// 输出结果:
// Stringternals(buffer: 0x0000000100297b10, data: 5, copyTracker: 0x0000000000000000)
data表示字符串长度,buffer是一个指针,指向实际存储的一组ASCII字符,因此你可以用C语言的puts方法输出这个buffer
// bits.buffer的类型是UnsafePointer<Void>,而puts方法的参数类型需要时UnsafePointer<Int8>
// 所以需要调用UnsafePointer的初始化方法做一个转化
puts(UnsafePointer(bits.buffer)) // 输出结果:hello
输出结构也有可能是字符串hello以及后面的一堆垃圾信息。这是因为buffer可以不用像C语言字符串那样以null结尾。
以上测试结果并不能说明Swift使用UTF-8编码格式存储字符串,我们可以做以下实验:
let hello = "hello,😂"
let bits = unsafeBitCast(hello, Stringternals.self)
print(bits)
// 输出结果:
// Stringternals(buffer: 0x000000010028fdf0, data: 9223372036854775816, copyTracker: 0x0000000000000000)
输出结果与此前有两个区别:
data的值是一个非常大的数:这是因为data现在不仅仅用来存储字符串的长度,它高位上的数用来存储一个flag,表示字符串包含了非ASCII的值,还有一个flag表示这个字符串指向了一个NSString的buffer。我们可以通过添加一个计算属性来屏蔽这些flag,以获得字符串的真实长度:
extension Stringternals {
var length: Int {
let mask = 0b11 << UInt(sizeof(UInt) * 8 - 2)
return Int(data & ~mask)
}
}
let hello = "hello,😂"
let bits = unsafeBitCast(hello, Stringternals.self)
print(bits.length) // 输出结果:8
- 第二个区别在于
buffer指向的字符,现在都是16位的。一旦我们使用了一个或多个非ASCII字符就会导致Swift使用UTF-16编码方式来存储它们。这与非ASCII码字符具体是什么无关,也就是说即使它需要32位来存储,Swift也会使用UTF-16编码方式。
为了证明这一点,我们选择一个4字节(32位)的emoji,然后判断Swift是否使用了UTF-16编码方式。我们可以使用UTF-16.decode方法来解压buffer:
let nonASCII = unsafeBitCast("hello,😂", Stringternals.self)
let buf16 = UnsafeBufferPointer(start: UnsafePointer<UInt16>(nonASCII.buffer), count: nonASCII.length)
let buf32 = UnsafeBufferPointer(start: UnsafePointer<UInt32>(nonASCII.buffer), count: nonASCII.length)
var gen16 = buf16.generate()
var gen32 = buf32.generate()
var utf16 = UTF16()
var utf32 = UTF32()
print("UFT16解码: ", terminator: "")
while case let .Result(scalar) = utf16.decode(&gen16) {
print(scalar, terminator: "")
}
print("\nUFT32解码: ")
while case let .Result(scalar) = utf32.decode(&gen32) {
print(scalar)
}
输出结果如下:
UFT16解码: hello,😂
UFT32解码:
此前我们看到的copyTracker属性一直是一个空指针,这是因为到目前为止我们都是用字符串字面量来初始化字符串,所以buffer指向的是二进制文件中的只读数据区,如果我们使用字符串的初始化方法:
let s1 = String("helllo")
let bits1 = unsafeBitCast(s1, Stringternals.self)
这时的结果就不是空指针了,它是一个指向ARC管理的类引用的指针,与isUniquelyReferenceed函数联合使用,为字符串提供具备写时复制特性的值语义。
这种字符串内部结构的最后一个好处体现在字符串切片方面。调用字符串的split方法得到的其实是一组starting和ending指针,每一对指针对应了字符串buffer内部的一个子数组,于是就避免了不必要的复制。这么做也是有代价的,在ARC机制下,即使一小片字符串切片也会阻止整个字符串被释放,尽管这个字符串的长度可能有好几Mb。
如果你通过NSString创建String类型的变量,copyTracker还会使用一种优化方法,它实际上是指向原来NSString对象的引用,此时的buffer直接指向NSString的存储区域:
let ns = "hello" as NSString
let s = ns as String
let (_,_,ref) = unsafeBitCast(s, (UInt,UInt,NSString).self)
print(ref) // 输出结果:hello
print(ref === ns) // 输出结果:true
Character内部结构
在之前的文章中我们已经介绍过,Swift.Character表示了一组不确定长度的代码点,在Character结构体的内部如何管理这些代码点呢?尝试运行下面这行代码:
print(sizeof(Character)) // 输出结果:9
奇数位长度的类型通常表示这是一个枚举类型——枚举成员占用一位,其他的由关联值占用,所以Character结构体的内部大体上如下:
enum Character {
case ArbitraryLength(Buffer)
case Small(Int64)
}
这种在内部保存一小部分元素,然后切换到堆上的buffer上技术有事被称作“小字符串优化”。由于通常情况下字符长度大约是几个字节,所以在这种情况下,这种技术就特别适用。
需要强调一点,Character实际上是结构体而不是枚举类型,枚举类型只是其内部的一种私有表示,事实上,在命令行中输入以下代码,你可以观察到详细的Character的内部结构:
echo ":type lookup Character" | swift | less
这种技术适用于Swift中的任何类型,以便我们更深入的了解这个类型的内部实现。再重复一遍:这些东西不要当作文档使用,你应该总是依赖于Swift对外提供的文档。