Swift 底层_类与结构体(上)

347 阅读6分钟

一、类和结构体的对比

结构体和类的主要相同点有:
  1. 定义存储值的属性
  2. 定义方法
  3. 定义下标以使用下标语法提供对其值的访问
  4. 定义初始化器
  5. 使用 extension 来拓展功能
  6. 遵循协议来提供某种功能
主要的不同点有:
  1. 类有继承的特性,而结构体没有
  2. 类型转换使您能够在运行时检查和解释类实例的类型
  3. 类有析构函数用来释放其分配的资源
  4. 引用计数允许对一个类实例有多个引用
对于类与结构体,重点区分:
  • 类是引用类型 : 不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用。

  • 结构体是值类型 : 相比较类类型的变量中存储的是地址,那么值类型存储的就是具体的实例

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

*可以借助lldb的po和x/8g指令来查看当前的变量的内存结构

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

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

关于内存区域基本概念的认知图:

WX20211228-085404@2x.png

  • 栈区(Stack):存储局部变量和运行过程中的上下文,内存分配是从高地址到低地址
  • 堆区(Heap):存储所有的对象,内存分配是从低地址到高地址
  • 全局区(Global):存储全局变量和静态变量,内存分配是从低地址到高地址
  • 常量区:存储常量,内存分配是从低地址到高地址
  • 代码区(_text):存放编译后的代码
以下通过代码说明内存结构的不同:

WX20211228-084441@2x.png 类 存放相同内存地址。

WX20211228-085220@2x.png

结构体 存放具体实例。

可以通过工具来查看当前地址是在堆还是栈上。

image.png

如图所示,age变量是在栈区。

  • Heap: 存储所有对象
  • Global: 存储全局变量;常量;代码区

如代码:

int a = 10;
int age;

int main(int argc, const char* argv[]) {
    @autoreleasepool {
        NSLog(@"1");
    }
    return 0;
}

如下图,通过断点调试,可以发现,a和age分别代表初始化过的和没有初始化过的符号声明。

WX20211228-211251@2x.png

Segment & Section: Mach-O 文件有多个段( Segment ),每段分不同的Section

  • TEXT.text : 机器码
  • TEXT.cstring : 硬编码的字符串
  • TEXT.const: 初始化过的常量
  • DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量
  • DATA.bss: 没有初始化的(静态/全局)变量
  • DATA.common: 没有初始化过的符号声明
类与结构体的内存分布

通过frame variable -L指令来查看内存分布情况

WX20211228-212211.png

可以看出结构体是存在栈区的
总结:结合以上两点,可以看出结构体属于值类型
*分布示意图

6520f32850a04d2ea1a996fb0532e4a8.png

类与结构体混用时内存分布

  • 结构体内存地址在栈区
  • 结构体中的的内存地址在堆区再分配内存的过程中,会有时间速度的区别。我们可以通过GitHub上的StructVsClassPerformance工程来分析;

在选择数据结构类型时,尽可能优先选用结构体结构体上,是线程安全的;在内存分配上,栈区也要比堆区快;

二、类的初始化

class LeeTeacher {
    var age :Int
    var name : String
    init(_ age:Int, _name : String) {
    self.age = age;
    self.name = name;
}

struct LeeTeacher {
    var age:int
    var name :String
}

当前类编译器默认不会提供成员初始化器,结构体会提供默认的初始化方法。

便捷初始化器:

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


var age: Int
var name: String

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

convenience init() {
//便捷初始化器必须要调用同类的另一个初始化器
    self.init(age: 18, name:"Lee")
}

需要注意点:

  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其 它指定初始化器所覆盖。
  • 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例 属性的值,也不能引用 self 作为值。

可失败初始化器,这是为了满足,提供的参数可能存在初始化失败的情况下,来使用可失败初始化器:

    var age: Int
    var name: String
    init?(age: Int, name: String) {
        if age < 18 { return nil }
        self.age = age
        self.name = name
    }

    convenience init?() {
        self.init(age: 20, name: "Lee")
    }
}

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

三.类的生命周期

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

image.png OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码) Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。

image.png

SIL (Swift Intermediate Language) swift中间语言
// 分析输出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文件分析

通过添加脚本,运行会在vscode打开main.sil文件:

swiftc -emit-sil ${SRCROOT}/文件夹路径/main.swift > ./main.sil && open main.sil

WX20211228-230513@2x.png

其中sil语法规则参考:github.com/apple/swift…

  • @main:表示入口函数、
  • %0、%1、等等: 寄存器,可以理解为日常开发中的常量,一旦赋值之后不可更改。虚拟寄存器。
  • alloc_global:分配一个全局变量,该全局变量的名称是混写的,我们可以通过终端指令还原xcrun swift-demangle
  • %5 = function_ref :函数引用,拿到function_refLGTeacher. __allocating_init() 函数的指针地址
  • %6 = apply %5(%4) : $@convention(method): 把结果值给%6
  • store %6 to %3 : $*LGTeacher : 把当前的%6 存储到 %3
Swift源码分析

Swift源码下载地址:github.com/apple/swift
分析流程大致如下表示:
->搜索 HeapObject.cpp 文件,找到 swift_allocObject 函数,其有三个参数:

  • HdapMetadata const *metadata:元数据类型
  • requiredSize:所需要的大小
  • requiredAlignmentMask:值为7, 因为8字节对齐 。对齐所需要的掩码。

-> swift_allocObject 中调用了 _swift_allocObject_ 
-> _swift_allocObject_ 中又调用了 swift_slowAlloc
-> 可以看到 Swift 中也调用了 malloc 函数。

由此可得出:

Swift 对象内存分配:
  • __allocating_init -> swift_allocObject -> swift_allocObject -> swift_slowAlloc -> Malloc
  • Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是Metadata ,一个是 RefCount ,默认占用 16 字节大小。而OC则是objc_object对象,只有一个isa,占用8字节。

MetadataKind 源码分析

源码查看 MetadataKind 可以看到它是一个 uint32_t 类型

enum class MetadataKind : uint32_t {
#define METADATAKIND(name, value) name = value,
#define ABSTRACTMETADATAKIND(name, start, end)                                 \
  name##_Start = start, name##_End = end,
...

和 Swift 中的类型关联表如下 taKind的定义:

namevalue
Class0x0
Struct0x200
Enum0x201
Optional0x202
ForeignClass0x203
Opaque0x300
Tuple0x301
Function0x302
Existential0x303
Metatype0x304
ObjCClassWrapper0x305
ExistentialMetatype0x306
HeapLocalVariable0x400
HeapGenericLocalVariable0x500
ErrorObject0x501
LastEnumerated0x7FF

查看 TargetHeapMetadata 的父类 TargetMetadata:
kind的类型是StoredPointer

image.png

继续向下寻找: 通过kind类型,区分class、struct、enum,TargetClassMetadata就是所有类型元类的最终基类。

image.png

继续查看TargetClassMetadata,查看内存结构:

image.png

可以分析出来swift类的数据结构:

    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
}