一、类与结构体的异同
-
相同点
- 定义存储值的属性
- 定义方法
- 定义初始化器
- 定义下标,并使用下表语法访问其值
- 使用extension来扩展功能
- 遵循协议来提供某种功能
-
不同点
- 类有继承,而结构体没有
- 类型转换使得您能够在运行时检查和解释类实例的类型
- 类有析构函数来释放其分配的资源
- 类有引用计数记录对一个是咧的引用次数
类是引用类型。意味着,一个类型的变量并不直接存储具体的实例对象,而是存储具体实例对象的内存地址。
// 类的时候
class PSYModel{
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var t = PSYModel.init(age: 18, name: "psy")
var t1 = t
print("end")
可以通过lldb命令:cat address 0x100570af0
查看其实存取的区域,可以发现是在heap(堆)区。
结构体是值类型(值类型还有enum枚举等)。意味着,一个结构体类型的存储是具体的实例值,而不是像引用类型存储的是具体实例对象的内存地址。
// 结构体的时候
struct PSYModel{
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var t = PSYModel.init(age: 18, name: "psy")
var t1 = t
t.name = "俏~"
print("end")
由上面预演的可知,打印t
和t1
都是直接打印出结构体的值,并且,t1
是t
的一个副本,类似于本地化的一份,修改任何一个实例化的值对另一个实例没有影响。
关于内存可以先了解一下 内存管理的五大区 这篇文章,以下主要验证相关的各个内存区域:
栈区(stack)
:局部变量,参数,函数运行时上下文
堆区(heap)
:存储多有对象,由程序员/系统申请并由程序员/系统释放
全局区(global)
:
常量区(data)
:
代码区(text)
结构体类型中如果添加了引用类型的成员变量,则会在堆区申请空间,而原来的结构体实例存储的位置不变,只是引用类型的成员变量区域存储的是指向实例的堆区的内存指针,如下图:
所以在结构体中尽量不要添加引用类型成员变量,因为那样会涉及到堆内存的申请和释放,影响性能。
二、初始化器
1. 执行初始化器&便捷初始化器
类编译器默认不会自动提供成员初始化器,也就是如果类中有两个成员,如果没有初始化,则会报错。但是类中在没有成员的时候,会默认有一个init(){}
初始化器。
结构体默认自动提供初始化器
Swift中创建类的实例时必须为所有的存储属性设置初始值,因为swift会根据类型确定变量的类型或者根据初始值推断变量类型,这就是类型安全。所以类必须提供指定初始化器,也可以根据需要提供便捷初始化器(在初始化前面加上convenience
关键字,便捷初始化器必须从相同的类中调用指定初始化器)。
便捷初始化器定义有一些规则,加上convenience
关键字之后,需要严格控制初始化器的创建 规则:
指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
2. 可失败初始化器
可失败初始化器 : 可以根据业务需要,根据参数d的决定是否初始化失败,也就是return nil
三、类的生命周期
1. Swift的编译流程
iOS开发语言OC和Swift后端都是通过LLVM进行编译最终生成可执行文件;
OC的编译是通过clang编译器,生成LLVM中间IR代码,最后由后端生成可执行文件.o;
Swift是通过Swift编译器编译生成,sil文件,再生成LLVM的可操作性的IR中间代码,然后再生成可执行文件。在这个过程中,区别就是前段编译器不一样,多了一个步骤是生成了sil文件,一下是编译流程图:
其生成步骤可以单步在终端输入特定的指令生成中间的代码,指令集如下:
// 分析输出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
2. Swift特有的(区别于OC)SIL代码
生成SIL文件的时候可以执行输出main.c文件,方便Xcode查看,指令如下:swiftc main.swift -emit-sil -o main.c
SIL的基本语法:SIL官方基本语法
@main: 入口函数, @作为标识符
%0、%1、%2....: 可理解为虚拟的寄存器,类似于日常开发中的常量,一旦赋值不可修改,最后如果跑到设备商会使用真的寄存器
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32位, 4字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
s4main1tAA8PSYModelCvp
这种是混写的,可以使用终端使用命令还原:xcrun swift-demangle s4main1tAA8PSYModelCvp
load: 读取数据
sil_global:标记变量为全局变量
hidden: 标记只针对同一个Swift模块中的对象可见
alloc_global: 开辟全局变量的内存
global_addr: 获取全局变量的地址
ref_element_addr: 获取元素地址
init_existential_addr: 指令会生成 Existential Container 结构, 包裹着实例变量和协议对应的 PWT
destroy_addr
bb0 / bb1 ... : basic block 数字,表示一个代码块,SIL中没有分支语句,只有入口和出口
alloc_ref / dealloc_ref: 开辟/释放内存
function_ref: 获取直接派发函数地址.
class_method: 通过函数表获取方法.
witness_method: 通过 PWT 获取对应的函数地址
objc_method : 获取OC 方法地址
apply:调用函数
store A to B : 把A 的值存储到B中。
begin_access / end_access: 开始、结束访问
[modify] / [read] / [deinit] :修改型访问、读取型访问、删除型访问
[dynamic]:动态访问
[static]:静态访问
retain_value: 引用计数 + 1
release_value: 引用计数 - 1
metatype 获得元类型
@thick 描述元类型代表的形式,是引用 对象类型或是其子类,
@thin 代表一个确切的值 类型,不需要存储,
$ : 类型标识
%: 表示寄存器,类似局部常量,赋值后不可修改。如果再需要新的寄存器,就增加寄存器编号,这样操作有利于编译器的优化;后续进行降级操作 时,才会把这些带编号的虚拟寄存器 转换成对应体系结构的真实寄存器。
@ : SIL中所有标识符均以@符号开头
@main 方法名字是 main
@_hasStorage 标识属性是存储属性
@_hasInitialValue 标识属性有初始值
@owned 代表函数接收者负责销毁返回值
@convention 这个标识用于明确指定当函数调用时参数和返回值应该如何被处理
@convention(c) 表示使用C函数的方式进行调用
@convention(swift) 纯Swift函数的默认调用方式
@convention(method) 柯里化的函数调用方式
@convention(witness_method) 协议方法调用,它等同于convention(method),除了在处理范型类型参数时
@convention(objc_method) Objective-C方式调用
SIL代码以及分析注释如下:
sil_stage canonical
import Builtin
import Swift
import SwiftShims
class PSYModel {
@_hasStorage var age: Int { get set } // 有一个变量age,还有set和get方法
@_hasStorage var name: String { get set } // 有一个变量name,还有set和get方法
init(age: Int, name: String) // 有一个指定初始化器
@objc deinit // 析构函数,@objc标识符
}
// 一个已经初始化的变量t
@_hasStorage @_hasInitialValue var t: PSYModel { get set }
// 一个已经初始化的变量t1
@_hasStorage @_hasInitialValue var t1: PSYModel { get set }
// t
sil_global hidden @$s4main1tAA8PSYModelCvp : $PSYModel
// t1
sil_global hidden @$s4main2t1AA8PSYModelCvp : $PSYModel
// main 入口函数
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
// 分配一个全局变量t, s4main1tAA8PSYModelCvp是混写之后的
alloc_global @$s4main1tAA8PSYModelCvp // id: %2
// 拿到全局变量的地址--->%3
%3 = global_addr @$s4main1tAA8PSYModelCvp : $*PSYModel // users: %15, %18
//
%4 = metatype $@thick PSYModel.Type // user: %14
%5 = integer_literal $Builtin.Int64, 18 // user: %6
%6 = struct $Int (%5 : $Builtin.Int64) // user: %14
%7 = string_literal utf8 "psy" // user: %12
%8 = integer_literal $Builtin.Word, 3 // user: %12
%9 = integer_literal $Builtin.Int1, -1 // user: %12
%10 = metatype $@thin String.Type // user: %12
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
// 函数引用String.init,有三个参数
%11 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %12
// 调用String.init("psy", 3 , -1, String.Type),得到一个字符串
%12 = apply %11(%7, %8, %9, %10) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %14
// function_ref PSYModel.__allocating_init(age:name:)
// 函数引用__allocating_init(age:name:)
%13 = function_ref @$s4main8PSYModelC3age4nameACSi_SStcfC : $@convention(method) (Int, @owned String, @thick PSYModel.Type) -> @owned PSYModel // user: %14
// 实例化一个对象调用指定初始化器,得到PSYModel对象
%14 = apply %13(%6, %12, %4) : $@convention(method) (Int, @owned String, @thick PSYModel.Type) -> @owned PSYModel // user: %15
// 将对象存储到全局变量
store %14 to %3 : $*PSYModel // id: %15
// 在一个全局变量 t1
alloc_global @$s4main2t1AA8PSYModelCvp // id: %16
// 拿到全局变量的内存地址
%17 = global_addr @$s4main2t1AA8PSYModelCvp : $*PSYModel // user: %19
// 开始:动态读取t全局变量内存
%18 = begin_access [read] [dynamic] %3 : $*PSYModel // users: %20, %19
// 地址拷贝存到t1的内存地址
copy_addr %18 to [initialization] %17 : $*PSYModel // id: %19
// 结束:
end_access %18 : $*PSYModel // id: %20
一下分析同理。。。。。。。。。
%21 = integer_literal $Builtin.Word, 1 // user: %23
// function_ref _allocateUninitializedArray(_:)
%22 = function_ref @$ss27_allocateUninitializedArrayySayxG_BptBwlF : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %23
%23 = apply %22(%21) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %25, %24
%24 = tuple_extract %23 : $(Array, Builtin.RawPointer), 0 // users: %43, %40
%25 = tuple_extract %23 : $(Array, Builtin.RawPointer), 1 // user: %26
%26 = pointer_to_address %25 : $Builtin.RawPointer to [strict] $*Any // user: %33
%27 = string_literal utf8 "end" // user: %32
%28 = integer_literal $Builtin.Word, 3 // user: %32
%29 = integer_literal $Builtin.Int1, -1 // user: %32
%30 = metatype $@thin String.Type // user: %32
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%31 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %32
%32 = apply %31(%27, %28, %29, %30) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %34
%33 = init_existential_addr %26 : $*Any, $String // user: %34
store %32 to %33 : $*String // id: %34
// function_ref default argument 1 of print(_:separator:terminator:)
%35 = function_ref @$ss5print_9separator10terminatoryypd_S2StFfA0_ : $@convention(thin) () -> @owned String // user: %36
%36 = apply %35() : $@convention(thin) () -> @owned String // users: %42, %40
// function_ref default argument 2 of print(_:separator:terminator:)
%37 = function_ref @$ss5print_9separator10terminatoryypd_S2StFfA1_ : $@convention(thin) () -> @owned String // user: %38
%38 = apply %37() : $@convention(thin) () -> @owned String // users: %41, %40
// function_ref print(_:separator:terminator:)
%39 = function_ref @$ss5print_9separator10terminatoryypd_S2StF : $@convention(thin) (@guaranteed Array, @guaranteed String, @guaranteed String) -> () // user: %40
%40 = apply %39(%24, %36, %38) : $@convention(thin) (@guaranteed Array, @guaranteed String, @guaranteed String) -> ()
release_value %38 : $String // id: %41
release_value %36 : $String // id: %42
release_value %24 : $Array // id: %43
%44 = integer_literal $Builtin.Int32, 0 // user: %45
%45 = struct $Int32 (%44 : $Builtin.Int32) // user: %46
return %45 : $Int32 // id: %46
} // end sil function 'main'
3. 类的加载流程
3.1 纯Swift代码
在菜单栏:Debug
--> Debug workflow
--> Always show Disassembly
运行源码,断点可看到汇编代码,由于笔者运行的是Mac上,所以只要关注call
指令即可(ARM64关注bl
、b
、br
等指令)。看到下断点到call __allocating_init
的指令出,然后按住control + ⏬箭头
单步进入可看到如下图:
再swift_allocObject
流程的时候,单步进入,里面并没有看到流程了,此时需要借助Swift源码进行分析。
使用VS Code打开 Swift源码 全局搜索swift_allocObject
,或者直接command+p
输入HeapObject
定位到文件,找到swift_allocObject
函数,在其上方有一个_swift_allocObject_
函数:
swift_slowAlloc
函数
3.2继承自NSObject
4.对象与类的内存结构
我们从上面的_swift_allocObject_
中可以看到return
的** object**对象是auto object = reinterpret_cast
这条语句生成的,类型是HeapObject
:
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \ InlineRefCounts refCounts
HeapMetadata
只是一个别名,真正是TargetHeapMetadata
而
TargetHeapMetadata
又继承自TargetMetadata
,这个里面根据MetadataKind
创建TargetHeapMetadata
,并且如果需要Swift与Object-C交互(SWIFT_OBJC_INTEROP
默认为1)还根据TargetAnyClassMetadata
也就是isa
创建。
TargetMetadata
结构体,基本到这里,这就是基类了,但是还是没有看见其成员变量,基本到这就到了瓶颈了。
但是既然kind可以理解为isa,那么基本MetadataKind就是一个重点,在类型的descriptor下,什么情况下是类,然后就发现了这个函数getTypeContextDescriptor
,根据kind来区分是class还是其他,如果是类的时候注意到TargetClassMetadata
kind:
1.class
2.Struct
3.Enum
4.Optional
5.ForeignClass
TargetClassMetadata
函数,以及父类TargetAnyClassMetadata
由此基本上类的数据结构可以展开为:
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
}
5.验证--将结构体绑定成为指针类型
// 实例对象的数据结构
// UnsafeRawPointer 就是一个原生指针
// 有一个64位的refcounted,可以拆分成两个32位的
struct HeapObject{
var metadata: UnsafeRawPointer
var refcounted1: UInt32
var refcounted2: UInt32
}
class PSYModel{
var age: Int = 18
var name: String = "psy"
}
var t = PSYModel()
// 获取实例对象的指针是一个原生指针类型
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
// 绑定内存
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self
, capacity: 1)
print("end")
下断点lldb打印:
// Metadata类似于类内存结构
struct Metadata{
var kind: Int // 可理解成isa
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
}
class PSYModel{
var age: Int = 18
var name: String = "psy"
}
var t = PSYModel()
// 获取实例对象的指针是一个原生指针类型
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
// 绑定内存
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self
, capacity: 1)
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout.stride).pointee
MemoryLayout.stride
print("end")
下断点,lldb输出如下: