Swift语言的类与结构体--1

一、类与结构体的异同

  1. 相同点
    • 定义存储值的属性
    • 定义方法
    • 定义初始化器
    • 定义下标,并使用下表语法访问其值
    • 使用extension来扩展功能
    • 遵循协议来提供某种功能
  2. 不同点
    • 类有继承,而结构体没有
    • 类型转换使得您能够在运行时检查和解释类实例的类型
    • 类有析构函数来释放其分配的资源
    • 类有引用计数记录对一个是咧的引用次数

类是引用类型。意味着,一个类型的变量并不直接存储具体的实例对象,而是存储具体实例对象的内存地址。

// 类的时候
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")
值类型输出

由上面预演的可知,打印tt1都是直接打印出结构体的值,并且,t1t的一个副本,类似于本地化的一份,修改任何一个实例化的值对另一个实例没有影响。

image.png

结构体存储

关于内存可以先了解一下 内存管理的五大区 这篇文章,以下主要验证相关的各个内存区域:

栈区(stack):局部变量,参数,函数运行时上下文

堆区(heap):存储多有对象,由程序员/系统申请并由程序员/系统释放

全局区(global)

全局

常量区(data):

代码区(text)

结构体类型中如果添加了引用类型的成员变量,则会在堆区申请空间,而原来的结构体实例存储的位置不变,只是引用类型的成员变量区域存储的是指向实例的堆区的内存指针,如下图:


结构体中添加引用类型成员

所以在结构体中尽量不要添加引用类型成员变量,因为那样会涉及到堆内存的申请和释放,影响性能。

二、初始化器

1. 执行初始化器&便捷初始化器

类编译器默认不会自动提供成员初始化器,也就是如果类中有两个成员,如果没有初始化,则会报错。但是类中在没有成员的时候,会默认有一个init(){}初始化器。


结构体默认自动提供初始化器
结构体默认有成员初始化器

Swift中创建类的实例时必须为所有的存储属性设置初始值,因为swift会根据类型确定变量的类型或者根据初始值推断变量类型,这就是类型安全。所以类必须提供指定初始化器,也可以根据需要提供便捷初始化器(在初始化前面加上convenience关键字,便捷初始化器必须从相同的类中调用指定初始化器)。

image.png

便捷初始化器定义有一些规则,加上convenience关键字之后,需要严格控制初始化器的创建 规则:

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

  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖

  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。

  • 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。

2. 可失败初始化器

可失败初始化器 : 可以根据业务需要,根据参数d的决定是否初始化失败,也就是return nil

可失败初始化器

三、类的生命周期

1. Swift的编译流程

iOS开发语言OC和Swift后端都是通过LLVM进行编译最终生成可执行文件;

编译器

OC的编译是通过clang编译器,生成LLVM中间IR代码,最后由后端生成可执行文件.o;
Swift是通过Swift编译器编译生成,sil文件,再生成LLVM的可操作性的IR中间代码,然后再生成可执行文件。在这个过程中,区别就是前段编译器不一样,多了一个步骤是生成了sil文件,一下是编译流程图:


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


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关注blbbr等指令)。看到下断点到call __allocating_init的指令出,然后按住control + ⏬箭头单步进入可看到如下图:

纯Swift

纯Swift流程

swift_allocObject流程的时候,单步进入,里面并没有看到流程了,此时需要借助Swift源码进行分析。

  使用VS Code打开 Swift源码 全局搜索swift_allocObject,或者直接command+p输入HeapObject定位到文件,找到swift_allocObject函数,在其上方有一个_swift_allocObject_函数:

_swift_allocObject_函数

swift_slowAlloc函数

swift_slowAlloc
纯swift流程
3.2继承自NSObject
继承自NSObject
4.对象与类的内存结构

我们从上面的_swift_allocObject_中可以看到return的** object**对象是auto object = reinterpret_cast(swift_slowAlloc(requiredSize, requiredAlignmentMask))这条语句生成的,类型是HeapObject:
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \ InlineRefCounts refCounts

image.png

HeapMetadata只是一个别名,真正是TargetHeapMetadata

image.png

TargetHeapMetadata又继承自TargetMetadata,这个里面根据MetadataKind创建TargetHeapMetadata,并且如果需要SwiftObject-C交互(SWIFT_OBJC_INTEROP默认为1)还根据TargetAnyClassMetadata也就是isa创建。
image.png

TargetMetadata结构体,基本到这里,这就是基类了,但是还是没有看见其成员变量,基本到这就到了瓶颈了。

TargetMetadata

但是既然kind可以理解为isa,那么基本MetadataKind就是一个重点,在类型的descriptor下,什么情况下是类,然后就发现了这个函数getTypeContextDescriptor,根据kind来区分是class还是其他,如果是类的时候注意到TargetClassMetadata

kind:
1.class
2.Struct
3.Enum
4.Optional
5.ForeignClass


TargetClassMetadata函数,以及父类TargetAnyClassMetadata
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输出如下:


你可能感兴趣的:(Swift语言的类与结构体--1)