Swift汇编分析结构体和类的本质02

1、结构体

结构体都有一个编译器自动生成的初始化器。
根据情况可能会生成多个初始化器,保证所有的成员(存储属性、Stored Property)都有初始值。

struct Person {
    var name: String
    var age: Int
}
//正确
var person = Person(name: "wang", age: 18)
//错误
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()

可以看到我们在确保所有的储存属性有值的情况下去初始化才是正确的。
那我们也可以这样去定义属性

struct Person {
    var name: String = ""
    var age: Int = 0
}

var person = Person(name: "wang", age: 18)
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()

这种情况下下面的即中初始化方法均可,因为我们已经给我们的存储属性赋了初值了。
那同样,我们也可以给定其初始值为nil,这样也可以使用下面的初始化方法。

当然,我们也可以自定义初始化器。

struct Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

同样我们也可以在自定义的初始化器中给定其默认值。

struct Person {
    var age: Int?
    
    init() {
        self.age = 10
    }
}
var person = Person()

此时我们通过汇编进入看一下,可以看到其调用了init函数

image.png

通过命令si进入该函数
image.png

可以看到在第8行处将10赋值到一处内存中。

但如果我们没有自定义初始化器呢?也就是直接给定其一个默认值的情况下,其内部是怎么调用的呢?

struct Person {
    var age: Int = 10
}
var person = Person()

通过断点走入汇编代码。可以看到与上面相比基本是一模一样的,甚至连调用方法的地址都是一致的。

image.png

可以看到仍然是调用了init方法,同样进入该方法中,可以看到也是有赋值操作的。
image.png

也就是说,不管我们是通过自定义初始化器给定属性默认值还是直接定义属性时给定其默认值都是在init方法中将数据写入属性中去的。

至于结构体在内存中的布局其实与上一期我们讲到的枚举是一致的。
比如上面我们实例化的person对象,其有一个属性为age,该属性在内存中占8个字节,又因为其内存对齐字节为8,因此其内存中的布局为0x000000000000000a

2、类

相比结构体而言类是没有可以传入成员值的初始化器的。
同样,当我们定义了类以后没有给其成员默认值的情况下也是会有错误提示的


image.png

但是类因为没有默认的成员初始化器,因此我们直接在初始化时传入成员属性时不可行的:


image.png

此时需要我们手动去给其实现初始化器。

3、区别

其实结构体和类的本质区别在于结构体是值类型而类是引用类型。
也就是说结构体对象其位于内存中即栈空间中,而引用类型其真正的内存空间在堆空间中。
但严格来说我们并不能说结构体内存一定是在栈空间,其内存分配在哪儿取决于它定义的位置,如果其在函数内定义的那么其内存在栈空间内。如果其在外部定义,那么其内存在数据段内(全局区)。如果其作为一个类的属性,那么其内存自然是会分配在堆空间。
至于类的话不管在哪儿定义其内存一定是在堆空间,在不同的地方定义改变的只是其指针的内存位置。

问题:那如果结构体定义在一个类的方法里边其内存会在哪儿?

3.1 内存分配

那么我们怎么去区分一个对象是在栈空间还是在堆空间?
其实如果对象是在堆空间时其初始化时会牵涉到alloc或者malloc

此时我们定义一个类对象并实例化,此时加上断点

image.png

汇编开启后会看到
image.png

此时可以看到在第14行处明显的有调用allocating_init,即向堆空间申请内存,此时通过命令si进入
image.png

可以看到在第16行处有allocObject,此时进入,然后一直通过si命令进入。
最终到这个地方:
image.png

可以看到在这里调用了一个slowAlloc方法。
进入后可以看到在这里会调用系统级别的malloc方法分配内存
image.png

而同样的我们可以看到结构体会调用init,也就是直接分配在栈空间的。

//类对象堆空间申请内存过程

接下来我们看看structclass在内存中的地址。定义一个结构体以及类,并实例化

struct StructObject {
    var age: Int = 10
    var count: Int = 11
}


class ClassObject {
    var age: Int = 10
    var count: Int = 20
}

var strutObject = StructObject()
var classObject = ClassObject()



print("结构体变量的地址:",Mems.ptr(ofVal: &strutObject))
print("结构体变量的内容:",Mems.memStr(ofVal: &strutObject))

print("----------------")

print("类变量指针的地址:",Mems.ptr(ofVal: &classObject))
print("类变量指针的内容:",Mems.memStr(ofVal: &classObject))

此时我们查看打印结果:

结构体变量的地址: 0x00000001000083c8
结构体变量的内容: 0x000000000000000a 0x000000000000000b
----------------
类变量指针的地址: 0x00000001000083d8
类变量指针的内容: 0x000000010079c950

其中0x00000001000083c80x00000001000083d8分别是两个对象的地址。我们查看内存中的值,可以看到前8个字节是Person.age的值,后面8个字节是Person.count的值

image.png

而类对应的内存中只是存放了另一个地址,而这个地址实际上指向的就是堆空间。

image.png

可是我们根据该地址去查看内存中的数据时发现前8个字节以及第2个8个字节中放的数据不知道是啥,而从第3个把个字节才看到是对应的属性的值。
其实类对应的存储空间中,前8个字节指向的是内存信息,第2个8字节是引用计数信息,后面就是成员信息了。
我们通过计算类变量指针地址与结构体变量的指针地址可以获知其差了0x10,也就是16个字节,那么0x00000001000083c8这个地址存放0x000000000000000a,接下来8个字节存放0x000000000000000b,接下来的地址0x00000001000083d8存放的就是0x000000010079c950内容,也就是这三个挨着的地址前两个放着结构体的内容,后一个存放着类对象的地址。

我们也可以打印出类对象对应的信息:

print("类变量指针所指向的内容的地址:",Mems.ptr(ofRef: classObject))
print("类变量指针所指向的内容的内容:",Mems.memStr(ofRef: classObject))

类变量指针所指向的内容的地址: 0x00000001006086f0
类变量指针所指向的内容的内容: 0x00000001000082c8 0x0000000200000002 0x000000000000000a 0x0000000000000014

可以看到指针中存放的就是类对象的地址,类对象的4个8字节即是前面所说的一些信息。

我们也可以分析结构体对象和类对象在内存中的大小。
上述我们定义的结构体对象有2个属性,那么其在内存中的大小为16字节
类对象同样也有两个属性,但是指针仅占8字节,而这8个字节中存的是类对象的地址,而真正的类对象是占用32个字节的。
上述结论可通过MemoryLayout打印结果可知。

print("结构体内存大小",MemoryLayout.stride)
print("类对象指针内存大小",MemoryLayout.stride)

结构体内存大小 16
类对象指针内存大小 8
3.2 值类型汇编分析

值类型为深拷贝(deep copy) ,引用类型为浅拷贝(shallow copy)
深拷贝也就是赋值一份副本,即重新分配一块内存而浅拷贝仅仅是定义一个指针指向相同的内存。

    func test() {

    struct Auction {
        var id: Int
        var type: Int
    }

    let auction = Auction(id: 10, type: 20)
    var p = auction
    p.id = 11
    p.type = 22
    print("qeqwwq")

}
test()

同样断点分析汇编

image.png

直接找立即数,可以看到在第8行和第9行处将10和20分别放入ediesi中,而实际上ediesi内容在rdirsi中可取得,地市我们进入init方法中:
image.png

可以看到分别从rdirsi中取值放入raxrdx中,这个时候raxrdx分别存放的就是1020,此时再看第一张图片中call指令下面可以看到分别将raxrdx中的值放入了-0x10(%rbp)-0x8(%rbp)中,同时也放入了-0x20(%rbp)-0x18(%rbp)中,其实这也就对应了代码的实例化auction以及p = auction ,第15行和16行又将1122放入到p对应的内存中,也就是重新给p赋值操作。
此时我们是将结构体定义在函数内,实际上我们将结构体定义在外部也是一样,只是稍显复杂而已,但是其逻辑完全相同。

