Swift —— 类与结构体

Swift —— 类与结构体,第1张

Swift —— 类与结构体 1. 初识类与结构体1.1 异同点1.2 类(引用类型)1.3 结构体 (值类型)1.4 类和结构体的内存位置1.4.1 结构体的内存位置1.4.2 类的内存位置 2. 类的初始化器2.1 指定初始化器&便捷初始化器2.2 可失败初始化器2.3 必要初始化器 3. 类的生命周期3.1 SIL文件分析3.2 类的初始化流程3.3 swift_allocObject3.4 Swift 对象内存结构

1. 初识类与结构体 1.1 异同点

结构体和类的主要相同点有:

定义存储值的属性定义方法定义下标以使用下标语法提供对其值的访问定义初始化器使用 extension 来拓展功能遵循协议来提供某种功能

主要的不同点有:

类有继承的特性,而结构体没有类型转换使您能够在运行时检查和解释类实例的类型类有析构函数用来释放其分配的资源引用计数允许对一个类实例有多个引用 1.2 类(引用类型)

对于类与结构体我们需要区分的第一件事就是:

类是引用类型。也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体
实例内存地址的引用。

在xcode中来实验一下,声明一个class

class LGTeacher{
    var age: Int
    var name: String
    init(age: Int, name: String){
        self.age = age
        self.name = name
    }
    deinit {
    }
}

输入下面代码后打下断点运行。

那么打印t 和 t1 的值后可以发现,他们的值的内存地址是一样的。

但是t 和 t1 本身的内存地址是不一样的

1.3 结构体 (值类型)

swift 中有引用类型,就有值类型,最典型的就是 Struct ,结构体的定义也非常简单,相比较 类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值)。

声明一个结构体

struct LGStudent{
    var age: Int
    var name: String
}

输入下面代码然后打下断点后运行。

 var t = LGStudent(age: 18, name: "llll")
var t1 = t

从打印可以看到,这里直接打印出来存储的值而不是地址。

当我们修改t1的值的时候,t的值并不会跟着变化。

其实引用类型就相当于在线的 Excel ,当我们把这个链接共享给别人的时候,别人的修改我们 是能够看到的;值类型就相当于本地的 Excel ,当我们把本地的 Excel 传递给别人的时候,就 相当于重新复制了一份给别人,至于他们对于内容的修改我们是无法感知的。

1.4 类和结构体的内存位置

另外引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在
栈上,引用类型存储在堆上。

栈区(stack): 局部变量和函数运行过程中的上下文

声明一个函数

   func test() {
        var age:Int = 10
        print(age)
    }

打下断点后运行,输出age的地址,然后可以看出age是在栈区的。

Heap: 存储所有对象Global: 存储全局变量;常量;代码区Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每 个段又分为很多小的 SectionTEXT.text : 机器码TEXT.cstring : 硬编码的字符串TEXT.const: 初始化过的常量DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量DATA.bss: 没有初始化的(静态/全局)变量DATA.common: 没有初始化过的符号声明 1.4.1 结构体的内存位置

声明一个结构体变量

var s = LGStudent()

打下断点后运行,在lldb中明显可以看到s是在栈区的。

结构体在内存当中的分布示意图:

那么如果在结构体中有含有引用类型,那么会不会改变结构体的位置呢?
在结构体中添加一个LGTeacher属性

运行后发现这里结构体还是在栈区的,这里的变量p也是在栈区上的,只不过里面存储的内存地址是在堆区的。

1.4.2 类的内存位置

接下里,我们将Struct修改为class,来看一下分析结果:

这里可以看到类的位置在堆区上。这里在栈上分配8字节的内存大小,用来存放t。在LGTeacher初始化的过程中,在堆上寻找合适的内存区域,将该内存区域的地址返回,然后将age和name的值拷贝到堆的内存空间上。最后将栈上的内存地址,指向堆区。这个时候就完成了实例对象的内存分配。
如果离开了作用域,那么堆内存就要销毁,那么就会查找并且把内存块重新插入到堆区,也就是说对堆区来说需要有一个查找的过程。然后在销毁在栈上的指针。所以,类和结构体在分配内存空间有时间和速度上的差异。

我们可以通过GitHub上的StructVsClassPerformance工程来分析当前结构体和类的时间分配。

可以使用结构体来作为字典的key来节省时间。这里如果使用String作为字典的key,那么因为字符串是在堆上面的,所以每次在调用makeBalloon的时候, 那么就需要堆内存进行分配和销毁,那么就降低了代码的执行效率。这里如果key使用结构体Balloon,那么就少了堆区上内存的分配和销毁,那么执行效率就提示了。

enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }

