5

Swift 拾遗 - struct & class

 3 years ago
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.
neoserver,ios ssh client
Date Notes 2020-08-06 首次提交

Preface

《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的枚举 structclass。"

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 位于 0x00007ffeefbff4a0baz 位于 0x00007ffeefbff498,它们的地址差值均为 8,即占用 8 个字节:

(lldb) p 0x00007ffeefbff4a8 - 0x00007ffeefbff4a0
(Int) $R0 = 8
(lldb) p 0x00007ffeefbff4a0 - 0x00007ffeefbff498
(Int) $R2 = 8

接下来我们尝试使用 LLDB 中的 memory readx 命令,查看这些变量对应内存地址中的内容:

(lldb) x --size 8 --format x --count 4 0x00007ffeefbff498
0x7ffeefbff498: 0x0000000000000001 0x0000000000000003
0x7ffeefbff4a8: 0x00000001005052a0 0x0000000000000000

其中只有 0x7ffeefbff4a8foo 中的内容有些不同,其中并没有直接存储类中变量的值,而是一段堆空间的内存地址。

如何确定分配堆空间

内存可以被分为很多不同的区域,其中栈空间无需开发者手动申请和回收,比如函数调用时栈空间系统自动分配,而当函数返回时又会被系统自动回收;而堆空间需要手动开辟和回收。在 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

Reference


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK