Swift进阶-类&对象&属性

1,284 阅读13分钟

要想深入的学习Swift,就得从最基本的类,对象,属性开始深入分析,本文将使用SIL文件以及Swift源码,来对类、对象和属性的底层结构进行探究。

编译流程

  • 在了解SIL之前,先来了解iOS的编译过程:

    • iOSOCSwift两个语言,但他们后端都是通过LLVM编译的,如下图:

    截屏2021-12-06 22.08.30.png

    • OC是通过Clang编译器编译成IR,然后再生成可执行文件.o
    • Swift是通过swiftc编译器编译生成IR,然后再生成可执行文件.o

Swift编译流程

  • 根据在 Swift Compiler 中对swift编译器主要组件的描述,可以得到下图:

    截屏2021-12-06 23.06.09.png

    • 主要分为以下几个过程:
        1. Parsing(解析):解析器是一个简单的递归下降解析器(在 lib/Parse 中实现),带有一个集成的、手工编码的词法分析器。解析器负责生成没有任何语义或类型信息的抽象语法树(AST),并针对输入源的语法问题发出警告或错误
        1. Semantic analysis(语义分析):语义分析(在 lib/Sema 中实现)负责获取解析后的AST并将其转换为格式良好、经过完全类型检查的AST形式,针对源代码中的语义问题发出警告或错误。语义分析包括类型推断,如果成功,则表明从生成的、类型检查的AST生成代码是安全的
        1. Clang importer(Clang导入器)Clang导入器(在 lib/ClangImporter 中实现)导入Clang并将它们导出的CObjective-C API映射到相应的Swift API。生成的导入AST可以通过语义分析进行引用
        1. SIL generation(SIL生成)Swift中间语言(SIL)是一种高级的、特定于Swift的中间语言,适用于进一步分析和优化Swift代码。SIL生成阶段(在 lib/SILGen 中实现)将经过类型检查的AST降低为所谓的“原始”SILSIL的设计在docs/SIL.rst 中进行了描述
        1. SIL guaranteed transformations(SIL保证转换)SIL保证转换(在 lib/SILOptimizer/Mandatory 中实现)执行影响程序正确性的附加数据流诊断(例如使用未初始化的变量)。这些转换的最终结果是“规范的”SIL
        1. SIL Optimizations(SIL优化)SIL优化(在 lib/Analysislib/ARClib/LoopTransforms和 lib/Transforms 中实现)对程序执行额外的高级、特定于Swift的优化,包括(例如)自动引用计数优化,去虚拟化和泛型专业化
        1. LLVM IR Generation(LLVM IR 生成)IR生成(在 lib/IRGen 中实现)将SIL 降低到LLVM IR,此时LLVM可以继续优化它并生成机器代码。

其中SILSwift编译过程中的中间代码,位于在ASTLLVM IR之间

生成SIL

  • 下面将swift代码生成SIL文件:
    // main.swift
    class WSPerson {
        var age: Int = 18
        var name: String = "wushuang"
    }
    var ws = WSPerson()
    
    • swift在编译过程中使⽤的前端编译器是swiftc,可以使用命令swiftc -emit-sil main.swift >> ./main.silswift文件生成SIL文件:
    截屏2021-12-07 09.12.52.png

