Swift中的类与结构体

670 阅读5分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

为了在iOS开发行业“一条路走到黑”,不得不拾起荒废了很久的Swift,做一个完整的iOS developer

Swift中的类与结构体

//类
class RectClass {
    var height = 0.0
    var width = 0.0
}
//结构体
struct RectStruct {
    var height = 0.0
    var width = 0.0
}

var rectCls = RectClass()
var rectStrct = RectStruct()

类和结构体的区别

首先我们大家都知道在Swift中类和结构体的基本区别,如下

共同点:
  1. 能定义存储值的属性

  2. 能定义方法

  3. 能定义下标以使用下标语法提供对其值得访问

  4. 能定义初始化器

  5. 能够使用extension来拓展功能

  6. 能遵循协议来提供某种功能

不同点:
  1. 类有继承属性,而结构体没有
  2. 类型转换使你能够在运行时检查和解释类的实例类型
  3. 类有析构函数用来释放其分配的资源
  4. 引用计数允许对一个类实例有多个引用

但是事实并没有那么简单,因为在Swift中,Struct、Class和enum在一般的使用中能够做到互相替换,那么在实际开发中是选择使用结构体还是类?因此探究其背后的逻辑就十分必要,而这一问题就引出了Swift中的值类型和引用类型的区别

由于在Swift中的struct为值类型,class为引用类型,因此我们以这两种类型为代表来具体阐述,那么如何论证呢?

  1. 在Swift中,典型的有struct、enum、以及tuple(元祖)都是值类型,而平时使用的IntDoubleFloatStringArrayDictionarySet 其实都是用结构体实现的,也是值类型。

  2. 在Swift中,class和闭包是引用类型。

stack & heap

内存(RAM)中有两个区域,栈区(stack)和堆区(heap)。在Swift中,值类型的变量存放在堆区,引用类型的变量存放在栈区

内存基本分布图

Tcd9xI.md.png

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

Heap: 存储所有对象

Global: 存储全局变量;常量;代码区

在lldb调试环境下,使用以下命令,可以查看变量在内存中的分布示意图

frame varibale -L xxx

引用类型(reference type)

引用类型,即所有实例变量共享一份数据拷贝,也就意味着⼀个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引⽤。

T6oGQ0.png

见代码:

class LPTeacher {
		var age = 0
		var name = ""
}

 var t = LPTeacher()
 var t2 = t
 t.age = 27
 print("t.age -> \(t.age)")
 print("t2.age -> \(t2.age)")

//t.age -> 27
//t2.age -> 27

发现1:

会发现新对象和源对象的变量名虽然不同,当使用新对象操作其内部数据时,源对象的内部数据也会受到影响

接下来我们需要用到以下两个lldb调试指令来查看当前变量的内存结构

po:p和po的区别在于使用po只会输出对应的值,而p则会返回值的类型以及命令结果的引用名

x/8g:读取内存中的值

Unmanaged.passRetained(t).toOpaque():获取引用类型变量的值

withUnsafePointer(to: &t){print($0)}:获取变量的地址

如图,分别po变量t和变量t2,得到变量的值

T6LKoT.md.png

在Swift中,我们也可以用以下方法获取变量指向的内存地址以及变量地址,如以下代码

//打印引用类型变量的值
 print(Unmanaged.passRetained(t).toOpaque())
 print(Unmanaged.passRetained(t2).toOpaque())
//0x0000600003db4000
//0x0000600003db4000
//会发现同样是一个8字节的内存地址
发现2

会发现是二者的值是相同的一个8字节的地址。说明二者指向的内存地址是相同的

使用命令frame varibale -L xxx,得到以下图

Tc0s8H.md.png

发现3

会发现引用类型变量的引用是指向堆上的

引用类型总结

在Swift中,引用类型的赋值是浅拷贝(Shallow Copy),引用的语义(Reference Semantics)即新对象和源对象的变量名不同,但其引用(指向的内存地址)是一样的,因此当使用新对象操作其内部数据时,源对象的内部数据也会受到影响。并且引用类型的变量的值是存放在堆上

值类型(value type)

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

TcDPfg.md.png

见代码

struct LPTeacher {
		var age = 0
		var name = ""
}

 var t = LPTeacher()
 var t2 = t
 t.age = 27
 print("t.age -> \(t.age)")
 print("t2.age -> \(t2.age)")

//t.age -> 27
//t2.age -> 0

//打印变量的地址
print(withUnsafePointer(to: &t){print($0)})
print(withUnsafePointer(to: &t2){print($0)})

