美文网首页Swift基础
Swift语言的类与结构体--1

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

作者: spyn_n | 来源:发表于2021-12-28 23:45 被阅读0次

    一、类与结构体的异同

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

    类是引用类型。意味着,一个类型的变量并不直接存储具体的实例对象,而是存储具体实例对象的内存地址。\color{#88ff00}{内存是存在堆上}

    // 类的时候
    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枚举等)。意味着,一个结构体类型的存储是具体的实例值,而不是像引用类型存储的是具体实例对象的内存地址。\color{#88ff00}{一般情况下存储在栈上}

    // 结构体的时候
    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的\color{#ff0000}{合法性}决定是否初始化失败,也就是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<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
    bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
    // 分配一个全局变量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<A>(_:)
      %22 = function_ref @$ss27_allocateUninitializedArrayySayxG_BptBwlF : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %23
      %23 = apply %22<Any>(%21) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %25, %24
      %24 = tuple_extract %23 : $(Array<Any>, Builtin.RawPointer), 0 // users: %43, %40
      %25 = tuple_extract %23 : $(Array<Any>, 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<Any>, @guaranteed String, @guaranteed String) -> () // user: %40
      %40 = apply %39(%24, %36, %38) : $@convention(thin) (@guaranteed Array<Any>, @guaranteed String, @guaranteed String) -> ()
      release_value %38 : $String                     // id: %41
      release_value %36 : $String                     // id: %42
      release_value %24 : $Array<Any>                 // 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<HeapObject *>(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<Metadata>.stride).pointee
     MemoryLayout<Metadata>.stride
    
    print("end")
    

    下断点,lldb输出如下:


    相关文章

      网友评论

        本文标题:Swift语言的类与结构体--1

        本文链接:https://www.haomeiwen.com/subject/bsksqrtx.html