Swift类与结构体(上)

1,302 阅读8分钟

1、类与结构体的区别

相同点:

  • 定义成员属性、方法
  • 定义下标以使用下标语法提供对其值的访问
  • 定义初始化器
  • 可以使用extension来拓展功能
  • 遵循协议来提供某种功能

不同点:

  • 类可以继承
  • 类型转换使你能够在 运行时检查和解释类实例的类型
  • 类有析构函数用来释放其分配的资源
  • 引用函数允许 对一个类实例有多个引用

归纳:

  • 类是引用类型。也就是说一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用(通过地址修改是可以改变具体实例的内容的)。而结构体是值类型,存储的是具体的值,但是一套拷贝的值(修改值不影响原有具体实例)。简单说,对一个具体实例,类是指针,结构体是copy一份它的值(枚举也是值类型)
    • po一下类: image.png
    • po一下结构体: image.png 可以看到,只改了LZPerson的类型,打印结果一个是指针,一个是具体的值
  • 简单介绍4个命令:
    • pop: ppo 的区别在于使用po只输出对应的值,而p则会返回值的类型以及命令结果的引用名
    • x/8g: 读取内存中的值(8g: 8字节格式输出)
    • withUnsafePointer(to: T, body: (UnsafePointer<T>) throws -> Result):Swift中定义了一些特定类型的指针,每个类型都有他们的作用和目的,使用适当的指针类型可以防止错误的发生,并且更清晰地表达开发者的意图,防止未定义行为的产生。通过指针类型的名称,我们可以知道这是一个什么类型的指针:可变/不可变原生(raw)/有类型是否是缓冲类型(buffer),大致有以下8种类型: image.png
      • unsafe:不安全的
      • Write Access:可写入
      • Collection:像一个容器,可添加数据
      • Strideable:指针可使用 advanced 函数移动
      • Typed:是否需要指定类型(范型) 扯远了...打印t和t1的值看一下效果: image.png
    • frame variable:按某种规则查看某个变量的内存布局,具体规则使用help frame variable查看
      //断点后,lldb中执行命令
      frame variable -L 变量名
      
      image.png
  • 引用类型值类型 还有个很大的区别是储存位置的区别:一般情况,引用类型存储在堆上,而值类型存储在栈上
    • 栈(stack):局部变量和函数运行过程中的上下文

      • 编译时确定,是静态的,由系统分配,所以速度快
      • 栈从高地址向低地址扩展
      • 函数调用是在栈上的,不同函数中的栈数据不能被另一个函数调用,多线程中每个线程启动的时候是调用一个函数,所以各线程有各自的栈
      • 数据 size可以确定时使用栈 ,因为效率高、速度快
    • 堆(heap):存储所有对象

      • 运行时确定,是动态的,由程序员分配,所以速度慢
      • 堆由低地址向高地址扩展
      • 堆是在进程中 的,进程中所有的线程都可以访问 堆数据
      • 数据 size不确定时使用堆 ;使用 非常庞大的数据时使用堆,因为使用完需要抓紧释放
    • 全局(global):存储全局变量、常量、代码区

    • 又扯远了..好吧,所以我们平时代码可以 尽量使用结构体和枚举来定义内容 ,因为效率会高

1.1、初始化器

  • Swift是类型安全的,要求对创建的类和结构体的成员属性赋值,不进行赋值的需要创建初始化器,对类来说 需要手动创建初始化器 ,而 结构体系统会默认创建一个初始化器 image.png
1.1.1、指定初始化器
  • 无赋值情况,自己定义的初始化器 image.png

  • 继承的类中,要按新增加的属性初始化 -> 调用父类super的初始化器 -> 对继承的属性重新赋值的顺序 image.png

1.1.2、便捷初始化器
  • 使用convenience修饰 init ;通过预初始化,来实现缺参初始化类或结构体,因此便捷初始化器必须从 相同的类里 调用另一个初始化器来先进行预初始化; 预初始化完成前,调用受限,甚至self都无法使用 image.png
1.1.3、可失败初始化器
  • init后添加可选标识? ;当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况; 对输入的参数进行条件筛选 ,并且要注意,指定初始化器如果就是可选的,那么便捷初始化也必须是可选的 image.png
1.1.4、必要初始化器
  • 在类的初始化器前添加required修饰符来表明所有该类的子类都必须实现该初始化器 image.png

2、类的生命周期

  • iOS开发语言,不论是OC还是Swift,都是通过LLVM进行编译的,最终生成.o文件 image.png
  • OC 通过 clang 编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们的机器码)
  • Swift 则是通过 Swift编译器 编译成 IR ,然后在生成可执行文件

2.1、Swift编译流程