var cache = [Balloon : UIImage]()
func makeBalloon(_ balloon: Balloon) -> UIImage {
    if let image = cache[balloon] {
        return image
        
    }
    return UIImage()
}
struct Balloon: Hashable{
    var color: Color
    var orientation: Orientation
    var tail: Tail
}

如果在结构体中过多的使用引用类型也会导致执行效率的降低,比如在结构体中使用String类型。

struct Attachment {
    let fileURL: URL
    let uuid: String
    let mineType: String
    init?(fileURL: URL, uuid: String, mimeType: String) {
        self.fileURL = fileURL
        self.uuid = uuid
        self.mineType = mimeType
    }
}
    
enum MimeType: String {
    case jpeg = "image/jpeg"
}

那么如果修改成值类型,那么就会使执行效率提高。

struct Attachment {
    let fileURL: URL
    let uuid: UUID
    let mineType: MimeType
    init?(fileURL: URL, uuid: UUID, mimeType: MimeType) {
        self.fileURL = fileURL
        self.uuid = uuid
        self.mineType = mimeType
    }
}
    
enum MimeType: String {
    case jpeg = "image/jpeg"
}

2. 类的初始化器

初始化器的目的就是确保在访问类及其属性的时候,类和属性都已经被初始化了。

当前的类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!这里可以看到类如果没有实现初始化器是会报错的。结构体没有指定初始化器则有默认的初始化方法。

2.1 指定初始化器&便捷初始化器

Swift 中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。所以 类 LGPerson 必须要提供对应的指定初始化器,同时我们也可以为当前的类提供便捷初 始化器(注意:便捷初始化器必须从相同的类里调用另一个初始化器。)

class LGPerson {
    var age: Int
    var name: String
    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
        
    }
    convenience init() {
        self.init( 18, "Kody")
    }
}

如果没有便捷初始化器,那么我们在创建类的代码的时候就可能会有很多初始化器,比如

  init(_ age: Int) {
        self.age = age
        self.name = "name"
        
    }
    init( _ name: String) {
        self.age = 18
        self.name = name
    }

那么这个时候调用初始化器的时候,那么就会有三种方式,这三种都可以通过这样的途径来构建实例对象。有时候我们只希望通过其中一种来实现,因为这一种里面有很多默认的配置,而在这个过程中,需要指定初始化器,也就是当调用API的时候,是通过这个途径来初始化实例对象的,从而严格把控提供API的入口。而init 就是指定初始化器,而 convenience init,也就是便捷初始化器,就是提供上面两种其他初始化器的。

class LGPerson {
    var age: Int
    var name: String
    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
        
    }
  convenience  init(_ age: Int) {
        self.age = age
        self.name = "name"
        
    }
    convenience init( _ name: String) {
        self.age = 18
        self.name = name
    }
    convenience init() {
        self.init( 18, "Kody")
    }
}

始化器规则:

指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性
都要初始化完成。(确保成员变量访问安全)

指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖

便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。

初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例
属性的值,也不能引用 self 作为值。

2.2 可失败初始化器

这个也非常好理解,也就意味着当前因为参数的不合法或者外部条件 的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句, 来表明可失败初始化器在何种情况下会触发初始化失败。写法也非常简单:

2.3 必要初始化器

在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须 实现该初始化器

  required init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
        
    }

3. 类的生命周期

iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:

OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器 码)Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。
// 分析输出AST 
swiftc main.swift -dump-parse 

// 分析并且检查类型输出AST 
swiftc main.swift -dump-ast 

// 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen 
 
// 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil

// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir

// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc

// 生成汇编
swiftc main.swift -emit-assembly

// 编译生成可执行.out文件
swiftc -o main.o main.swift

相对于OC,在Swift的编译过程中多了SIL,SIL会对我们的代码进行安全检查,比如如下代码,在Swift中的编译阶段就会报错。

3.1 SIL文件分析

在项目中添加脚本后运行,就会打开生成的SIL文件。

这里可以看到 @_hasStorage @_hasInitialValue代表着有一个初始化过的储存属性。

@main代表入口函数。

%0: 寄存器,这里是虚拟的,跑到设备上会使用真的寄存器

alloc_global @$s4main1tAA9LGTeacherCvp:分配一个全局变量,该全局变量的名称是混写的,我们可以通过终端指令xcrun swift-demangle将其还原:

%3 = global_addr @$s4main1tAA9LGTeacherCvp:拿到该全局变量的地址给%3寄存器

%4 = metatype $@thick LGTeacher.Type:获取LGTeacher.Type的元类型给%4寄存器

%5 = function_ref @$s4main9LGTeacherCACycfC : %5是LGTeacher.__allocating_init()函数的引用,也就是拿到LGTeacher.__allocating_init()函数的指针地址

%6 = apply %5(%4):使用%5函数并且传入参数%4,把结果返回值(也就是实例变量)给到%6

