Swift学习笔记1——swift类于结构体(上)

285 阅读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/4g 0x00000ff: 读取指定内存中的值(4g:按格式(8字节)输出4个单位的内存地址)

frame variable -L 打印对象的内存布局(带内存地址)

class类型的变量并不直接存储具体的实例对象,是对当前存储具体实例的内存地址的引用。

ctG4UMQ2xA.png

从代码角度理解:

O2tOG85W3p.png

struct类型是值类型,直接存储具体的实例(或者说具体的值)。

image.png 从代码上看:

image.png

内存存储位置的不同

一般情况下,值类型存储在栈上,引用类型存储在堆上。

首先通过下面的图堆内存区域有个基本认识:

image.png 我们重点关注其中堆区和栈区

栈区(Stack):局部变量和函数运行过程中的上下文,内存地址较高

堆区(Heap):存储所有对象,内存地址较低

在函数中创建两个局部变量并打印他们的地址,可以发现类对象的地址较小,结构体对象的地址较大image.png

创建对象耗时的差异

类的内存分配相较于结构体更加复杂,创建对象时耗时也更久。

通过git上的项目可以进行一个简单的性能对比。github.com/knguyen2708…

对象内有1 or 10个成员变量,创建10000000万次耗时。 image.png

初始化器

初始化器会为所有的存储属性设置一个合适的初始值。

默认初始化器

默认初始化器即代码中没有定义init函数时,编译器自动提供的初始化器,需要注意的是,编译器不会为类提供默认的成员初始化器,但会在成员变量都有初始化值时提供不带参数的默认初始化器。 image.png

image.png

结构体内自定义初始化器后,编译器不再提供默认的成员初始化器image.png

指定初始化器

不带修饰符的init就是指定初始化器,每个类至少有一个指定初始化器。下面代码中两个init都是指定初始化器。

class classObjWithInit {
    var age: Int
    var name: String
    // 指定初始化器
    init(age: Int) {
        self.age = age
        self.name = "default"
    }
    // 指定初始化器
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

需要注意的点:

  • 子类必须向上委托父的初始化器。
  • 子类必须先向上委托父类初始化器,然后才能为继承的属性设置新值。
  • 子类在向上委托父类初始化器之前,其所在类引入的所有属性都要初始化完成。
  • 初始化器在所有属性初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。

image.png

类的便捷初始化器

便捷初始化器用convenience修饰,struct对象无法使用便捷初始化器。

class classObjWithInit {
    var age: Int
    var name: String
    // 指定初始化器
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    // 便捷初始化器
    convenience init() {
        self.init(age: 12, name: "test")
    }
}

需要注意的点:

  • 便捷初始化器必须从相同的类里调用另一个初始化器。
  • 便捷初始化器必须先委托同类中其他初始化器,然后再为任意属性赋新值。

image.png

可失败初始化器

即可以初始化失败的初始化器,可在初始化器中使用return nil语句 image.png

必要初始化器

在类的初始化器前添加required修饰符,表示该类的子类都必须实现该初始化器。

image.png

类的创建(内存分配)

我们从源码层面来解析类的创建过成。

Swift编译

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

image.png LLVM IR(LLVM Intermediate Representation),它是一种 low-level languange,是一个像RISC的指令集。 Swift通过Swfit编译器生成IR,然后再生成可执行文件,分步流程如下图。

image.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

SIL文件分析

SIL是Swift和LLVM IR之间的中间语言。我们可以通过SIL文件分析Swift中类的构建过程。

SIL官方链接

生成SIL文件

示例代码

class Father {
    var age: Int = 12
    var name: String = "default"
}

var obj = Father()

main.swift文件目录下执行命令,会生成main.sil文件,打开后分析关键代码。

swiftc main.swift -emit-sil > ./main.sil

SIL文件分析

在分析SIL前补充一些相关知识:

%0 %1 %2:虚拟寄存器

@main: 函数入口

s4main3objAA6FatherCvp:经过加密的文本,可由命令 xcrun swift-demangle [string]还原

@_hasInitialValue:表示这个属性有初始化值

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

@_hasStorage @_hasInitialValue var obj: Father { get set }

// obj
sil_global hidden @$s4main3objAA6FatherCvp : $Father

// main 函数入口
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  // 声明一个全局变量 obj:Father
  alloc_global @$s4main3objAA6FatherCvp           // id: %2
  // 获取全局变量obj的地址
  %3 = global_addr @$s4main3objAA6FatherCvp : $*Father // user: %15
  // 获取Father类的源类型 可以理解为ISA
  %4 = metatype $@thick Father.Type               // user: %14
  // 获取s4main6FatherCACycfC即Father.__allocating_init()的函数指针
  // function_ref Father.__allocating_init()
  %5 = function_ref @$s4main6FatherCACycfC : $@convention(method) (@thick Father.Type) -> @owned Father // user: %6
  // 调用Father.__allocating_init()并传入参数Father的源类型 得到一个实例变量
  %6 = apply %5(%4) : $@convention(method) (@thick Father.Type) -> @owned Father // user: %7
  // 存储实例变量到全局变量obj中
  store %6 to %3 : $*Father                       // id: %7
  // Int是结构体 构建一个Int类型的0
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
  // main函数返回0
  return %9 : $Int32                              // id: %10
} // end sil function 'main'

