最近有兴趣想要看看 Swift 对象的内存布局,所以尝试研究一下这个话题。这篇文章主要是关注结构体和类的内存模型。
# MemoryLayout
Swift 官方提供了 MemoryLayout
这样的一个类方便我们来分析内存模型,这个类描述了类型的 size
, stride
和 alignment
The memory layout of a type, describing its size, stride, and alignment.
举个例子来分别解释一下 size
,stride
和 alignment
。如下代码
struct Point {
let x: Double
let y: Double
let isFilled: Bool
}
print(MemoryLayout<Point>.size) //17
print(MemoryLayout<Point>.stride) //24
print(MemoryLayout<Point>.alignment) //8
alignment
属性是表示类型的内存对齐。在这个例子里 Double
是占用 8 个字节,isFilled
是占用 1 个字节。结构体总体大小是要能被最大的成员的内存大小整除。所以这里 Point
是按照 8 个字节来进行内存对齐的。
size
是表示结构体真正需要的大小 类型实例占用连续内存字节的大小。这里两个 Double
类型加一个字节的 isFilled
类型就是 17 个字节。
stride
表示结构体真正分配内存的大小,它和 size 的区别在于 stride
是需要考虑内存对齐的,所以即使是只占用 1 个字节的 Bool 类型,也要出于内存对齐的原因占用 8 个字节,剩下的 7 个字节其实是被浪费掉了。
值得注意的是 size 的计算方式,如果结构体类型是下面这种的化,尽管实际可能只需要 9 个字节(Int
占用 8 个字节),但是最终的 size 是显示 16 个字节,所以 size 并不是计算需要的内存空间,而是连续占用的内存空间。
struct Point {
let isFilled: Bool
let i:Int
}
print("\(MemoryLayout<Point>.size)") //16个字节
print("\(MemoryLayout<Point>.stride)") //16个字节
print("\(MemoryLayout<Point>.alignment)") //8个字节
我这里感觉 MemoryLayout
其实有点类似 OC 中的 class_getInstanceSize (opens new window) 的作用。要记得 OC 里面还有 malloc_size
用来计算对象真正在内存中占用的空间呢,所以 Swift 中有没有类似的 API 呢?
其实 Swift 中也是能直接使用 malloc_size
的,不过作为值类型的 Struct
是存储在栈上的。而 malloc_size
统计的是内存分配的信息,是堆上的信息,同时最小分配 16 个字节这种要求也是针对堆上的内存分配。栈上的内存占用只需要满足内存对齐规则就好了。
# Struct
Struct 在 Swift 中被大量使用,比如 String 和 Array 都是结构体类型。值类型的临时变量一般都是存放栈上的。其实看明白上面一段 MemoryLayout 的介绍也就基本上能明白结构体的内存分配了。
举个直观的例子来看下,在结尾打断点,查看 point 实例的内存布局,内存是不会骗人的。
struct Point {
var isFilled: Bool
var a:Int
}
var point = Point(isFilled: true, a: 10)
var value = 100
# Xcode 查看内存
这里跑个题,说下Xcode 查看内存的方式
在 Xcode 变量调试区,右键 point 变量,点击「View Memory of “point”」,然后就能看到对应
point
实例的内存信息。前 8 个字节是isFilled
,后 8 个字节是a
变量。后面紧跟着的是 value 对象。这里内存的字节序是通过小端存储,所以看着还有点费劲
·如果我们知道 point 实例的内存地址的话,也可以通过 的相关命令来看,详细的文档说明看
LLDB 命令 (opens new window)
memory read 0x0000000100008000
打印结果如下0x100008000: 01 00 00 00 00 00 00 00 0a 00 00 00 00 00 00 00 ................ 0x100008010: 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 d...............
结果不是很直观,可以使用这个命令
memory read --size 8 --format x 0x0000000100008000
,这里的展示结果就很好看了。0x100008000: 0x0000000000000001 0x000000000000000a 0x100008010: 0x0000000000000064 0x0000000000000000
x
是memory read
命令的简写形式,LLDB 中也有x
命令,GDB 中也有x
命令,不过两者对应的参数配置不一样,这里稍微有点让人混淆。GDB 命令 (opens new window)
x/4gx 0x0000000100008000
。检查内存中的 8个字(double word)的内容,并以十六进制格式显示它们的值。打印结果同上面的 memory read 指令打印结果。0x100008008: 0x0000000000000001 0x000000000000000a 0x100008018: 0x0000000000000064 0x0000000000000000
p/x point
命令是按照 16 进制格式展示 point 命令。这个是不需要内存打印的。指令说明在这里 (opens new window)。(TerminalToolSwiftDemo.Point) (isFilled = true, a = 0x000000000000000a)
这里其实还涉及到一个问题,我们如何通过代码获取结构体实例的内存地址?可以使用如下 API,打印出来的值就是结构体实例对应的地址,可以通过 Xcode IDE 查看内存变量来验证。
var point = Point(isFilled: true, a: 10)
withUnsafeMutablePointer(to: &point) {
print("\($0)")
}
好的,回到正题,我们通过内存查看的方式,可以确认 point 结构体实例在真实内存中也是占用 16 个字节。
# Class
类和结构体不同,类是引用类型,在堆上分配内存空间,然后栈上仅保留指向堆上内存空间的指针。
我们通过下面这个例子来尝试分析一下类实例的内存布局
class Foo {
var a = 1
var b = 2
}
func test() {
var foo = Foo()
var baz:Int = 4
print(MemoryLayout<Foo>.size) //8
print(MemoryLayout<Foo>.stride) //8
print(MemoryLayout<Foo>.alignment) //8
withUnsafeMutablePointer(to: &foo) { print("\($0)") } // 0x000000016fdff1a8
withUnsafeMutablePointer(to: &baz) { print("\($0)") } // 0x000000016fdff1a0
}
打印出来的 foo
和 baz
地址都是栈上指针的地址,因为栈上的内存空间分配是从高地址到低地址分配,所以先声明的类实例 foo
的栈指针所在的内存地址更高,并且占用 8 个字节,所以 baz
的所在内存地址就等于 foo
的指针地址减去 8 个字节。
通过命令来看下 0x000000016fdff1a0
处的内存信息。
(lldb) x/2gx 0x000000016fdff1a0
0x16fdff1a0: 0x0000000000000004 0x0000600002f4b080
(lldb) x/4gx 0x0000600002f4b080
0x600002f4b080: 0x0000000100008118 0x0000000000000003
0x600002f4b090: 0x0000000000000001 0x0000000000000002
大概就是下面这个样子的
为 foo
实例分配的堆上的内存空间中,除了我们赋值的 1,2 之外,还有起始的两个8字节的内容:
- 首个 8 字节(
0x0000000100008118
)的内容,应该和 OC 中的isa
类似,保存着指向类信息的指针。 - 第二个8 字节(
0x0000000000000003
)中的内容参考网上的文章,大家认为是存储的内容是引用计数(这个我并没有深入研究,感觉讲明白还需要单独写一篇博客,这里先存疑)。
所以真正属性内容是从第 16 个字节起始的,这里类实例真正占用的内存空间应该是 32 个字节,而我们代码当中通过 MemoryLayout
打印的 foo
相关的 size
就是不正确的,它返回的是指针的大小,而并非是对应堆上内存空间的大小。
我们之前说过 Swift
也可以通过 malloc_size
的方法去获取内存大小,于是有了下面获取内存实例大小的方法,打印结果也是 32 字节。
(lldb) p malloc_size(Unmanaged.passUnretained(foo).toOpaque())
(Int) 32
参考地址: