iOS-Swift 独孤九剑:一、结构体与类

7,141 阅读8分钟

一、结构体

在 Swift 的标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。比如 BoolIntDoubleStringArrayDictionary 等常见类型都是结构体。

我们现在定义一个结构体。

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

let p = SHPerson(age: 18, name: "Coder_张三")

所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)。如上代码,可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)。

1. 结构体的初始化器

编译器根据情况,可能会为结构体生成多个初始化器。前提是保证 所有成员都有初始值。

结构体初始化器.png

2. 自定义初始化器

一但在定义结构体时自定义初始化器,编译器就不会帮自动生成其他初始化器。

struct SHPerson {
    var age: Int
    var name: String

    init(age: Int) {
        self.age = age
        self.name = "Coder_张三"
    }
}

let p = SHPerson(age: 0)

在对结构体进行初始化的时候,必须保证结构体的成员都值,所以当我们对结构体的某个成员变量设置初始值时,生成的初始化器可以不用传该成员变量的参数赋值。

3. 结构体的内存结构

我们来看下面这个结构体,SHPerson 有 age,weight,sex 三个成员。

struct SHPerson {
    var age: Int
    var weight: Int
    var sex: Bool
}

print(MemoryLayout<SHPerson>.size)         // 17
print(MemoryLayout<SHPerson>.stride)       // 24
print(MemoryLayout<SHPerson>.alignment)    // 8

打印出其内存对齐字节数和占用的内存,在 64 位系统下,结构体中 Int 占 8 字节,Bool 占一个字节 ,所以 SHPerson 一共占 17 个字节,但是因为要遵守内存对齐原则(8个字节),所以系统会分配 24 个字节来存储 SHPerson

二、类

类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器。

1. 指定初始化器

当类的成员没有初始值时,必须自定义初始化器,初始化成员值。

class SHPerson {
    var age: Int
    var name: String

    init(age: Int, name: String) {
        self.age = age;
        self.name = name;
    }
}

let p = SHPerson(age: 18, name: "Coder_张三")

如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器,成员的初始化是在这个初始化器中完成的。

class SHPerson {
    var age: Int = 18
    var name: String = "Coder_张三"
}

let p = SHPerson()

2. 可失败初始化器

当初始化的值不满足某个条件时我们需要给初始化方法返回一个nil,那么可以在 init 后面加上一个可选项来修饰。

class SHPerson {
    var age: Int
    var name: String

    init?(age: Int, name: String) {
        if age < 18 { return nil}
        self.age = age
        self.name = name
    }
}

let p1 = SHPerson(age: 16, name: "Coder_ 张三")
let p2 = SHPerson(age: 18, name: "Coder_李四")
print("p1 - \(String(describing: p1))")
print("p2 - \(String(describing: p2))")
打印结果:
p1 - nil
p2 - Optional(_1_结构体与类.SHPerson)

例如,当 SHPerson 不满 18 岁时返回 nil,属于未成年人。

3. 必要初始化器

必要初始化器需要在 init 前用 required 修饰。

class SHPerson {
    var age: Int
    var name: String
    // 父类定义必要实现初始化器
    required init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

class SHStudent: SHPerson {
    var height: Int

    init(height: Int) {
    self.height = height
        super.init(age: 18, name: "Coder_ 张三")
    }

    // 子类必须实现父类的必要初始化器
    required init(age: Int, name: String) {
        fatalError("init(age:name:) has not been implemented")
    }
}

如代码所示,当在 init 前修饰 required,该类的子类都必须实现该初始化器。

4.便捷初始化器

我们可以为类提供一个便捷初始化器,便捷初始化器需要在 init 前用 convenience 修饰。

class SHPerson {
    var age: Int
    var name: String

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

    convenience init() {
        self.init(age: 18, name: "Coder_ 张三")
    }
}

如代码所示,便捷初始化器必须从相同的类里调用另一个初始化器,并且最终必须调用一个指定初始化器。

三、结构体与类的本质区别

结构体与类的本质区别为结构体是值类型,类是引用类型(其实也可以理解为指针类型)。那么它们还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在栈上,引用类型存储在堆上

class SHPerson {
    var age = 18
    var height = 180
}

struct SHPoint {
    var x = 0;
    var y = 0;
}

func test() {
    let point = SHPoint()
    let person = SHPerson()
}

内存结构.png

SHPoint 在初始化完成赋值给 point 后,SHPoint 的内存数据直接就是放在栈空间。而 SHPerson 在初始化完成赋值给 person 后,person 只是一个引用地址,这个地址存的内存数据为 SHPerson 的内存地址,该内存地址放在堆空间。

四、值类型

值类型赋值给 varlet 或者给函数传参,是直接将所有内容拷贝一份。类似于对文件进行 copy、paste操作,产生了全新的文件副本。属于深拷贝(deep copy)。

struct SHPoint {
    var x = 4;
    var y = 8;
}

var p1 = SHPoint()
var p2 = p1;

p2.x = 6

print("p1 - \(p1)")
print("p2 - \(p2)")
打印结果:
p1 - SHPoint(x: 4, y: 8)
p2 - SHPoint(x: 6, y: 8)

我们可以看到在修改 p2 的 x 后,对 p1 并没有影响,这属于深拷贝。 我们来看数组的打印结果。

var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1)
print(a2)
打印结果:
[2, 2, 3]
[1, 2, 3, 4]

在 Swift 标准库中,为了提升性能,StringArrayDictionarySet 采取了Copy On Write 的技术, 比如仅当有“写”操作时,才会真正执行拷贝操作。

对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值。

建议:不需要修改的,尽量定义成 let

五、引用类型

引用赋值给var、let或者给函数传参,是将内存地址拷贝一份。类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝(shallow copy)。

class SHPerson {
    var age: Int = 18
    var name: String = "Coder_张三"
}

let p1 = SHPerson()
let p2 = p1

print("p1-age: \(p1.age)")
p2.age = 20
print("p1-age: \(p1.age)")
print("p2-age: \(p2.age)")
打印结果:
p1-age: 18
p1-age: 20
p2-age: 20

对象的堆空间申请过程:

在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下:

  1. Class.__allocating_init()
  2. libswiftCore.dylib: swift_allocObject
  3. libswiftCore.dylib: swift_slowAlloc
  4. libsystem_malloc.dylib: malloc

在Mac、iOS中的 malloc 函数分配的内存大小总是16 的倍数。

  • class_getInstanceSize: 返回类实例的大小。
  • malloc_size:系统分配的内存大小。
class CGPoint  {
var x = 11
var y = 22
var test = true
}
var p = CGPoint()
print(class_getInstanceSize(CGPoint.self))
print(malloc_size(unsafeBitCast(p, to: UnsafeRawPointer.self)))
打印结果:
40
48
  • 通过打印得知,CGPoint 的大小为 40 个字节,系统分配 CGPoint 的内存大小为 48 个字节。

  • CGPoint 中,x 占 8 个字节,y 占 8 个字节,test 占 1 个字节,所以目前我们看到的有 17 个字节。但是因为类存储在堆空间中,它前面会有 8 个字节存放类型信息,8个字节存引用计数,再加上面的,加起来一共是 33 个字节,根据内存对齐原则(8 个字节),所以 CGPoint 的大小为 40 个字节。

  • 因为在Mac、iOS中的 malloc 函数分配的内存大小总是16 的倍数,所以最终系统会分配 CGPoint 的内存大小为 48 字节。

六、结构体与类的选择

结构体与类的使用方式很相似,那么在平时开发中使用结构体比较好还是类比较好呢?这种时候分情况,如果定义的数据结构比较简单的情况下,建议用结构体,比如 Model。如果定义的数据结构比较复杂的话,建议用类,比如需要用到多态的时候。

  • 结构体的内存分配在在栈空间,当结构体用完时,会自动释放内存,不需要进行额外的处理。
  • 类的内存分配在堆空间,系统需要对类的内存大小进行分配以及析构等操作,相对于结构体,性能会有所消耗。

StructVsClassPerformance demo 测试:

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

具体的代码就不去贴出来了,我们来看一下调用方式以及打印结果:

Tests.runTests()
打印结果:
Running tests

class (1 field)
9.566281178005738

struct (1 field)
6.391943185008131

class (10 fields)
10.430800677015213

struct (10 fields)
6.610909776005428

通过打印结果可以直观的看到,结构体对比类的时间分配时要快将近一倍的速度。

七、Swift 原理拓展

1. Swift 代码编译流程

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

LLVM编译过程.png

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

Swift 编译成 IR.png

  1. 首先 Swift Code 经过 -dump-parse 进行语义分析解析成 Parse(抽象语法树)。
  2. Parse 经过 -dump-ast 进行语义分析分析语法是否正确,是否安全。
  3. Seam 之后会把 Swift Code 会降级变成 SILGen(Swift 中间代码),对于 SILGen 又分为原生的(Raw SIL)和经过优化的(SIL Opt Canonical SIL)。
  4. 优化完成的 SIL 会由 LLVM 降级成为 IR,降级成 IR 之后由后端代码编译成机器码。

以上就是 Swift 的编译流程,下面为编译流程的命令。

分析输出 AST:

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

将以下代码编译成 sil 代码:

import Foundation

class SHPerson {
var age = 18
var name = "Coder_张三"
}

let p = SHPerson()

终端 cd 到项目的 main.swift 目录,输入 swiftc main.swift -emit-sil 并按回车键,会在生成一个 main.sil 文件,并且会在终端输出 SIL 代码。 SIL 代码如下:

SIL代码.png

关于 SIL 的语法说明,其实也有相应的文档。这里贴上文档说明的地址:SIL参考文档

2. 汇编探索类的初始化流程

接下来我们通过汇编来查看类的初始化流程,我们打个断点如下:

SHPerson 断点.png

接下来打开汇编调试。

汇编调试入口.png

汇编调用第一个初始化方法.png

通过汇编查看,SHPerson 在进行初始化的时候,在底层会调用 __allocating_init 的函数,那么 __allocating_init 做了什么事情呢,跟进去看一下。

让断点走到 __allocating_init 这一行代码,按住 control 键,点击这个向下的按钮。

断点跟进__allocating_init.png

__allocating_init 的内部实现.png

可以看到,进入到 __allocating_init 的内部实现后,发现它会调用一个 swift_allocObject 函数,那么在继续跟汇编的时候跟丢了。

接下来我们来看一下源码。源码可以去苹果官网下-swift源码下载地址。用 VSCode 打开下载好的 swift 源码,全局搜索 swift_allocObject 这个函数。

HeapObject.cpp 文件中找到 swift_allocObject 函数的实现,并且在 swift_allocObject 函数的实现上方,有一个 _swift_allocObject_ 函数的实现。

// 第一个参数,元数据。
// 第二个参数,分配内存的大小
// 第三个参数,内存对齐,值一般为 7,因为遵守8字节对齐
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_slowAlloc 函数,我们来看下 swift_slowAlloc 函数的内部实现:

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

swift_slowAlloc 函数的内部是去进行一些分配内存的操作,比如 malloc。所以就印证了第四点引用类型->对象申请堆空间的过程。

3. Swift 类的源码结构

3.1. OC 与 Swift 的区分调用

在调用 _swift_allocObject_ 函数的时候有一个参数,名为 metadataHeapMetadata。以下是 HeapMetadata 跟进的代码过程:

//  HeapMetadata 为 TargetHeapMetadata 的别名,InProcess 为泛型。
using HeapMetadata = TargetHeapMetadata<InProcess>;
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
};

在这里有对 OC 和 Swift 做兼容。调用的 TargetHeapMetadata 函数的时候,如果是 OC 的类,那么参数为 isa 指针,否则就是一个 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"
  
  /// The largest possible non-isa-pointer metadata kind value.
  ///
  /// This is included in the enumeration to prevent against attempts to
  /// exhaustively match metadata kinds. Future Swift runtimes or compilers
  /// may introduce new metadata kinds, so for forward compatibility, the
  /// runtime must tolerate metadata with unknown kinds.
  /// This specific value is not mapped to a valid metadata kind at this time,
  /// however.
  LastEnumerated = 0x7FF,
};