...

// Father.__allocating_init()
// 需要一个源类型的参数@thick Father.Type
sil hidden [exact_self_class] @$s4main6FatherCACycfC : $@convention(method) (@thick Father.Type) -> @owned Father {
// %0 "$metatype"
bb0(%0 : $@thick Father.Type):
// alloc_ref:申请内存空间 Allocates an object of reference type `T`. The object will be initialized with retain count 1; 
  %1 = alloc_ref $Father                          // user: %3
  // 获取方法 Father.init()的地址
  %2 = function_ref @$s4main6FatherCACycfc : $@convention(method) (@owned Father) -> @owned Father // user: %3
  // 调用Father.init()获得初始化后的变量
  %3 = apply %2(%1) : $@convention(method) (@owned Father) -> @owned Father // user: %4
  return %3 : $Father                             // id: %4
} // end sil function '$s4main6FatherCACycfC'

从上面的SIL文件中可以发现,最为关键的就是__allocating_init()这个函数,其中最为关键的alloc_ref的关键解释如下:

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:).

下面会从汇编的角度进一步解析__allocating_init()以及alloc_ref

汇编分析 对象内存分析

纯Swift类

image.png

  1. 断点,看汇编代码如下,可以看到程序确实会执行__allocating_init()image.png
  2. 进入__allocating_init(),发现程序会执行swift_allocObjectinit()

image.png

继承自NSObject类

image.png 重复上述步骤,从__allocating_init()处断点进入,执行的代码变成了objc_allocWithZone,并通过objc_msgSend发送了init消息。 image.png 通过以上的表现对应了alloc_ref的官方解释,当swift类继承自NSObject时,会使用OC的初始化方法。

The optional objc attribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).

Swift源码分析

接下来我们通过Swift源码分析swift_allocObject函数。

  1. 首先在HeapObject.cpp文件中搜索swift_allocObject, 函数内发送消息调用了_swift_allocObject_函数。'

image.png 2. 找到_swift_allocObject_函数,其主要调用了swift_slowAlloc()函数。 image.png 3. 找到swift_slowAlloc函数,其中通过malloc分配了内存。

image.png

综上所述,Swift对象内存分配可以总结为下面的流程。

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

类的结构

Swift对象的内存结构是HeapObject(OC是 objc_object),在源码中找到对应代码。 image.png 可以发现HeapObject有两个属性:一个是metadata,一个是refCounts。两者加在一起默认占用16字节。 即一个类的实例对象的结构如下:

struct HeapObject {
    var metadata: HeapMetadata      // 8 bytes
    var refCounts: InlineRefCounts  // 8 bytes
};

// 对比OC对象的结构
struct objc_object {
    isa;    // 8bytes
};

HeapMetadata源码分析

  1. 搜索HeapMetadata的定义,发现它是TargetHeapMetadata的别名。 image.png
  2. 查看TargetHeapMetadata,发现它继承自TargetMetadata,同时知道其初始化时可传入MetadataKind(纯Swift类)或ISA(与OC交互的类)类型。 image.png 补充一下MetadataKind的相关信息

查看MedataKind的定义发现是uint32_t类型变量,总结其种类如下。 image.png image.png 3. 查看TargetMetadata,发现其只有一个私有成员Kindimage.png 4. 我们耐心的查看TargetHeapMetadata中的代码,发现下面这段代码,里面讲this转为了TargetClassMetadata类型,说明该类型可能是类的最终结构。 image.png 5. 查看TargetClassMetadata类型,结合官方注释,我们得到以下信息:

  • 该类就是Swift的classmetadata
  • 如果runtime时支持与OC交互,则该类继承自TargetAnyClassMetadataObjCInterop image.png
  1. 继续查看TargetAnyClassMetadataObjCInterop,发现其继承自TargetAnyClassMetadata,自身有两个成员变量:CacheDataData image.png
  2. 查看TargetAnyClassMetadata,发现其继承自TargetHeapMetadata,有一个成员变量:Superclass image.png
  3. 结合5-7中的信息,可以得出结论:

类的实例对象中的metadata变量具体类型为TargetClassMetadata,其数据结构大致如下:

struct Metadata {
    var Kind: StoredPointer
    var SuperClass: TargetClassMetadata*
    var CacheData: (Int, Int)
    var Data: StoredSize
    var Flags: ClassFlags    // uint32_t
    var InstanceAddressPoint: uint32_t
    var InstanceSize:  uint32_t
    var InstanceAlignmentMask: uint16_t
    var Reserved: uint16_t
    var ClassSize: uint32_t
    var ClassAddressPoint: uint32_t
    var Descriptor: TargetClassDescriptor* 
    var IVarDestroyer: ClassIVarDestroyer*
};

通过类型转换验证

验证类的实例对象

我们自定义HeapObject结构体,如果能将类的实例对象强转成自定义结构体,则说明数据结构正确。 image.png 结果证明确实可以强转。

验证Metadata

同理,我们验证TargetClassMetadata的数据结构分析的是否正确。 image.png 成功转换,并且能看到其中一些字段的具体值,比如InstanceSize:40(16+24),也能对照上。