初步分析SIL文件

  • 类和对象在SIL文件中的形式:

    class WSPerson {
      // 存储属性 age
      @_hasStorage @_hasInitialValue var age: Int { get set }
      // 存储属性 name
      @_hasStorage @_hasInitialValue var name: String { get set }
      // @objc标记的deinit方法
      @objc deinit
      // 构造方法init
      init()
    }
    // 存储属性ws
    @_hasStorage @_hasInitialValue var ws: WSPerson { get set }
    // ws
    // s4main2wsAA8WSPersonCvp是混淆之后的,main.ws
    sil_global hidden @$s4main2wsAA8WSPersonCvp : $WSPerson
    
    • 其中s4main2wsAA8WSPersonCvp是混淆后的变量,可以使用命令xcrun swift-demangle s4main2wsAA8WSPersonCvp进行还原,结果如下:

      $s4main2wsAA8WSPersonCvp ---> main.ws : main.WSPerson
      
  • main函数:

    // @main 入口函数
    // @convention(c)代表c函数
    // main函数有两个参数Int32类型和 UnsafeMutablePointer指针类型,并且返回值是Int32类型
    sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
    // `%0、%1` 是SIL中的寄存器,赋值后就不会改变可以理解为常量,是虚拟的寄存器,与register read 中的寄存器不同
    bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
    
    // 1. 创建一个 main.WSPerson类型的全局变量并存入 %2
      alloc_global @$s4main2wsAA8WSPersonCvp          // id: %2
    
    // 2. 获取 全局变量的地址,并将地址赋值给 %3
      %3 = global_addr @$s4main2wsAA8WSPersonCvp : $*WSPerson // user: %7
    
    // 3. 获取WSPerson元数据类型,并赋值给%4
      %4 = metatype $@thick WSPerson.Type             // user: %6
    
    // 4. 将WSPerson.__allocating_init()函数赋值给 %5
      // function_ref WSPerson.__allocating_init()
      %5 = function_ref @$s4main8WSPersonCACycfC : $@convention(method) (@thick WSPerson.Type) -> @owned WSPerson // user: %6
    
    // 5. apply方法调用函数%5 __allocating_init,并将结果赋值给 %6
      %6 = apply %5(%4) : $@convention(method) (@thick WSPerson.Type) -> @owned WSPerson // user: %7
    
    // 6. 将%6 存储到 地址%3,
      store %6 to %3 : $*WSPerson                     // id: %7
    
    // 创建整形变量 0 并返回
      %8 = integer_literal $Builtin.Int32, 0          // user: %9
      %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
      return %9 : $Int32                              // id: %10
    } // end sil function 'main'
    
    • @main函数是main.swift文件的入口函数,%0、%1SIL中的寄存器,赋值后就不会改变可以理解为常量,是虚拟的寄存器,与看汇编时用到的register read中的寄存器不同
    • main函数中主要是做了些创建对象的一些工作,主要有以下几个步骤
        1. 创建一个全局变量ws并赋值%2
        1. 获取全局变量ws的地址,并赋值给%3
        1. 获取WSPerson元数据类型,并赋值给%4
        1. WSPerson.__allocating_init()函数赋值给%5
        1. 根据元数据类型调用__allocating_init函数创建对象,并将结果赋值给%6
        1. 将创建的对象%6存入全局变量%3地址,也就是对全局变量ws进行赋值
        1. return结束main函数

创建对象

  • 在上面分析中,我们知道创建对象的核心是调用了__allocating_init函数,它的代码如下:

源码分析

// WSPerson.__allocating_init()
sil hidden [exact_self_class] @$s4main8WSPersonCACycfC : $@convention(method) (@thick WSPerson.Type) -> @owned WSPerson {
// %0 "$metatype"
bb0(%0 : $@thick WSPerson.Type):
// 1. 在堆上创建WSPerson型的对象,并赋值给%1
  %1 = alloc_ref $WSPerson                        // user: %3
  // 2. 获取WSPerson.init()函数并赋值给 %2
  // function_ref WSPerson.init()
  %2 = function_ref @$s4main8WSPersonCACycfc : $@convention(method) (@owned WSPerson) -> @owned WSPerson // user: %3
  // 3. 对象调用init方法并返回当前对象
  %3 = apply %2(%1) : $@convention(method) (@owned WSPerson) -> @owned WSPerson // user: %4
  return %3 : $WSPerson                           // id: %4
} // end sil function '$s4main8WSPersonCACycfC'
  • 创建对象的核心过程主要有以下三步:
      1. 在堆上创建WSPerson型的对象,并赋值给%1
      1. 获取WSPerson.init()函数并赋值给%2
      1. 对象%1调用init方法初始化并返回当前对象

