类与结构体(-)
一、 初识类与结构体
我们先看一段对比的代码:
class MJYPerson {
var age = 19
var name = "卿卿"
init(_ age: Int,_ name: String ) {
self.age = age
self.name = name
}
}
struct MJYTeache {
var age = 18
var name = "卿卿1"
init(_ age: Int,_ name: String) {
self.age = age
self.name = name
}
}
结构体和类的主要相同点有:
- 都能定义存储值得属性
- 都能定义方法
- 都能定义下表以使用下表语法对其值得访问
- 都能定义初始化器
- 使用extension来扩展功能
- 遵循某种协议来提供某种功能
结构体和类的主要相同点有:
- 类有类继承的特性,而结构体没有
- 类型转换使类在运行时检查和解释实例的类型
- 类有析构函数用来释放其分配的内存资源
- 类中的引用计数允许对一个类实例有多个引用
对于类和结构体我们需要区分的第一件事就是:
类是引用类型,也就意味着一个类的类型变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用。
通过以下代码我们能更清晰的认识到类与结构体在存储上的不同
这里我们借助两个指令来查看当前变量的内存结构
po : p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果
的引用名。
123 x/8g: 读取内存中的值(8g: 8字节格式输出)
class MJYPerson {
var age = 19
var name = "卿卿"
init(_ age: Int,_ name: String ) {
self.age = age
self.name = name
}
}
var person1 = MJYPerson(18, "卿卿1")
var person2 = person1
person1.age = 19
(lldb) po person1
<MJYPerson: 0x1006797e0>
(lldb) po person2
<MJYPerson: 0x1006797e0>
(lldb) p person1
(结构体.MJYPerson) $R8 = 0x00000001006797e0 (age = 19, name = "卿卿1")
(lldb) p person2
(结构体.MJYPerson) $R10 = 0x00000001006797e0 (age = 19, name = "卿卿1")
(lldb)
以上代码可以看出 person1和person2存储着相同的地址,并且当person1得值改变的时候,person2的值也对应着改变了。
接下来我们看一下结构体:
struct MJYTeache {
var age = 18
var name = "卿卿1"
init(_ age: Int,_ name: String) {
self.age = age
self.name = name
}
}
var teacher1 = MJYTeache(18, "卿卿1")
var teacher2 = teacher1
teacher1.age = 19
(lldb) po teacher1
▿ MJYTeache
- age : 19
- name : "卿卿1"
(lldb) po teacher2
▿ MJYTeache
- age : 18
- name : "卿卿1"
(lldb)
通过以上代码打印teacher1和teacher2,可以看出直接打印的是值,并且teacher1和teacher2的值改变的时候互不影响
【其实引用类型就相当于我们每个人都对一个仓库拥有钥匙,别人只要访问就能看到里面的存储,而修改更是对这个仓库的货物进行更改,无论是谁只要拥有钥匙都能查看到这个仓库现在当前的货物状态。
值类型就相当于别人是看我们加仓库货物不错,仿照着建了个一模一样的仓库,大家拥有的仓库不是同一个钥匙锁自然不是同一把锁,大家都是对自己仓库进行修改,数据并不共通。】
【另外引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在 栈上,引用类型存储在堆上。 】
二 、类的初始化器
类编译器默认不会自动提供成员初始化器(init方法),但结构体会。
class MJYPerson {
var age: Int
var name: String
}
struct MJYTeache {
var age: Int
var name: String
}
var person1 = MJYPerson()
var teacher1 = MJYTeache(age: 18, name: "卿卿1")
提供以上代码,大家可以尝试一下
如以上代码,如果类中的成员没有给定具体的值,且没有提供初始化器,类会报错,但是对于结构体来说编译 器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!
class MJYPerson{
var age: Int
var name: String
init(_ age: Int, _ name: String)
{
self.age = age
self.name = name
}
convenience init() {
self.init(18, "卿卿")
}
}
Swift 中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。所以类 MJYPerson 必须要提供对应的指定初始化器,同时我们也可以为当前的类提供便捷初始化器(注意:便捷初始化器必须从相同的类里调用另一个初始化器。)
如果我们在上一份代码的的基础上增加一个子类MJYTeacher
class MJYTeacher: MJYPerson{
var subjectName:String
init(subjectName: String){
self.subjectName = subjectName
super.init(19,"卿卿2")
}
}
你会发现必须将subjectName先初始化,再调用父类的代码才不会报错,先调用父类的初始化器再给自己的的属性赋值,代码照样会报错。
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。
- 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
- 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
class MJYPerson{
var age: Int
var name: String
init?(_ age: Int, _ name: String) {
if age < 18 {return nil}
self.age = age
self.name = name
}
convenience init?(age: Int, name: String) {
if age < 18 {return nil}//比如这里我们定义了小于18就不是一个合法的成人
self.init(age, name)
}
}
var t = MJYPerson(17,"卿卿")
-----------------------------------------------------------------------------------
(lldb) po t
nil
可失败初始化器: 这个也非常好理解,也就意味着当前因为参数的不合法或者外部条件 的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句,来表明可失败初始化器在何种情况下会触发初始化失败。写法也非常简单
class MJYPerson{
var age: Int
var name: String
init(_ age: Int, _ name: String)
{
self.age = age
self.name = name
}
required convenience init() {
self.init(18, "卿卿")
}
}
class MJYTeacher: MJYPerson{
var subjectName:String
init(subjectName: String){
self.subjectName = subjectName
super.init(19,"卿卿2")
}
convenience override init(_ age: Int, _ name: String) {//父类该方法标记了require,如果MJYTeacher没有提供该方法,将会报错
self.init(subjectName: "卿卿2")
}
}
必要初始化器: 在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须 实现该初始化器
三、类的生命周期
iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:
OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码)
Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。
- swift code 经过-dump-parse的命令来进行语法分析,解析成我们的AST(也就是抽象语法树);
- 通过-dump-ast语义分析来检查我们的语义,(例如类型检查是否准确安全);
- 完成之后Swift
代码将会降级为SIL,也就是Swift中间语言/代码(Swift intermediate language); - SIL
分为Raw SIL (原生的,没有开启优化选项) 和SILOpt Canonical SIL(经过优化的); - 最后通过
LLVM降级为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
相对于OC出现了比较显眼的SIL也就是swift的中间语言,这个中间语言到底有什么作用呢!
在OC代码中,以下代码能执行,但是是结果是不正确,
在Swift中,以下代码在编译过程中就会直接告诉你溢出了避免后续发生不可预知的错误;这些都是因为
SIL存在的结果;
SIL文件分析
我们来看一下一段简短的代码生成的sil文件
class MJYPerson{
var age: Int = 18
var name: String = "卿卿"
}
var person = MJYPerson()
我们找到swift.mian 所在的文件夹利用命令$:swiftc -emit-sil main.swift >> main.sil && open main.sil 将其转换成sil文件,首先我们先定位到我们写的类MJYPerson:
里面包含了两个初始化过的age和name属性,一个@objc标识的deinit,以及一个默认初始化器
接下来我们快速定位到main函数。@mian 标识main的入口函数,@是我们的标识符:
以下对中间代码稍作解释:
- 0%~10% 也叫做寄存器,是虚拟寄存器,一旦赋值不可修改,和日常寄存器相比这个是虚拟的,最终运行在设别上会使用真正的寄存器
- alloc_global 分配一个全局变量 该变量是按照一定规则进行混写的变量,如何恢复呢可以使用 xcrun swift-demangle xxxx变量(lg: s4main6personAA9MJYPersonCvp)
- %3 = global_addr @MJYPerson // user: %7 拿到我们这个全局变量的地址给到3%这个寄存器***
- %4 = metatype $@thick MJYPerson.Type 是获取 MJYPerson的元类型
- %5 = function_ref @@convention(method) (@thick MJYPerson.Type) -> @owned MJYPerson 方法引用 function_ref MJYPerson.__allocating_init(),拿到这个函数的指针地址
- %6 = apply %5(%4) : $@convention(method) (@thick MJYPerson.Type) -> @owned MJYPerson 使用5%方法用4%的值做为参数 将结果6%这个寄存器
- *store %6 to %3 : $MJYPerson 将6%的值存储到3%(当实例变量的值存储到全局变量s4main6personAA9MJYPersonCvp中)
- %8 = integer_literal $Builtin.Int32, 0 Int类型在swift中是结构体类型,所有8%,9%在构建一个Int32位的整型,数值是0,所以最终return 0,这return 0可以参考OC的main函数最后的那个return
以上代码中提到了__allocating_init(),接下来我们找到这个方法:
- 该函数需要MJYPerson.Type 需要一个元类型,可以理解为isa指针
- %1 = alloc_ref $MJYPerson 会创建一个t的变量,引用计数会初始化为1,就是去堆区申请一块内存空间,如果标识为Objc ,那么就会使用oc的初始化方法allocInitWithZone:,接下来我们验证一下这个说法:
class MJYPerson{
var age: Int = 18
var name: String = "卿卿"
}
var person = MJYPerson()
我们打断点进入汇编可以得到一下结果
如果我们该类继承自NSObject:
class MJYPerson: NSObject{
var age: Int = 18
var name: String = "卿卿"
}
var person = MJYPerson()
同样通过断点进入能查看到以下情况
以上我们可以得出我们发现,初始化函数变成了objc_allocWithZone以及objc_msgSend;
- objc_allocWithZone
调用malloc函数申请内存空间; - 使用objc_msgSend
发送init`消息;
swift_allocObject
在之前的代码中我们发现纯swift会使用swift_allocObject进行类的初始化,那这里面到底进行了什么样的操作呢:
接下来我们需要用到swift源码来进行分,我们从stdlib->public->runtime->HeapObject.cpp文件,该文件是和我们的Swift类初始化相关的文件;
进来以后我们可以看到swift_allocObject是一个私有函数,他是被swift::swift_allocObject调起来的,下图是该方法的实现
- *HeapMetadata const metadata 元数据类型
- size_t requiredSize 所需要的大小
- size_t requiredAlignmentMask 所需要对齐的掩码64位环境下 ==7
- 该方法是object = 将参数传进去返回一个指针类型(HeapObject类型)
lg: reinterpret_cast 指针类型的转换
接下来我们看一下swift_slowAlloc这个方法的作用是什么
我们通过搜索在Heap.cpp的文件中找到了该函数我们可以清晰看到malloc函数:
通过以上情况我们可以得出
Swift 对象内存分配:
_allocating_init -----> swift_allocObject -----> _swift_allocObject -----> swift_slowAlloc -----> Malloc
最后malloc在堆区申请内存空间,内存对齐8字节,
HeapObject() = default;
// Initialize a HeapObject header as appropriate for a newly-allocated object.
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{ }
我们可以从HeapObject.h文件中找到以上代码,可得
- Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性:
- 一个是Metadata ,一个是 RefCount ,默认占用 16 字节大小。
- 相对于OC来说是使用8字节,里面只有isa指针
接下来我们来看一下HeapMetadatade的结构
struct InProcess;
template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;
从源码中我们可以找到以上代码:HeapMetadata是TargetHeapMetadata这个类型的别名定义,是TargetHeapMetadata的类型是一个泛型,接收一个参数InProcess,接下来我们看一下TargetHeapMetadata是什么
TargetHeapMetadata
从其定义可以分析得知:如果该类是一个纯Swift类,那么其类型为MetadataKind,如果该类需要与objc交互,那么它就是我们OC中常见的isa
MetadataKind
enum class MetadataKind : uint32_t {
#define METADATAKIND(name, value) name = value,
#define ABSTRACTMETADATAKIND(name, start, end) \
name##_Start = start, name##_End = end,
#include "MetadataKind.def"
/// The largest possible non-isa-pointer metadata kind value.
///
/// This is included in the enumeration to prevent against attempts to
/// exhaustively match metadata kinds. Future Swift runtimes or compilers
/// may introduce new metadata kinds, so for forward compatibility, the
/// runtime must tolerate metadata with unknown kinds.
/// This specific value is not mapped to a valid metadata kind at this time,
/// however.
LastEnumerated = 0x7FF,
};
该类清晰的表明了这是一个uint32_t类型,但是不能知道里面是什么样的机构这个时候我们找回父类Metadata,在里面我们可以找到一个StoredPointer 的kind,这个就是kind的类型
在metadata中我们可以找到下面这个方法
getTypeContextDescriptor() const {
switch (getKind()) {
case MetadataKind::Class: {
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;
}
}
该方法表明通过getKind这个返回来确认kind的类型是Class、Enum、Struct或者是Optional等,我们从这里可以知道TargetClassMetadata是所有类型的元类的最终基类。 接下来我们分析TargetClassMetadata,经过源码分析我们可以得到以下数据结构
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
}
在此我们大胆的猜测一下
//实例对象的内存结构
struct HeapObject {
var metadata:UnsafeRawPointer //原生指针
// 接下来我们有一个64位的refcount,为了便于区别,我们把64位的refcount分成两个32位的
var refcounted1: UInt32
var refcounted2: UInt32
}
class MJYPerson{
var age: Int = 18
var name: String = "卿卿"
}
//此时此刻person这个变量里面存储的内存地址指向HeapObject这个结构体
var person = MJYPerson()
//我们可以把person这个指针重新绑定成HeapObject这个结构体呢?
//获取当前对象的实例指针
let objcRawPtr = Unmanaged.passRetained(person as AnyObject).toOpaque()
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)//将objcRawPtr绑定成HeapObject.self的类型
print(objcPtr.pointee) //访问当前的指针
可以看到控制台打印出
可以看到metadata和refCount1中有值,那么接下来我们可以大胆的将person对象的metadata进行绑定来验证我们对metadata结构体的想法
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
}
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee//元类型函数指针把我当前metadata的指针绑定成当前Metadata类型的指针,大小是测量出来的
print(metadata)
我们可以看到打印出来的metadate中的信息跟我们的person实例是能够对的上的