Date | Notes |
---|---|
2020-08-06 | 首次提交 |
Preface
《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的枚举 struct
和 class
。"
结构体
Swift 中的结构体被广泛使用,这是因为它和枚举都是值类型,在操作时会更加安全。我们这里定义一个简单的结构体:
struct Foo {
var a = 1
var b = 1
}
var foo = Foo() // Breakpoint 🔴
withUnsafeMutablePointer(to: &foo) { print("\($0)") } // 0x00000001000030d8
print(MemoryLayout<Foo>.size) // 16
print(MemoryLayout<Foo>.stride) // 16
print(MemoryLayout<Foo>.alignment) // 8
// (lldb) memory read 0x00000001000030d8
// 0x1000030d8: 01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................
// 0x1000030e8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
我们可以从以上的内存地址和对应的值中看出,结构体变量的地址就是结构体中第一个成员的地址,结构体中所有成员的地址是连续的;结构体变量占用内存的大小受其成员的影响。
再来一个更加复杂的嵌套类型的例子:
// ...
print(MemoryLayout<Int>.stride) // 8
print(MemoryLayout<Bool>.stride) // 1
print(MemoryLayout<Double>.stride) // 8
print(MemoryLayout<Foo>.stride) // 16
struct Bar {
var a: Int
var b: Bool
var c: Bool
var d: Int
var e: Foo
}
var bar = Bar(a: 10, b: true, c: true, d: 11, e: Foo(a: 11, b: 12))
var baz = 20
print(MemoryLayout<Bar>.size) // 40
print(MemoryLayout<Bar>.stride) // 40
print(MemoryLayout<Bar>.alignment) // 8
// (lldb) po withUnsafeMutablePointer(to: &bar) { print("\($0)") }
// 0x0000000100004290
// (lldb) x/6xg 0x0000000100004290
// 0x100004290: 0x000000000000000a 0x0000000000000101
// 0x1000042a0: 0x000000000000000b 0x000000000000000b
// 0x1000042b0: 0x000000000000000c 0x0000000000000014
此时虽然 Bar
结构体中嵌套了 Foo
结构体,但其仍按照 8
个字节进行内存对齐;其中连续的两个 Bool
类型的变量共同占用一份 8
个字节的内存。
类
Swift 中的类不同于结构体和枚举,是引用类型,即初始化后会在堆上开辟空间存储类中的变量,并将首地址通过栈上的指针变量引用。
当函数调用时,将会在栈上开辟一块内存空间供函数使用。为了更加直观,我们在函数中分别创建类和结构体的变量并初始化:
class Foo {
var a = 1
var b = 2
}
struct Bar {
var a = 3
}
func test() {
var foo = Foo()
var bar = Bar()
var baz = 4
withUnsafeMutablePointer(to: &foo) { print("\($0)") } // 0x00007ffeefbff4a8
withUnsafeMutablePointer(to: &bar) { print("\($0)") } // 0x00007ffeefbff4a0
withUnsafeMutablePointer(to: &baz) { print("\($0)") } // 0x00007ffeefbff498
}
print(MemoryLayout<Foo>.size) // 8
print(MemoryLayout<Foo>.stride) // 8
print(MemoryLayout<Foo>.alignment) // 8
test()
在 test
函数的栈空间中,内存地址从高地址开始分配,因此 foo
首地址位于 0x00007ffeefbff4a8
,而 bar
位于 0x00007ffeefbff4a0
,baz
位于 0x00007ffeefbff498
,它们的地址差值均为 8
,即占用 8 个字节:
(lldb) p 0x00007ffeefbff4a8 - 0x00007ffeefbff4a0
(Int) $R0 = 8
(lldb) p 0x00007ffeefbff4a0 - 0x00007ffeefbff498
(Int) $R2 = 8
接下来我们尝试使用 LLDB 中的 memory read
或 x
命令,查看这些变量对应内存地址中的内容:
(lldb) x --size 8 --format x --count 4 0x00007ffeefbff498
0x7ffeefbff498: 0x0000000000000001 0x0000000000000003
0x7ffeefbff4a8: 0x00000001005052a0 0x0000000000000000
其中只有 0x7ffeefbff4a8
即 foo
中的内容有些不同,其中并没有直接存储类中变量的值,而是一段堆空间的内存地址。
如何确定分配堆空间
内存可以被分为很多不同的区域,其中栈空间无需开发者手动申请和回收,比如函数调用时栈空间系统自动分配,而当函数返回时又会被系统自动回收;而堆空间需要手动开辟和回收。在 C/C++ 中,通常在调用
alloc
/malloc
函数时将会在堆上开辟一段内存空间。在 Swift 中我们也可以观察是否调用了malloc
函数来判断构造方法到底有没有在堆上开辟内存空间。我们在var foo = Foo()
一行打个断点,并 Xcode Menu - Debug - Debug Workflow - Always Show Disassembly:demo`test(): ; ... -> 0x1000011eb <+43>: movq %rdx, %rdi ; ... 0x100001206 <+70>: callq 0x100001050 ; demo.Foo.__allocating_init() -> demo.Foo at main.swift:1 ; ...
跳转到
0x100001206
一行并si
(Step Into):demo`Foo.__allocating_init(): -> 0x100001050 <+0>: pushq %rbp ; ... 0x100001064 <+20>: callq 0x100001c4e ; symbol stub for: swift_allocObject ; ...
跳转到
0x100001064
一行并si
:demo`swift_allocObject: -> 0x100001c4e <+0>: jmpq *0x13ec(%rip) ; (void *)0x0000000100001cec
从此处连续
si
多次直到进入libdyld.dylib`dyld_stub_binder:
中,跳转至最后一行并si
:libswiftCore.dylib`swift_allocObject: -> 0x7fff6df5dd00 <+0>: pushq %rbp ; ... 0x7fff6df5dd22 <+34>: callq 0x7fff6df5dc90 ; swift_slowAlloc ; ...
跳转到
0x7fff6df5dd22
一行并si
:libswiftCore.dylib`swift_slowAlloc: -> 0x7fff6df5dc90 <+0>: pushq %rbp ; ... 0x7fff6df5dca4 <+20>: callq 0x7fff6dfda28c ; symbol stub for: malloc ; ...
此时我们就能看到符号桩
malloc
,也可以进一步si
直到进入libsystem_malloc.dylib`malloc:
,Obj-C 中的alloc
其实也来自这里,这也验证了类的对象分配在堆空间。关于malloc
可详见《iOS 中的 NSObject》一文。
我们继续查看这个内存地址中的内容:
(lldb) x --size 8 --format x --count 4 0x00000001005052a0
0x1005052a0: 0x00000001000031a8 0x0000000000000002
0x1005052b0: 0x0000000000000001 0x0000000000000002
虽然 Foo
类型的变量只占用 8 个字节,这是因为在 64 位下,指针存储的内存地址占用 8 个字节。而指针指向的内存地址,即对象实际占用的内存空间这里我们可以看出一共占用了 32 个字节,其中的内容如下表:
内存地址 | 内容 | 信息 |
---|---|---|
0x1005052a0 | 0x00000001000031a8 | 指向类型信息 |
0x1005052a8 | 0x0000000000000002 | 引用计数 |
0x1005052b0 | 0x0000000000000001 | bar.a |
0x1005052b8 | 0x0000000000000002 | bar.b |
我们也可以通过 Foundation
中的 malloc_size
函数获取对象实际被分配的内存大小:
import Foundation
// ...
var foo = Foo()
// Swift / Obj-C 中的对象将按照 16 的倍数进行对齐
print(malloc_size(Unmanaged.passUnretained(foo).toOpaque())) // 32
// class_getInstanceSize 并非对象实际被分配的内存大小
print(class_getInstanceSize(Foo.self)) // 32
print(class_getInstanceSize(type(of: foo))) // 32