汇编分析

  • xcode代码中添加符号断点__allocating_init,然后分析汇编:

    截屏2021-12-07 14.58.00.png

  • __allocating_init在底层实质上是调用了swift_allocObject函数,此时需要去 Swift源码 (需要进行源码编译) 查看:

    static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                         size_t requiredSize,
                                         size_t requiredAlignmentMask) {
      assert(isAlignmentMask(requiredAlignmentMask));
      auto object = reinterpret_cast<HeapObject *>(
      // 计算内存大小
          swift_slowAlloc(requiredSize, requiredAlignmentMask));
    
      // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
      // check on the placement new allocator which we have observed on Windows,
      // Linux, and macOS.
      // 根据元数据类型创建实例对象
      new (object) HeapObject(metadata);
    
      // If leak tracking is enabled, start tracking this object.
      SWIFT_LEAKS_START_TRACKING_OBJECT(object);
    
      SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);
    
      return object;
    }
    
  • _swift_allocObject_函数的主要进行两个步骤:

      1. 调用swift_slowAlloc函数计算内存大小
      1. 根据内存和元数据类型在堆区创建对象
    • swift_slowAlloc计算内存的源码如下:
    // Linux malloc is 16-byte aligned on 64-bit, and 8-byte aligned on 32-bit.
    #  if defined(__LP64)
    #    define MALLOC_ALIGN_MASK 15
    #  else
    #    define MALLOC_ALIGN_MASK 7
    #  endif
    
    void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
      void *p;
      // This check also forces "default" alignment to use AlignedAlloc.
      if (alignMask <= MALLOC_ALIGN_MASK) {
    #if defined(__APPLE__)
        p = malloc_zone_malloc(DEFAULT_ZONE(), size);
    #else
        p = malloc(size);
    #endif
        } else {
        size_t alignment = (alignMask == ~(size_t(0)))
                               ? _swift_MinAllocationAlignment
                               : alignMask + 1;
        p = AlignedAlloc(size, alignment);
      }
      if (!p) swift::crash("Could not allocate memory.");
      return p;
    }
    
    • alignMask是内存对齐的mask,在64位16字节对齐,而32位8字节对齐,关于对齐的方式可以参考 iOS底层-内存对齐
    • 至于MALLOC_ALIGN_MASK为什么是对齐位数减去1,是因为计算对齐时,要&~MALLOC_ALIGN_MASK,例如7~7就是8的倍数。
  • HeapObject方法的源码如下:

    struct HeapObject {
      /// This is always a valid pointer to a metadata object.
      // metadata 是指针指向 HeapMetadata
      HeapMetadata const *__ptrauth_objc_isa_pointer metadata;
    
      SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
    
    #ifndef __swift__
      HeapObject() = default;
    
      // Initialize a HeapObject header as appropriate for a newly-allocated object.
      // 构造方法
      constexpr HeapObject(HeapMetadata const *newMetadata) 
        : metadata(newMetadata)
        , refCounts(InlineRefCounts::Initialized)
      { }
    
      // Initialize a HeapObject header for an immortal object
      constexpr HeapObject(HeapMetadata const *newMetadata,
                           InlineRefCounts::Immortal_t immortal)
      : metadata(newMetadata)
      , refCounts(InlineRefCounts::Immortal)
      { }
    
    #endif // __swift__
    };
    
    • 在上面HeapObject构造函数中有两个参数
      • metadataHeapMetadata类型的指针,占8字节
      • 通过查找refCounts得到它是InlineRefCounts类型,占用8字节
    • 此时可以得出结论,swift对象本质为HeapObject,占用16字节