image.png

  1. Swift Code 经过-dump-parse进行语义分析解析成Parse(抽象语法树)

  2. Parse 经过-dump-ast进行语义分析分析语法是否正确,是否安全

  3. Seam之后会把 Swift Code 会降级变成SILGen(Swift中间语言Swift intermediate language),对于 SILGen 又分为Raw SIL(原生的,没有开启优化选项)和SIL Opt Canonical SIL(经过优化的)

  4. 优化完成的SIL会由 LLVM 降级成为IR,降级成 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,Swift代码编译过程中添加了 SIL ,对代码进行更严格的安全检查,举例如下,因为int8_t只有一个字节,其运算结果已经溢出,OC打印错误数据,而Swift会直接报错 image.png

2.2、查看SIL中间代码

  • 先看个别的简单 SIL 代码熟悉下,抛个砖 image.png

  • 再来查看我们自己的SIL代码,我们需要先获得SIL文件,可以创建一段JS脚本后运行这段脚本生成(File --> New --> target) image.png image.png

    • swiftc:Swift编译器;后边为要编译的文件路径与名称
  • 以这个简单代码分析一下: image.png

    • 定义的类 image.png

      image.png

      如果源Swift代码拿掉实例化调用,而是加上 print("end") ,得到以下 SIL ,这个一下复杂很多,本人尽量以自己的角度进行了理解可能并不很到位,要更好的理解全部还是需要对照下边官方SIL文档 image.png

    • __allocating_init():文中为__allocating_init(LZAge:_:_isMale),创建实例对象,该函数需要一个 LZPerson.Type 的元类型 image.png

    • alloc_ref会创建对应的实例变量,其引用计数初始化为1; alloc_ref 实际上也就是去堆区申请内存空间;如果标识为objcSwift类将会使用Objective-C+allocWithZone:初始化方法

SIL指令
  • @main: 入口函数

  • %0、%1、%2...: 寄存器,类似于常量,一旦赋值就不能更改,只能通过增加角标再赋值、地址通过store赋予可以,虚拟的,跑在真机上才使用真的寄存器 

  • @$:后边跟的是经过混写之后的名称,可以在终端通过xcrun swift-demangle方法解析出来 image.png

  • integer_literal:创建一个系统内置整数类型

  • tuple_extract:与元组中提取的元素同类型

    %1 = tuple_extract %0 : $(T...), 123
    // %0 must be of a loadable tuple type $(T...)
    // %1 will be of the type of the selected element of %0
    
  • print(_:separator:terminator:):是一个⽤来输出一个或多个值到适当输出区的全局函数

  • begin_access:开始访问目标内存。

    • vtable:虚表,将可能进行动态调用的目标记录在虚表中跟踪
    • SIL 表示使用 class_methodsuper_methodobjc_methodobjc_super_method 指令对类方法的动态调度。
  • class_methodsuper_method 的潜在目标在每个类类型的 sil_vtable 声明中进行跟踪.该声明包含从类的每个方法(包括从其基类继承的方法)到实现该类方法的 SIL 函数的映射:

2.3、Swift对象内存分配流程:

  • 对Swift代码打断点,走汇编调试 image.png image.png

  • 下载Swift源码查看HeapObject.cpp:

    • swift_allocObject image.png
    • swift_slowAlloc:执行malloc开辟内存空间 image.png
内存分配流程总结
  • __allocating_init(编译器为每个实例对象生成) --> swift_allocObject --> _swift_allocObject_(私有函数) --> swift_slowAlloc --> Malloc 

  • Swift对象的内存结构为 HeapObject(OC中为objc_object),有两个属性: 

    • Metadata
    • RefCount
    • 因此默认占用16字节大小 (OC中objc_object内为isa,因此是8字节)
      objc_object{ 
          isa 
      }
      

3、Swift对象内存结构

  • 上边已经说到两个属性,那么我们继续来分析一下Metadata是个什么东西 image.png

  • 看到了是HeapMetadata类型,继续跳转进去查看 image.png

  • 原来 HeapMetadataTargetHeapMetadata这个类型的别名,那么就再分析 TargetHeapMetadata image.png

  • TargetHeapMetadata继承自TargetMetadata,MetadataKind只是个int32分析不出东西,没办法,向上找父类TargetMetadata代码 image.png

  • 在该函数中通过getKind返回的类型来区分当前的类型;而TargetClassMetadata就是所有类型元类最终基类;

    • 如果是 Class, 那么将会把当前指针this通过static_cast强转为TargetClassMetadata类型;
    • TargetClassMetadata 继承自TargetAnyClassMetadata;
    • TargetAnyClassMetadata的数据结构含有Superclass,CacheData[2],Data等属性,这与OC类的结构很相似
  • 结论: Metadata 基本相当于 OC 中的 isa ,其内存结构为:

    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
    }
    

Swift方法拓展

image.png

  • Unmanaged.passUnretained(实例 as AnyObject).toOpaque():获取实例对象的指针
  • objcRawPtr.bindMemory(to: *T.type*, capacity: *Int*):绑定内存
  • .pointee:访问指针