在swift标准库中字符串、数组、字典其底层都是结构体,因此其也都是值类型。
而swift中为了提升性能它们采用 Copy On Write机制。也就是修改的时候才去copy,如果不进行修改的时候仍然是同一份内存。

当对同一个结构体对象进行第二次实例化的时候其只是对原地址中的数据进行重新赋值,其地址不会改变。

var auction = Auction(id: 10, type: 20)
auction = Auction(id: 11, type: 22)

image.png

查看汇编,直接找到立即数1020可以看到分别赋值给ediesi,也就是rdirsicall调用可函数,进入可以看到,从rdirsi中取出内容又给了raxrdx

SwiftStudy`init(id:type:) in Auction #1 in test():
->  0x1000010e0 <+0>:  pushq  %rbp
    0x1000010e1 <+1>:  movq   %rsp, %rbp
    0x1000010e4 <+4>:  movq   %rdi, %rax
    0x1000010e7 <+7>:  movq   %rsi, %rdx
    0x1000010ea <+10>: popq   %rbp
    0x1000010eb <+11>: retq   

此时我们看汇编断点的截图,函数调用完成后又把raxrdx中的值给了-0x10(%rbp)-0x8(%rbp),此时我们查看对应的内存空间:

image.png

可以看到对应的正是1020,那么对应的地址0x7FFEEFBFF530则是通过拿到rbp的地址后计算得到对应的地址。
可以看到后面又重新走了赋值,调用call方法然后再赋值的操作。可以看到后面的赋值和前面的赋值的地址是一致的。

3.3 引用类型汇编分析

定义一个类

func testClass() {
    
    class Person {
        var age: Int
        var height: Int

        init(age: Int, height: Int) {
            self.age = age
            self.height = height
        }
    }

    var person = Person(age: 10, height: 20)
    var person1 = person
    person1.age = 11
    person1.height = 22
}

同样,进入汇编断点后直接找对应的立即数,可以看待第13、14行对应的分别是1020,分别放入寄存器ediesi,然后执行call指令。

image.png

接下来调用了allocating_init,进入该函数可以看到从对应的寄存器中取值后给到了相邻的两个地址中。

image.png

结束函数调用,回到执行call

image.png

可以知道,函数调用结束后将rax中的值给了-0x10(%rbp)-0x60(%rbp)
而根据我们的代码可知,在实例化person后会将person赋值给person1。那么和这里的汇编代码吻合了,也就是rax存放的就是实例化Person的地址,该地址分别给放在了对应的两个地址中。

  • rax一般存放函数调用的返回值
  • -0x60(%rbp) 类似于这种rbp-地址值的类型一般都是局部变量

获取rax存放的地址值,直接去查看改地址对应的内存

(lldb) register read rax
     rax = 0x00000001005388f0
(lldb) memory read 0x00000001005388f0
0x1005388f0: c0 72 00 00 01 00 00 00 02 00 00 00 00 00 00 00  .r..............
0x100538900: 0a 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00  ................
(lldb) 
image.png

可以看到,存放数据的共有32个字节,第一个8字节存放的是类信息,第2个8字节存放的是引用计数信息,之后便是其两个属性数据了。
接下来我们去看修改person1的属性值。同样我们去找对应的立即数

image.png

可知,将两个立即数放入到了对应的两个对应的空间。此时断点在37行,获取rax的地址,此时的地址既是对应的personperson1内存放的地址。

(lldb) register read rax
     rax = 0x00000001005388f0

而又分别在0x00000001005388f0的基础上移动了16个字节(0x100538900)和24个字节(0x100538908)对应的不就是两个属性数据的地址,也就是这里直接是修改了属性的值了,从而personperson1指向的堆空间的数据也改变了。

  • 类似rax+地址值 一般都是堆空间的地址

你可能感兴趣的:(Swift汇编分析结构体和类的本质02)