内存分配

  • 通过上面分析,我们知道对象的内存,那么类WSPerson的内存是多少呢,下面使用class_getInstanceSize来打印下类的内存:

    截屏2021-12-07 19.38.09.png

    • 结果占用的40字节,这是为什么?再使用MemoryLayout<类型>.stride去分别打印IntString所占用内存:

    截屏2021-12-07 19.48.00.png

    • 打印得出Int占用8字节String占用16字节,通过源码搜索发现IntString都是结构体类型:

      // InterTypes.swift
      @frozen
      public struct Int
        : FixedWidthInteger, SignedInteger,
          _ExpressibleByBuiltinIntegerLiteral {...}
      
      // String.swift
      @frozen
      public struct String {...}
      
    • 40 = metadata(8字节)+ refCount(8字节)+ Int(8字节)+ String(16字节),但metadata还不知道是什么,下面将对它进行研究

总结

    1. 对象内存分配流程:__allocating_init -> swift_allocObject_ -> swift_slowAlloc -> malloc
    1. Swift实例对象占用16字节,比OC中多了refCounted(引用计数大小)

  • 通过上面的分析我们知道metadataHeapMetadata类型的指针,去源码查看:

    template <typename Target> struct TargetHeapMetadata;
    using HeapMetadata = TargetHeapMetadata<InProcess>;
    
    • HeapMetadataTargetHeapMetadata模版函数的别名
  • 在查看TargetHeapMetadata的代码:

    template <typename Runtime>
    struct TargetHeapMetadata : TargetMetadata<Runtime> {
      using HeaderType = TargetHeapMetadataHeader<Runtime>;
    
      TargetHeapMetadata() = default;
      // 初始化方法
      constexpr TargetHeapMetadata(MetadataKind kind)
        : TargetMetadata<Runtime>(kind) {}
    #if SWIFT_OBJC_INTEROP
      constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
        : TargetMetadata<Runtime>(isa) {}
    #endif
    };
    
    • 代码中TargetHeapMetadata继承TargetMetadata,方法核心是是根据参数kind(传入的InProcess)调用TargetMetadata的构造方法。下面继续查看代码
  • TargetMetadata的代码中主要是对kind的一些操作,kind类型如下:

    struct TargetMetadata {
    ...
    private:
      StoredPointer Kind;
    ...
    }
    
    • 它实际就是传入InProcess结构体中的uintptr_t,继续查看类型得知为unsigned long类型:
    struct InProcess {
      ...
      using StoredPointer = uintptr_t;
      ...
    }
    typedef unsigned long           uintptr_t;
    
    • 所以kindunsigned long类型,它主要是区分当前是那种类型的数据
  • 进入TargetMetadata函数中的MetadataKind函数,然后在点击#include "MetadataKind.def"中的MetadataKind,可以进入MetadataKind.def文件夹,可以看到很多类型,类型对应的kind值如下:

    • class : 0x0
    • Struct : 0x200
    • Enum : 0x201
    • Optional : 0x202
    • ForeignClass : 0x203
    • Opaque : 0x300
    • Tuple : 0x301
    • Function : 0x302
    • Existential : 0x303
    • Metatype : 0x304
    • ObjCClassWrapper : 0x305
    • ExistentialMetatype : 0x306
    • HeapLocalVariable : 0x400
    • HeapGenericLocalVariable : 0x500
    • ErrorObject : 0x501
    • Task : 0x502
    • Job : 0x503
    • LastEnumerated : 0x7FF

类的结构分析

  • TargetMetadata中有获取class的方法

    const TargetClassMetadata<Runtime> *getClassObject() const;
    
    • 其中的getClassObject函数是根据kind获取object类型:
    template<> inline const ClassMetadata *
    Metadata::getClassObject() const {
      switch (getKind()) {
      case MetadataKind::Class: {
        // Native Swift class metadata is also the class object.
        // 如果是class,则强转成 ClassMetadata类型
        return static_cast<const ClassMetadata *>(this);
      }
      case MetadataKind::ObjCClassWrapper: {
        // Objective-C class objects are referenced by their Swift metadata wrapper.
        auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
        return wrapper->Class;
      }
      // Other kinds of types don't have class objects.
      default:
        return nullptr;
      }
    }
    
    • 如果kindClass类型,则将当前的metadata强转成ClassMetadata,而ClassMetadataTargetClassMetadata根据类型的别名

      using ClassMetadata = TargetClassMetadata<InProcess>;
      
  • 继续跟进TargetClassMetadata查看

    截屏2021-12-08 10.52.40.png

    • 在函数中我们得知TargetClassMetadata继承TargetAnyClassMetadata,并且有自己的构造方法,以及相关的字段,它的结构和 objc4-818.2 源码中的swift_class_t结构体是一样的

      struct swift_class_t : objc_class {
          uint32_t flags;
          uint32_t instanceAddressOffset;
          uint32_t instanceSize;
          uint16_t instanceAlignMask;
          uint16_t reserved;
      
          uint32_t classSize;
          uint32_t classAddressOffset;
          void *description;
          // ...
      
          void *baseAddress() {
              return (void *)((uint8_t *)this - classAddressOffset);
          }
      };
      
  • 再继续查看TargetAnyClassMetadata中的代码:

    截屏2021-12-08 11.21.05.png

    • 此处的结构与OC中的objc_class的结构一样,有isa,有父类,有cacheDataData类似于objc_class中的bits
  • 根据上面分析我们可以得到结论:当metadatakindClass时,有如下的继承关系:

    截屏2021-12-08 14.47.20.png

属性

swift中有四种属性,分别是:存储属性计算属性延迟属性类型属性,下面对他们进行详细的讲解

存储属性

  • 存储属性又分为两种: 常量存储属性(let修饰)变量存储属性(var修饰),具体代码如下:

    class WSPerson {
        let age: Int = 18 // 常量存储属性
        var name: String = "wushuang" // 变量存储属性
    }
    
    • 常量存储属性的值不可修改,而变量存储属性的值可修改,在SIL文件中可以看的更直白:
    class WSPerson {
      @_hasStorage @_hasInitialValue final let age: Int { get }
      @_hasStorage @_hasInitialValue var name: String { get set }
      @objc deinit
      init()
    }
    
    • Sil文件中常量存储属性只能使用getter方法,变量存储属性可以settergetter方法

计算属性

  • 计算属性不占用内存,它本身不存储值,通过getter间接访问值,如下两个案例

    class WSName {
        var familyName: String = "Bryant"
        var givenName: String = "Kobe"
        var fullName: String {
            get {
                return givenName + familyName
            }
        }
    }
    
    class Square {
        var width: Double = 20.0
        var area: Double {
            get {
                return pow(width, 2)
            }
            set {
                width = sqrt(newValue)
            }
        }
    }
    
    • 查看SIL文件:

      截屏2021-12-08 16.19.41.png

      • 可以看到计算属性没有计算属性所有的_hasStorage @_hasInitialValue标识符
  • 再打印这两个类的占用内存:

    截屏2021-12-08 16.25.18.png

    • 结果WSName占用的内存为48字节familyName占用16字节givenName占用16字节metadata占用8字节refCount占用8字节,加起来刚好48字节fullName没有占用内存。
    • Square中的width占用8字节metadata占用8字节refCount占用8字节,加起来刚好24字节area没有占用内存
    • 所以得出结论:计算属性不占用内存