那么 MetadataKind 的种类如下:

name                       Value

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

3.2. Swift 类底层的源码结构

接下来我们找到 TargetHeapMetadata 的继承 TargetMetadata(在 C++ 中结构体是允许继承的)。在 TargetMetadata 结构体中找到了 getTypeContextDescriptor 函数,代码如下:

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

可以看到,当 kind 是一个 Class 的时候,会拿到一个名为 TargetClassMetadata 的指针,我们看看 TargetClassMetadata 的实现:

swift 类的本质.png

终于看到熟悉的东西了,我们在看看它的继承 TargetAnyClassMetadata 结构体,可以看到有 superclassisa 等。

Swift 类的本质 2.png

3.3. Swift 类底层的源码结构

通过以上的分析,我们可以得出,Swift 类中的 metadata 数据结构大致如下:

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
}

接下来我们做一个测试,通过 lldb 查看 Swift 类的内存结构,那么既然在 Swift 的底层,_swift_allocObject_ 函数返回的是 HeapObject 的指针类型,我们来看一下 HeapObject 的结构:

// The members of the HeapObject header that are not shared by a
// standard Objective-C instance
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

/// The Swift heap-object header.
/// This must match RefCountedStructTy in IRGen.
 struct HeapObject {
   /// This is always a valid pointer to a metadata object.
   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)
   { }

 #ifndef NDEBUG
   void dump() const SWIFT_USED;
 #endif

 #endif // __swift__
 };

知道了 HeapObject 的源码结构之后,我们也假里假气的模仿源码,自己定义一个 HeapObjectrefCounts 可以先忽略,不管,主要看 metadata

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refCounts: UInt32
}

接下来我们将 SHPerson 类转成 HeapObject 结构体,通过 lldb 打印,查看其内存结构。

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

let p = SHPerson()

// 将 SHPerson 转成 HeapObject 指针
let p_raw_ptr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)
// 将 p_ptr 指针转成 HeapObject 的指针类型并打印出 HeapObject 的内存结构
print(p_ptr.pointee)
打印结果:
HeapObject(metadata: 0x00000001000081a0, refCounts: 3)

(lldb) x/8g 0x00000001000081a0
0x1000081a0: 0x0000000100008168 0x00007fff806208f8
0x1000081b0: 0x00007fff20208aa0 0x0000803000000000
0x1000081c0: 0x00000001085040f2 0x0000000000000002
0x1000081d0: 0x0000000700000028 0x00000010000000a8

(lldb) x/8g 0x0000000100008168
0x100008168: 0x00007fff80620920 0x00007fff80620920
0x100008178: 0x00007fff20208aa0 0x0000a03100000000
0x100008188: 0x0000000108504090 0x00000001000032b0
0x100008198: 0x00007fff8152f3e0 0x0000000100008168
(lldb)

通过打印,得知,Swift 类的本质就是 HeapObject 的结构体指针,并且,我们将其内存布局以 x/8g 的形式打印出来。

接下来我需要打印出 HeapObjectmetadata 的内存结构,来试一下:

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
}

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refCounts: UInt32
}

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

let p = SHPerson()

let p_raw_ptr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)
// 我们将 HeapObject 中的 metadata 绑定成 Metadata 类型,并转成 Metadata 的指针类型,那么数据类型的大小可以用 MemoryLayout 测量出来。
let metadata = p_ptr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee
print(metadata)
打印结果:
Metadata(kind: 4295000432, 
         superClass: _TtCs12_SwiftObject, 
         cacheData: (140733732391584, 140943646785536), 
         classFlags: 2, 
         instanceAddressPoint: 0, 
         instanceSize: 40, 
         instanceAlignmentMask: 7, 
         reserved: 0, 
         classSize: 168, 
         classAddressPoint: 16, 
         typeDescriptor: 0x0000000100003c6c, 
         iVarDestroyer: 0x0000000000000000)
(lldb)

我们成功的打印出 kindsuperClasscacheData 等成员变量的值。