store %6 to %3:将%6也就是实例变量的内存地址,存放到%3这个全局变量中

Int在Swift底层中t就是一个Struct类型,%8和%9是在构建一个Int32的整数类型0,所以也就是return 0,类似与OC中main函数最终的return 0

3.2 类的初始化流程

可以看到上面的%5调用了LGTeacher.__allocating_init(),LGTeacher.__allocating_init是用来创建实例对象的,所以需要看到函数执行的过程,那么在SIL文件中搜索s4main9LGTeacherCACycfC,看到下面的代码。这个函数需要一个元类型,这里是LGTeacher.Type,现在可以把元类型先理解成isa。

%1 = alloc_ref $LGTeacher ,这里可以看到alloc_ref表示创建一个T的实例对象,并且引用计数初始化为1,所以alloc_ref实际上是去堆区申请内存空间。

同时这里后面写到如果标识为Objc的话,那么这个swift类就会使用OC的初始化方法。这个是什么意思呢?打下断点后打开汇编模式查看。

这里看到调用了LGTeacher.__allocating_init方法

LGTeacher.__allocating_init里面主要调用了swift_allocObject和LGTeacher.init这两个方法。swift_allocObject应该是在堆区找到合适的内存空间,而 init是用来初始化所有的成员变量

那么如果让LGTeacher继承自NSObject。
class LGTeacher:NSObject {
    var age: Int = 18
    var name: String = "asd"

}

那么运行后就可以看到LGTeacher.__allocating_init里面调用的就是OC的初始化方法objc_allocWithZone 和 init 两个方法了。

3.3 swift_allocObject

那么 swift_allocObject 做了些什么呢?那么就需要在swift源码中查看。打开swift源码然后找到HeapObjec.cpp文件,在里面找到了_swift_allocObject_ 方法并且看到这里有三个参数

HdapMetadata const *metadata:元数据类型requiredSize:所需要的大小requiredAlignmentMask:对齐所需要的掩码,可以从objc的源码中得知,因为是8字节对齐所以其值为7,

之后_swift_allocObject_里面调用了swift_slowAlloc方法,并将requiredSize和requiredAlignmentMask作为参数传了进去,然后swift_slowAlloc方法返回了一个HeapObject类型的指针


然后看到swift_slowAlloc里面调用malloc低开辟了内存空间,并且进行了内存对齐。


到此Swift对象进行内存分配的流程就清晰了。

调用_allocating_init(),_allocating_init是由编译器生成的swift类会调用 swift_allocObject()swift_allocObject()调用私有函数 swift_allocObject_()swift_allocObject_()调用malloc来申请堆区的空间并且进行内存8字节对齐 3.4 Swift 对象内存结构

在上面的swift_allocObject_方法中可以看到,swift对象的内存结构是HeapObject,HeapObject包含两个属性,并且默认占用是16字节大小。而OC则是objc_object对象,只有一个isa,占用8字节。
属性:

Metadata: HeapMetadata类型,占用8字节内存大小

RefCounts:是一个64位的位域信息,占用8字节内存大小

HeapMetadata是TargetHeapMetadata这个类型的别名定义,所以接下去需要看TargetHeapMetadata。

看到TargetHeapMetadata继承自TargetMetadata,并且可以看到如果是纯swift类的话,那么就是MetadataKind。如果和objc交互,那么就是isa。其实MetadataKind本质上就是一个isa,为了和oc代码兼容,所以在swift原生里面就是MetadataKind。

源码中 kind 种类


这里还是没有弄清楚数据结构,只知道这里有一个kind。那么看到TargetHeapMetadata的父类TargetMetadata。这里看到了kind的类型是StoredPointer。


那么TargetMetadata是最终的基类,我们知道在objc中类的结构是objc_class,那么TargetMetadata的数据结构是什么样的呢?
往下翻看到一个getTypeContextDescriptor方法,这里根据kind的类型来区分是class,struct还是其他类型。这里显然TargetClassMetadata就是所有类型元类的最终基类。

点进去看TargetClassMetadata


在点击查看TargetAnyClassMetadata,这里就看到Superclass,cacheData,data,alignMask等属性。


这样就得到了swift类的数据结构

struct Metadata {
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer
    var iVarDestroyer: UnsafeRawPointer
}

为了验证,到xcode写实例对象的内存结构。为了便于区别32位,这里将64位的refCounts分为两个32位的refCount。

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refCount1: uint32
    var refCount2: uint32
}

t的变量的内存地址指向的就是HeapObject这样的结构体,这里将t的指针重新绑定为HeapObject这个结构体的类型。

var t = LGTeacher()

let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)
print("end")

运行后得到

那么接下来将meta指针还原为之前得到的结构体。

let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout.stride).pointee

运行后得到

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/996664.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存