属性观察者(willSet/didSet)

  • 属性观察者可以理解为OC中的KVO,在属性调用setter方法时:

      1. 新值存储前会调用willSet方法,可以获取新值newValue
      1. 新值存储后会调用didSet方法,可以获取旧值oldValue
    • 如下面案例所示:

    截屏2021-12-08 16.45.56.png

    • SIL文件查看:

    截屏2021-12-08 16.52.02.png

    • name.setter函数中主要进行以下几个操作:
        1. 获取name旧值并存入寄存器%6
        1. 调用name.willSet函数并传入新值%0
        1. 将新值存入name,并释放旧值
        1. 调用name.didSet函数并传入旧值%6
  • 问题1:init方法中修改属性的值,是否会触发观察者?

    • 在案例添加init再运行

    截屏2021-12-08 17.16.57.png

    • 结果并没有触发willSetdidSet方法,在SIL文件中分析得知,init时修改属性的值只是修改属性初始化时的值,由于此时对象创建的过程还没有完成,所以并不会触发setter方法,进而不会触发属性观察者

    截屏2021-12-08 17.20.09.png

  • 问题2:哪里可以添加属性观察

    • 可以在以下三个地方添加观察者:
        1. 类中定义的存储属性
        1. 通过类继承的存储属性
        1. 通过类继承的计算属性
        截屏2021-12-08 17.37.10.png
  • 问题3:子类和父类的计算属性同时存在didSet、willSet时,其调用顺序是什么?

    • 使用以下案例打印:

    截屏2021-12-08 17.46.32.png

    • 通过案例得知:子类父类的计算属性同时存在didSet、willSet时,当属性值改变父类和子类两个方法的调用顺序如下:
      • willSet:先子类,后父类
      • didSet:先父类,后子类
  • 问题4:子类调用了父类的init方法,并且在里面修改属性值是否会触发观察者?

    • 案例代码运行如下:

    截屏2021-12-08 17.53.52.png

    • 结果可以触发观察者,因为子类调用了父类的init,已经初始化了,而初始化流程保证所有属性都有值,所以可以触发观察属性了。

延迟属性

    1. 延迟存储属性的初始值在其第⼀次使⽤时才能访问,使用关键字lazy来标识一个延迟属性,延迟属性必须要设置一个初始值,代码如下:

    截屏2021-12-08 20.02.43.png

    1. 下面在Sil中分析可选类型:
    class WSPerson {
      lazy var age: Int { get set }
      @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
      @objc deinit
      init()
    }
    
    • Sil中延迟属性是可选类型,在继续分析getter方法

    截屏2021-12-08 20.28.09.PNG

    • 分析得之在访问getter时,才会对age赋值
  • 下面来看下延迟属性的内存情况:

    截屏2021-12-08 23.28.17.png

    • 结果发现使用延迟属性后会导致内存增大

类型属性

  • 类型属性属于这个类的本身,不管有多少个实例,类型属性只有⼀份,我们可以使⽤static来声明⼀个类型属性:

    class WSPerson {
        static let name: String = "wushuang"
    }
    // 访问
    let name = WSPerson.name
    
    • 查看Sil文件观察它的变化:

    截屏2021-12-08 18.48.32.png

    • 使用static修饰变量后,在Sil文件中会生成一个全局变量,而name是线程不安全的
    • nameglobal_init中可以看到,name只初始化一次:

    截屏2021-12-08 19.17.23.png

    • 可以在代码中打断点,然后查看汇编,再进入(step into)函数WSPerson.name.unsafeMutableAddressor中可以看到它调用了swift_once

    截屏2021-12-08 19.23.06.png

    • swift_once在源码中最后调用的dispatch_once_f,也就是单例

    截屏2021-12-08 19.39.06.png

  • 我们可以使用static来创建单例

    class WSPerson {
        var name: String = "wushuang"
        var age: Int = 18
        static let share = WSPerson()
      
        private init() {}
    }
    // 调用方法
    let ws = WSPerson.share
    

总结

    1. 存储属性:有常量存储属性和变量存储属性两种,他们都占用内存
    1. 计算属性:不占用内存
    1. 属性观察者
      1. 属性观察可以添加在类的存储属性继承的存储属性继承的计算属性
      1. 父类在调用init中改变属性值不会触发属性观察,子类调用父类的init触发属性观察
      1. 统一属性在父类和子类都添加观察,在触发观察时:
      • willSet方法,先子类后父类
      • didSet方法,先父类后子类
    1. 延迟属性(lazy):延迟属性必须有初始值,只有在访问后内存中才有值,延迟属性对内存有影响,
    1. 类型属性:类型属性必须有初始值,内存只分配一次,是线程安全的,可以用于单例

写在最后

由于下载的Swift-source 5.5.1xcode13.1中没有编译成功,所以导致很多的调试都不方便,如果有不正确的地方欢迎指正🙏