Swift 原理探索:类与结构体(上)

540 阅读8分钟

一、初识类与结构体

1.1 class & struct

Swift 中类用class修饰,结构体用struct修饰,如下所示:

struct SSLTeacher {
    var age: Int
    var name: String
    
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

class SSLPerson {
    var age: Int
    var name: String
    
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    deinit {
        
    }
}

1.2 相同点和不同点概述

结构体和类的主要相同点有:

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

主要的不同点有:

  • 类有继承的特性,而结构体没有
  • 类型转换使您能够在运行时检查和解释类实例的类型
  • 类有析构函数用来释放其分配的资源
  • 引用计数允许对一个类实例有多个引用

1.3 类是引用类型

对于类与结构体我们需要区分的第一件事就是:

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

下面我们借助指令来查看当前变量的内存结构

po:只会输出对应的值
p:返回值的类型以及命令结果 的引用名
x/8g:读取内存中的值(8g: 8字节格式输出)

看下面的示例:

image.png

  • 可以看到 t 和 t1 的地址是相同的,也就说明了类是引用类型

用图来表示如下:

image.png

1.4 结构体 是值类型

Swift 中有引用类型,就有值类型,最典型的就是 Struct ,结构体的定义也非常简单,相比较类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值)。

通过下面的例子来进行理解:

image.png

  • 可以看到,结构体是直接存储值。

1.5 存储区域的不同

引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在上,引用类型存储在上。

对内存区域不太了解的可以查看 内存五大区

1.5.1 结构体 的内存分配

我们用下面这个例子进行分析

struct SSLTeacher {
    var age = 18
    var name = "ssl"
}

func test() {
    var t = SSLTeacher()
    print("end")
}   

test()

接下来使用命令

frame varibale -L xxx

image.png

  • t 是存储在栈上的,age 和 name 也是存储在栈上的
  • t 的首地址直接是存储的 age 变量,age 和 name 也是连续的
  • 当执行 var t = SSLTeacher()这段代码时,会在栈上开辟所需的内存空间,当test()函数执行完时,回收所开辟的内存空间。

1.5.2 类 的内存分配

同样的用一个示例来进行分析

image.png

  • 在栈上,会开辟 8 字节内存空间,来存储 SSLTeacher 的地址
  • 在堆上,会寻找合适的内存区域,开辟内存,存储 value
  • 函数结束时,会销毁栈上的指针,查找并回收堆上的示例变量。

1.5.3 结构体和类的时间分配

上面我们可以发现类的内存分配,比较繁琐,而且还会有引用计数等操作。

我们也可以通过 github 上 StructVsClassPerformance 这个案例来直观的测试当前结构体和类的时间分配。

经过测试我们可以发现,结构体速度明显快于类。

1.5.4 优化案例

struct Attachment {
    let fileURL: URL
    let uuid: String
    let mineType: String
    ...
}

Attachment 是值类型,但是 uuid 和 mineType 是引用类型会影响性能,下面对它们进行优化,改为用值类型来进行修饰

struct Attachment {
    let fileURL: URL
    let uuid: UUID
    let mineType: MimeType
    ...
}

enum MimeType {
    case jpeg = "image/jpeg"
    ...
}

二、class的初始化器

Swift 中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。

2.1 结构体初始化器

类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!

struct SSLTeacher {
    var age: Int
    var name: String
}

2.2 类初始化器

类必须要提供对应的指定初始化器,同时我们也可以为当前的类提供便捷初始化器(注意:便捷初始化器必须从相同的类里调用另一个初始化器。)

class SSLPerson {
    var age: Int
    var name: String

    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
    }
    
    convenience init(_ age: Int) {
        self.init(18, "ssl")
        self.age = age
    }
}
  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。

当我们派生出一个子类 SSLTeacher ,看下它的指定初始化器的写法

class SSLTeacher: SSLPerson {
    var subjectName: String
    
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(18, "ss")
        self.age = 17
    }
}
  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成,这里的 subjectName
  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖,这里的 age。

2.3 可失败初始化器

可失败初始化器:这个也非常好理解,也就意味着当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句, 来表明可失败初始化器在何种情况下会触发初始化失败。写法如下:

class SSLPerson {
    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) {
        self.init(18, "ssl")
    }
}
  • 这里可失败初始化器的意思是:如果年龄小于 18 认为不是一个合法的成年人,创建失败。

2.4 必要初始化器

必要初始化器:在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器

image.png

  • 如果子类没有实现该必要初始化器,就会报错。

三、类的生命周期

3.1 Swift 编译

iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:

image.png

  • OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器 码)
  • Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。

详细看下 Swift 的编译过程:

image.png

下面是相关的编译命令,有兴趣的小伙伴可以玩一下:

// 分析输出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

3.2 sil 文件分析

在 main.swift 中写入下面的代码:

class SSLPerson {
    var age: Int = 18
    var name: String = "ssl"
    }

var t = SSLPerson()

运行下面的命令

swiftc -emit-sil main.swift > ./main.sil && open main.sil

