Swift 拾遗 - struct & class
source link: https://kingcos.me/posts/2020/swift_struct_and_class/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 个字节,其中的内容如下表:
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
Reference
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK