Swift:结构体与类(上)

184 阅读9分钟

Swift 中类用class修饰,结构体用struct修饰,

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

结构体和类的相同点不同点

相同点

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

主要的不同点

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

类和结构体最本质的区别

类是引用类型 结构体是值类型

我们可以通过lldb 指令来查看当前变量的内存结构

po:只会输出对应的值

p:返回值的类型以及命令结果 的引用名

x/8g:读取内存中的值(8g: 8字节格式输出)

frame variable -L 打印对象的内存布局

  • 类类型的引用 一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用,如图所示

image.png

image.png

image.png

ct 和 ct1 的地址是相同的,且两个变量引用对象的内存地址完全相同符合引用类型的结果。

  • 结构体是值类型

结构体是一种值类型,内存分配在栈中。复制后新的实例和值类型成员属性的地址空间都是重新分配的,引用类型的成员属性仍然指向之前的地址空间

image.png

image.png

ct 和 ct1 的地址是不相同的,且每个变量的内存地址中存储的属性也是不同的地址,说明两个指向的是两个实例对象

存储区域的不同

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

通过cat address打印地址所在的位置可以得对象在内存中的位置

  • 对于类的内存分配

    1. 在栈上开辟 8 字节内存空间存储 ct 变量,ct 变量中存储 ClassTeacher 的地址

    2. 在堆上,会寻找合适的内存区域,开辟内存,存储 ClassTeacher的实例对象

    3. 函数结束时,会销毁栈上的指针,查找并回收堆上的示例变量。

image.png

  • 对于结构体的内存分配

    在栈上直接分配结构体内存大小的空间存储结构体的值

结构体和类的时间分配

类的内存分配比较繁琐,而且还会有引用计数等操作。 通过 github 上 StructVsClassPerformance 这个案例来直观的测试当前结构体和类的时间分配。结果是结构体速度明显快于类。

初始化器

初始化器是为了可以完整地初始化实例,所以在初始化器中必须完成对存储属性的赋值

默认初始化器

  • 编译器不会自动提供类的初始化器,需要自己提供一个指定初始化器

  • 结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!

指定初始化器

  • 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器,类偏向于少量指定初始化器,一个类通常只有一个指定初始化器

  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成

  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值

类的便利初始化器

便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)

可失败初始化器

当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。 

必要初始化器

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

类的生命周期

Swift 的 编译

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

SIL 文件分析

sil 语法规则

示例代码

class ClassTeacher{
    var age: Int = 10
    var name: String = "XX"
    
}
var t = ClassTeacher()

生成 SIL 文件

这里输入,然后运行

 swiftc -emit-sil  ${SRCROOT}/SwiftTest/main.swift > ./main.sil && open main.sil

class ClassTeacher {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

@_hasStorage @_hasInitialValue var t: ClassTeacher { get set }

// t
sil_global hidden @$s4main1tAA12ClassTeacherCvp : $ClassTeacher

// main 函数入口
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):

// 声明一个全局变量   s4main1tAA12ClassTeacherCvp 也就 t
  alloc_global @$s4main1tAA12ClassTeacherCvp      // id: %2

// 获取全局变量的地址
 
 %3 = global_addr @$s4main1tAA12ClassTeacherCvp : $*ClassTeacher // user: %7

// 获取 ClassTeacher 的源类型
  %4 = metatype $@thick ClassTeacher.Type         // user: %6

// 获取 s4main12ClassTeacherCACycfC  函数的引用 也就是 __allocating_init  指针
// function_ref ClassTeacher.__allocating_init()
  %5 = function_ref @$s4main12ClassTeacherCACycfC : $@convention(method) (@thick ClassTeacher.Type) -> @owned ClassTeacher // user: %6
 
 // 使用  __allocating_init 函数 传入 元类型 得到一个实例对象
  %6 = apply %5(%4) : $@convention(method) (@thick ClassTeacher.Type) -> @owned ClassTeacher // user: %7

 // 存储 实例对象到 全局变量 t
  store %6 to %3 : $*ClassTeacher                 // id: %7

// Int 是结构体 构建一个  Int 类型 的 0
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10

// main函数返回
  return %9 : $Int32                              // id: %10
} // end sil function 'main'

。。。。。。。。

// ClassTeacher.__allocating_init() 
// 需要一个元类型的参数 (@thick ClassTeacher.Type) 暂时理解为 isa
sil hidden [exact_self_class] @$s4main12ClassTeacherCACycfC : $@convention(method) (@thick ClassTeacher.Type) -> @owned ClassTeacher {
// %0 "$metatype"
bb0(%0 : $@thick ClassTeacher.Type):

// 去堆区申请内存空间
//Allocates an object of reference type T. The object will be initialized with retain count 1; 
//its state will be otherwise uninitialized.
//The optional objc attribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).
  
  %1 = alloc_ref $ClassTeacher                    // user: %3

// 调用 init 函数
  //  获取init 函数指针 function_ref ClassTeacher.init()
  %2 = function_ref @$s4main12ClassTeacherCACycfc : $@convention(method) (@owned ClassTeacher) -> @owned ClassTeacher // user: %3
  // 调用 init函数生成变量 
  %3 = apply %2(%1) : $@convention(method) (@owned ClassTeacher) -> @owned ClassTeacher // user: %4
  return %3 : $ClassTeacher                       // id: %4
} // end sil function '$s4main12ClassTeacherCACycfC'

解读SIL

@ 标识符

%0 ,%1,%2 虚拟寄存器 ,复制后不能更改

  • @_hasInitialValue 标识这个类的结构有初始化的存储属性 age,name 和 默认的 init 和 析构函数

  • @main 入口 main 函数

    • @$s4main1tAA12ClassTeacherCvp 混淆的变量名通过xcrun swift-demangle s4main1tAA12ClassTeacherCvp 可以得到变量真是名称

  • 类的实例对象的创建

    • alloc_ref 申请内存 swift 调用 swift_allocObject oc 会调用 allocWithZone

      • 通过 汇编代码 进入到__allocating_init() 可以得到

Allocates an object of reference type T. The object will be initialized with retain count 1;its state will be otherwise uninitialized.The optional objc attribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).

汇编分析 类实例对象创建过程

纯 swift 类

class ClassTeacher{
    var age: Int = 10
    var name: String = "XX"
}
var t = ClassTeacher()
  • 断点在 __allocating_init 函数出

  • lldb 指令 si 进入 __allocating_init 函数内部可以得出

    • __allocating_init 内部会调用 swift_allocObject 和 init 函数

继承 NSObject 的 类

class ClassPerson: NSObject{
    var age: Int = 10
    var name: String = "XX"
    
}
var t1  = ClassPerson()
  • 进入 __allocating_init 后可以看出 内部调用了 allocWithZone 方法然后通过 msgsend 发送 init 消息

Swfit 源码 分析

Swift源码

搜索 HeapObject.cpp 文件 找到 swift_allocObject 函数 可以看到 内部调用了 swift_slowAlloc 函数

进入swift_slowAlloc 函数 可以看到 调用了 malloc

由此可以得出 swift对象创建时经过如下的过程分配内存的

__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc

Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是 Metadata ,一个是 RefCount ,默认占用 16 字节大小

类的结构窥探

在OC 中类的只有一个 isa 指针,但是在 swift 中有 HeapMetadata 和 refCounts 两个属性

Metadata 源码分析

Metadata 的继承关系及结构简述

查看 Metadata 的源码可以看到 Metadata 是 HeapMetadata 类型 这是 TargetHeapMetadata 定义的别名

查看 TargetHeapMetadata 可得它继承自 TargetMetadata,同时也可以知道 其初始化时如果是 纯swift类则传入 MetadataKind kind 如果是和 OC 交互的类传入 isa

TargetMetadata 中可得到一个 kind 的成员变量 

MetadataKind 的解析

查看 MetadataKind 可得它是一个 uint32_t的类型

在 swift 中  MetadataKind有如下类型

类的结构分析

源码分析猜想

上面说到了 Metadata 最终继承自  TargetMetadata,且其成员中的 kind 对应多种类型,所以猜测 Metadata 最终会在这里完成创建 通过源码分析可以看到下面的这个函数

可以看出 这里会根据 kind 的种类 将TargetMetadata 转换为其他类型的 Metadata ,所以 TargetMetadata 可能就是所有类型的元类型的基类

当 kind 的 是Class 时会将 this 强转为 TargetClassMetadata 类型 这个类型可能就是类的最终结构,所以分析它的结构就应该可以得到类的大致结构

 case MetadataKind::Class: {
      const auto cls = static_cast<const TargetClassMetadata<Runtime> *>(this);

进入 TargetClassMetadata 其继承自TargetAnyClassMetadata

最终通过对对源码的追踪可以发现类的结构由如下继承关系 TargetClassMetadata : TargetAnyClassMetadata :TargetHeapMetadata : TargetMetadata

那么 对于类的最终结构 就是这些结构体所有的成员的集合 经过以上分析 可以大致得出 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
}

强转结构验证

上面分析得到类的实例对象结构是 HeapObject 有两个属性: 一个是 Metadata ,一个是 RefCount

所以我们自定义一个结构体用来将我们的实例对象强转为该类型,如果转换成功则说明分析正确

结果说明上面分析 类的实例对象的结构为  HeapObject 没毛病

那么对于 metadata 结构分析正确与否,只需验证 metadata 是否可以转换成上述分析的 Metadata 类型即可

结果很明显 上述分析的结构 就是类的数据结构