通过上面的命令,将 main.swift 编译成 main.sil 文件,文件代码了解即可,下面也有一些相关的解析

class SSLPerson {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4main1tAA9SSLPersonCvp          // id: %2
  %3 = global_addr @$s4main1tAA9SSLPersonCvp : $*SSLPerson // user: %7
  %4 = metatype $@thick SSLPerson.Type            // user: %6
  // function_ref SSLPerson.__allocating_init()
  %5 = function_ref @$s4main9SSLPersonCACycfC : $@convention(method) (@thick SSLPerson.Type) -> @owned SSLPerson // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick SSLPerson.Type) -> @owned SSLPerson // user: %7
  store %6 to %3 : $*SSLPerson                    // id: %7
  %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'

// SSLPerson.__allocating_init()
sil hidden [exact_self_class] @$s4main9SSLPersonCACycfC : $@convention(method) (@thick SSLPerson.Type) -> @owned SSLPerson {
// %0 "$metatype"
bb0(%0 : $@thick SSLPerson.Type):
  %1 = alloc_ref $SSLPerson                       // user: %3 去堆区申请内存空间
  // function_ref SSLPerson.init()
  %2 = function_ref @$s4main9SSLPersonCACycfc : $@convention(method) (@owned SSLPerson) -> @owned SSLPerson // user: %3
  %3 = apply %2(%1) : $@convention(method) (@owned SSLPerson) -> @owned SSLPerson // user: %4
  return %3 : $SSLPerson                          // id: %4
} // end sil function '$s4main9SSLPersonCACycfC'
  • @main:入口函数
  • %0:寄存器,虚拟的
  • sil 语法规则:github.com/apple/swift…
  • 这段代码的大概意思是解析 SSLPerson 类,去堆区申请内存,并调用了关键函数__allocating_init(),接下来会通过 Swift 源码进一步进行分析。

3.3 断点 汇编分析

对下面的代码进行打断点,查看汇编

class SSLPerson {
    var age: Int = 18
    var name: String = "ssl"
    }

var t = SSLPerson()

image.png

class SSLPerson : NSObject {
    var age: Int = 18
    var name: String = "ssl"
}

var t = SSLPerson()

image.png

  • 找到关键函数 swift_allocObject,下面进行源码的分析。

3.4 Swift源码分析

Swift源码下载地址:github.com/apple/swift

搜索 HeapObject.cpp 文件,找到 swift_allocObject 函数:

HeapObject *swift::swift_allocObject(HeapMetadata const *metadata,
                                     size_t requiredSize,
                                     size_t requiredAlignmentMask) {
  CALL_IMPL(swift_allocObject, (metadata, requiredSize, requiredAlignmentMask));
}

swift_allocObject 中调用了 _swift_allocObject_ 函数:

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));

  ...
}

_swift_allocObject_ 中又调用了 swift_slowAlloc

void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
  void *p;
  #if defined(__APPLE__) && SWIFT_STDLIB_HAS_DARWIN_LIBMALLOC
    p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
    p = malloc(size);
    ...
}
  • 可以看到 Swift 中也调用了 malloc 函数。

由此我们可以得到 Swift 对象的内存分配:

  • __allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> Malloc
  • Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是 Metadata ,一个是 RefCount ,默认占用 16 字节大小。
    struct HeapObject {
      HeapMetadata const * metadata;
      InlineRefCounts refCounts;
    }
    

objc_object 只有一个 isa,HeapObject 缺有两个属性,接下来我们对 Metadata 进行探索

四、类的结构探索

4.1 Metadata 源码分析

Metadata 是 HeapMetadata 类型,查看 HeapMetadata

struct InProcess;

template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;

这是一个类似别名的定义,下面查看 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
  • 在初始化方法中,当是纯 Swift 类时传入了 MetadataKind,如果和 OC 交互时,它就传了一个 isa

4.2 MetadataKind 源码分析

源码查看 MetadataKind 可以看到它是一个 uint32_t 类型

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"
  LastEnumerated = 0x7FF,
};

和 Swift 中的类型关联表如下:

image.png

4.3 类结构源码分析

查看 TargetHeapMetadata 的父类 TargetMetadata:

struct TargetMetadata {

using StoredPointer = typename Runtime::StoredPointer;

private:
  StoredPointer Kind;
}
  • 可以看到 TargetMetadata 中有一个 Kind 成员变量,继续向下分析

我们找到 ConstTargetMetadataPointer 函数:

ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor>
  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;
    }
  }
  • 可以看到当 kidn 是 Class 时,会将 this 强转为 TargetClassMetadata 类型。

查看 TargetClassMetadata :

struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
    ...
}

struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
    TargetSignedPointer<Runtime, const TargetClassMetadata<Runtime> *superclass;
    TargetPointer<Runtime, void> CacheData[2];
    StoredSize Data;
    ...
}

在 TargetAnyClassMetadata 中我们可以找到 superclass、Data、CacheData等成员变量,经过上面一系列的分析,我们可以得到 swift 类的数据结构如下

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
}