//0x00007ffeed7a4fc0
//0x00007ffeed7a4fa0
发现1

可以发现,新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响。打印值类型变量的内存地址,这样就能看出两个变量的内存地址并不相同。

如图,分别po变量t和变量t2

TcRrwj.png

发现2

可以发现两个变量存放的值,这和引用类型是存放的是地址不同

使用命令frame varibale -L xxx,得到以下图

TcWhCt.md.png

发现3

可以发现变量的值存放在栈上的且为值的首地址,和引用类型变量的值存放在堆上不同

值类型总结

Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。

值类型和引用类型的嵌套

在实际使用中,其实值类型和引用类型并不是孤立的,有时值类型里会存在引用类型的变量,反之亦然。这里简要介绍这四种嵌套类型。

值类型嵌套值类型

值类型嵌套值类型时,赋值时创建了新的变量,两者是独立的,嵌套的值类型变量也会创建新的变量,这两者也是独立的。

值类型嵌套引用类型

值类型嵌套引用类型时,赋值时创建了新的变量,两者是独立的,但嵌套的引用类型指向的是同一块内存空间,当改变值类型内部嵌套的引用类型变量值时(除了重新初始化),其他对象的该属性也会随之改变。

引用类型嵌套值类型

引用类型嵌套值类型时,赋值时创建了新的变量,但是新变量和源变量指向同一块内存,因此改变源变量的内部值,会影响到其他变量的值。

引用类型嵌套引用类型

引用类型嵌套引用类型时,赋值时创建了新的变量,但是新变量和源变量指向同一块内存,内部引用类型变量也指向同一块内存地址,改变引用类型嵌套的引用类型的值,也会影响到其他变量的值。

类和结构体如何选择?

类类型的实例变量内存分配,先在栈区分配一个地址的空间大小(即8个字节),然后再堆区寻找合适内存,并返回堆的地址让栈区指向,同理,销毁的时候也会先在堆区操作,因此相对于值类型只在栈上进行操作,要更耗费时间

由于类和结构体在内存中的分布不同,所以导致二者在使用过程中速度和时间也会有所差异,从这个角度出发,对二者的选择则更容易确定

这⾥我们也可以通过github上StructVsClassPerformance这个案例来直观的测试当前结 构体和类的时间分配。

也可以通过两个官方案例

enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [String : UIImage]()
func makeBalloon(_ balloon: Ballon) -> UIImage {
	if let image = cache[balloon] {
		return image
	}
...
}
struct Balloon: Hashable{
	var color: Color
	var orientation: Orientation
	var tail: Tail
}
struct Attachment {
let fileURL: URL
let uuid: UUID
let mineType: MimeType
init?(fileURL: URL, uuid: String, mimeType: String) {
guard mineType.isMineType
else { return nil }
self.fileURL = fileURL
self.uuid = uuid
self.mineType = mimeType
} }
enum MimeType: String{
case jpeg = "image/jpeg"
....
}
结论

因为结构体分配在内存栈区,所以无论是分配时间,还是销毁时间都要优于类,所以一般优先考虑结构体,

类的初始化器

类成员必须有初始值

因为Swift是类型安全的语言,所以类和结构体在实例化的时候必须给所有存储属性一个合适的初始值。

class LPTeacher{
    var age : Int = 0
    var name : String = ""
   //没有初始化器,默认提供这个,但也必须给成员提供初始值
//    init(){
//
//    }
}
var t = LPTeacher()
//--------------------华丽的分割线----------------------
class LPTeacher{
    var age : Int = 0
    var name : String = ""
    init(_ age:Int , _ name:String){
        self.age = age
        self.name = name
    }
}
 var t = LPTeacher(27, "LP")

当前的类编译器默认不会⾃动提供成员初始化器,但是对于结构体来说编译 器会提供默认的初始化⽅法(前提是我们⾃⼰没有指定初始化器)!

struct LPStudent{
    var age : Int
    var name : String
}
var st = LPStudent(age: 27, name: "LP")
类中可以提供多个初始化器
class LPTeacher{
    var age : Int = 0
    var name : String = ""
    init(_ age:Int , _ name:String){
        self.age = age
        self.name = name
    }
    init(_ age:Int){
        self.age = age
        self.name = ""
    }
    init (_ name:String){
        self.age = 0
        self.name = name
    }
}

var t = LPTeacher(27, "LP")
var t2 = LPTeacher(27)
var t3 = LPTeacher("LP")


便捷初始化器

类 LGPerson 必须要提供对应的指定初始化器(没提供,则默认初始化值),同时我们也可以为当前的类提供便捷初始化器(注意:便捷初始化器必须从相同的类⾥调⽤另⼀个初始化器。)

class LPTeacher{
    var age : Int = 0
    var name : String = ""
    init(_ age:Int , _ name:String){
        self.age = age
        self.name = name
    }
    convenience init() {
        self.init(27, "LP")
    }
}

var t2 = LPTeacher()
总结
  • 指定初始化器必须保证在向上委托给⽗类初始化器之前,其所在类引⼊的所有属性都要初始化完成。
  • 指定初始化器必须先向上委托⽗类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被⽗类中的初始化器所覆盖
  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类⾥定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被⾃⼰类中其它指定初始化器所覆盖。
  • 初始化器在第⼀阶段初始化完成之前,不能调⽤任何实例⽅法、不能读取任何实例属性的值,也不能引⽤ self 作为值。
可失败初始化器

当前因为参数的不合法或者外部条件的不满⾜,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句,来表明可失败初始化器在何种情况下会触发初始化失败。

class LPPersion {
    var age : Int
    var name : String
    init?(_ age : Int , _ name : String) {
        if age < 18 {return nil}
        self.age = age
        self.name = name
    }
}
必要初始化器
class LPPersion {
    var age : Int
    var name : String
    required init(_ age : Int , _ name : String) {
        self.age = age
        self.name = name
    }

}
class LPStudeng: LPPersion {
    var classV : String
    var sAge : Int
    init(_ classV : String , _ sAge : Int) {
        self.classV = classV
        self.sAge = sAge
        super.init(20, "")
    }
    required init(_ age: Int, _ name: String) {//必须实现
        fatalError("init(_:_:) has not been implemented")
    }
}

类的生命周期

编译部分

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

T2TxYD.md.png

只是二者的编译器不同,我们大概知道OC 通过 clang 编译器,编译成 IR,然后⽣成可执⾏⽂件 .o(这⾥也就是我们的机器码)

而Swift稍微有点复杂,是通过 Swift 编译器编译成 IR,然后在⽣成可执⾏⽂件。流程如下:

T27lmq.png

其中编译命令行:

// 分析输出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

Swift 中间语言

其中,**Swift Intermediate Language (SIL)**中间语言,是我们分析的主要地方

首先,Swift是一门默认内存安全的语言

int8_t x = 100;
int8_t y = x + 100;
NSLog(@"%d",y);

//输出:-56

typedef signed char           int8_t;
//可以看出是1字节的char类型,其表达范围是-128~127,因此内存溢出,但并不会报错
let x = Int8(100)+100
//Wrong:'100+100'(on type 'Int8')result in an overflow
//内存溢出,直接报错

由于oc是运行时语言,编译期间并不会去检查类型,而swift是类型安全语言,且由于中间语言SIL的存在,所以在编译期间会检查类型,保障内存安全

在main文件中声明一个LPTeacher类型的全局变量

import Foundation

class LPTeacher {
    var age = 0
    var name = "LP"
}

var t = LPTeacher()

我们通过在xcode添加脚本,编译得到优化的**.sil文件并用VSCode**编辑器打开它

T2Xpp8.md.png

首先可以看到LPTeacher的声明,里面包含两个属性和析构函数以及默认初始化器(因为没有指定初始化器)

TR0XNV.md.png

接下来我们来看@main,函数入口,**@**sil语法中的标识符

其中**%0表示寄存器,当然这是虚拟的,理解为常量**,一旦赋值之后不能更改

s4main1tAA9LPTeacherCvp:混淆名称,用xcrun swift-demangle 可以还原出来

TRsANq.png

main.sil文件,Swift sil语法文档

TRyVZd.png

分析Swift中间语言在整个main以及声明一个全局变量的过程

  1. 分配一个全局变量(alloc_global)

  2. 获取这个全局变量的地址并赋值给寄存器%3(global_addr)

  3. 获取LPTeacher这个类的元类型(metatype)并赋值给寄存器4

  4. //function_ref LPTeacher.__allocating_init() ,表示获取到这个函数的地址并赋值给寄存器5

  5. apply,向函数指针传入刚获取的元类型参数并返回一个实例对象给寄存器6

  6. store 将寄存器6中的实例对象的地址存储到刚才获取的全局变量的地址中

  7. 最后三行代码,构建了一个Int32位的整数类型,其值为0,和oc中的return 0是一样的

通过分析类实例化的流程,我们发现一个第步骤4到步骤6的时候实例化了对象,其中关键函数 **LPTeacher.__allocating_init()**起了决定性作用,那么它具体做了什么呢?

全局搜索得到

TR2E1P.md.png

  1. (@thick LPteacher.type)该函数需要这样一个元类型,可以理解为isa
  2. alloc_ref $LPTeacher

TR4nFe.md.png

  1. alloc_ref $LPTeacher (sil语法,swift代码编译而来):alloc_ref分配一个引用类型为T的对象。该对象将用retain count 1初始化;否则其状态将未初始化。可选的objc属性表示应该使用Objective-C的分配方法(+allocWithZone:)分配对象。

从swift源码,断点看汇编进行分析会发现

第一句话:如果是纯swift类,则会用Swift_allocObject(swift源码的函数,更高一层)创建一个实例对象,并用swift_init去初始化实例成员变量,同时引用计数+1,即会去堆区申请一块内存空间

第二句话:如果是继承的NSObject类,则和oc相同,采用allocWithZone申请内存,并用objc_msgSend机制调用“init”

  1. 首先,通过断点知道先调用__allocating_init()

    TRXnxO.md.png

    断点继续往里走调用swift_allocObject

    TRXron.md.png

    同样,继续探究Swift_allocObject做了什么。通过查看swift resource中的 HeapObject.cpp文件

    可以知道直接调用了**_swift_allocObject_**

    TRXzTA.md.png

    分析_swift_allocObject_函数,发现有三个参数

    HeapMetadata:稍后解释

    size_t requireSize:所需要的大小

    size_t requireAlignmentMask:所需要对齐的源码(应该为7字节)

    里面调用swift_slowAlloc函数,并返回了一个HeapObject类型的值

    接下来看swift_slowAlloc,会发现最终还是调用了malloc来申请内存空间

    TRxSRP.md.png

总结

不用开发者调用,在进行对象实例化的时候编译器会默认自动生成 allocating_init -----> swift_allocObject -----> swift_allocObject ----->swift_slowAlloc -----> Malloc

Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: ⼀个是 Metadata,⼀个是 RefCount ,默认占⽤ 16 字节⼤⼩。

整体思路:首先知道有中间语言这个东西,swift 原生代码 --->中间语言---->得到allocating_init()关键函数---->该函数实现中---->alloc_ref()函数---->通过官方文档并用汇编证明-----该函数是由allocating_init函数到swift_allocObject函数而来。然后通过查看官方提供的swift源码得到类实例化的具体流程,并知道对象的基本内存结构

分析HeapMetadata
HeapMetadata constant *metadata
using HeapMetadata = TargetHeapMetadata<InProcess>;
-------
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};
-------
struct TargetMetadata {
  using StoredPointer = typename Runtime::StoredPointer;

  /// The basic header type.
  typedef TargetTypeMetadataHeader<Runtime> HeaderType;

  constexpr TargetMetadata()
    : Kind(static_cast<StoredPointer>(MetadataKind::Class)) {}
  constexpr TargetMetadata(MetadataKind Kind)
    : Kind(static_cast<StoredPointer>(Kind)) {}

#if SWIFT_OBJC_INTEROP
protected:
  constexpr TargetMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : Kind(reinterpret_cast<StoredPointer>(isa)) {}
#endif

private:
  /// The kind. Only valid for non-class metadata; getKind() must be used to get
  /// the kind value.
  StoredPointer Kind;
public:
  /// Get the metadata kind.
  MetadataKind getKind() const {
    return getEnumeratedMetadataKind(Kind);
  }
  
  --------------
  //发现一个 StoredPointer Kind;得知kind类型。基类
  //由OC类的结构:objc_class思考出swift类也应该有类似结构
 
  ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor>
  getTypeContextDescriptor() const {
    switch (getKind()) {
    case MetadataKind::Class: {//由此可以看到TargetClassMetadata最终类
      const auto cls = static_cast<const TargetClassMetadata<Runtime> *>(this);
      if (!cls->isTypeMetadata())
        return nullptr;
      if (cls->isArtificialSubclass())
        return nullptr;
      return cls->getDescription();
    }
    case MetadataKind::Struct:
    case MetadataKind::Enum:
    case MetadataKind::Optional:
      return static_cast<const TargetValueMetadata<Runtime> *>(this)
          ->Description;
    case MetadataKind::ForeignClass:
      return static_cast<const TargetForeignClassMetadata<Runtime> *>(this)
          ->Description;
    default:
      return nullptr;
    }
  }
 

最终找到TargetClassMetadata

TWPRln.png

分析源码得出:

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
}

MetadataKind 的类型

TWp7Yd.png