[Swift进阶]类与结构体的探究(上)

4,211 阅读9分钟

本文学习和实践了Swift当中的类(Class)与结构体(Struct)。包含基本语法,异同点。类的初始化和生命周期。结合Swift源码验证真相。

类和结构体的异同

分别定义一个class和struct:

// 父类
class CPerson {
    func getWeight(){
        
    }
}
// 协议
protocol PersonIntroProtocol {
    static func introSelf()
}

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

extension STeacher: PersonIntroProtocol {
    static func introSelf() {
        print("STeacher")
    }
}

class CTeacher: CPerson{
    var age: Int
    var name: String
    // class才能继承
    override func getWeight(){
    }
    
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    
    deinit {
        
    }
}

extension CTeacher: PersonIntroProtocol {
    // class和struct都能遵循协议
    static func introSelf() {
        print("CPerson")
    }
}
  • 内存对齐原则: 属性往往有不同的数据类型,比如int,boolean占的字节数不同。系统会分配同样的内存大小来使用。
结构体和类的主要相同点
  • 定义存储值的属性
  • 定义方法
  • 定义下标以使用下标语法提供对其值的访问
  • 定义初始化器
  • 使用 extension 来拓展功能
  • 遵循协议来提供某种功能
主要的不同点
  • 类有继承的特性,而结构体没有
  • 类型转换使您能够在运行时检查和解释类实例的类型
  • 类有析构函数用来释放其分配的资源。也就是deinit方法
  • 引用计数允许对一个类实例有多个引用。struct是值类型,存在栈上,无需引用。

因此引申出值类型(struct)和引用类型(class)的区别。

值类型和引用类型

首先我们对内存区域来一个基本概念的认知: 内存有一部分是系统自己使用的,剩下的才是我们可以操作的。这其中又分为: 栈区(stack): 局部变量和函数运行过程中的上下文。 堆区(Heap): 存储所有对象。也就是通常用new和malloc方法生成的。 全局区(Global): 存储全局变量;常量;代码区;

单独介绍Mach-O文件:Mach-O文件有多个段( Segment ),每个段有不同的功能。然后每个段又分为很多小的Section.例如:

// 格式:Segment.Section: 
TEXT.text : 机器码 
TEXT.cstring : 硬编码的字符串 
TEXT.const: 初始化过的常量 
DATA.data: 初始化过的可变的(静态/全局)数据 
DATA.const: 没有初始化过的常量 
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common: 没有初始化过的符号声明
查看内存结构

一般情况,值类型存在栈上,引用类型存储在堆上。通过lldb指令来查看strcut和class的内存分布情况: 指令:

frame variable -L <variable_name>

实例化:

var t0 = STeacher(age: 18, name: "test name")
var t1 = CTeacher(age: 18, name: "test name")

指令输出:

(lldb) frame variable -L t0
0x000000010000c4c8: (SwiftTest.STeacher) t0 = {
0x000000010000c4c8:   age = 18
0x000000010000c4d0:   name = "test name"
}
(lldb) frame variable -L t1
scalar: (SwiftTest.CTeacher) t1 = 0x0000000101a38a40 {
scalar:   SwiftTest.CPerson = {}
0x0000000101a38a50:   age = 18
0x0000000101a38a58:   name = "test name"
}

还可以借助以下指令来查看当前变量的内存结构: po:只会输出对应的值 p:返回值的类型以及命令结果 的引用名 x/8g:读取内存中的值(x:读取;8:连续打印8段;g:8字节格式输出)

// 控制台使用lldb命令输出
(lldb) frame variable -L t0
0x000000010000c638: (SwiftTest.STeacher) t0 = {
0x000000010000c638:   age = 18
0x000000010000c640:   name = "name"
}
(lldb) x/8g 0x000000010000c638
// 左边是内存地址,计数+1代表1字节;字节是最小储存单位
// 右边内存,每段是16位16进制数,1字节=8位二进制数=2位16进制,所以每段8字节,共16字节;
// 对应左边地址每行计数正好相差16。其中0x0000000000000012对应age=18的值
// 从第四段开始没值了,说明字符串也就占了16字节
0x10000c638: 0x0000000000000012 0x00000000656d616e
0x10000c648: 0xe400000000000000 0x0000000000000000
0x10000c658: 0x0000000000000000 0x0000000000000000
0x10000c668: 0x0000000000000000 0x0000000000000000

类的初始化器

指定初始化器
  • Swift中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。所以要提供对应的指定初始化器,也就是开头CTeacher的init()方法。 根据参数的不同,可以组合成多个指定的初始化器。
  • 当前的类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)
便捷初始化器

多个指定初始化器之间逻辑独立,如果想要每个初始化器都经过一个默认的初始化器时,就涉及到便捷初始化器convenience。

    convenience init(name: String) {
        // 必须先调用一个指定初始化器,
        self.init(age: 18, name:"nil")
        // 再补充自己的逻辑
        self.name = name
    }

这里我们记住:

  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
  • 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
可失败初始化器

实际场景中因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。init后面加"?"号表示可失败

    init?(age: Int, name: String) {
        // 比如未成年不让当教师.
        if age < 18 { return nil}
        self.age = age
        self.name = name
    }
必要初始化器

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

class CTeacher: CPerson{
    var age: Int
    var name: String
    
    required init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

class CCTeacher: CTeacher{
    required init(age: Int, name: String) {
        super.init(age: 0, name: "")
        self.age = age
        self.name = name
    }
}

类的生命周期

不管是OC还是Swift后端都是通过LLVM进行编译的。

  • OC通过 clang c compiler 编译器生成 LLVM IR文件;
  • swift通过 Swift compiler 编译器生成 LLVM IR文件;
  • 最后都通过LLVM生成可执行文件.o(这里也就是我们的机器码)
Swift详细编译流程
  • Swift code swift代码
  • Parse Parse解析器是一个简易的递归下降解析器(在 lib/Parse 中实现),并带有完整手动编码的词法分析器。通过parse进行词法分析
  • Sema Semantic Analysis语义分析阶段(在 lib/Sema 中实现)负责获取已解析的 AST(抽象语法树)并将其转换为格式正确且类型检查完备的 AST,以及在源代码中提示出现语义问题的警告或错误。语义分析包含类型推断,如果可以成功推导出类型,则表明此时从已经经过类型检查的最终 AST 生成代码是安全的; Clang 导入器(Clang Importer),(在 lib/ClangImporter 中实现)负责导入 Clang 模块,并将导出的 C 或 Objective-C API 映射到相应的 Swift API 中。最终导入的 AST 可以被语义分析引用
  • SILGen
  • RAW SIL SIL 生成(SIL Generation):Swift 中间语言(Swift Intermediate Language,SIL)是一门高级且专用于 Swift 的中间语言,适用于对 Swift 代码的进一步分析和优化。SIL 生成阶段(在 lib/SILGen 中实现)将经过类型检查的 AST 弱化为所谓的「原始」SIL。SIL 的设计在 docs/SIL.rst 有所描述。这个过程生成RAW SIL(原生SIL,代码量很大,不会进行类型检查,代码优化)
  • SIL Opt Canonical SIL SIL 优化(SIL Optimizations):SIL 优化阶段(在 lib/Analysis、lib/ARC、lib/LoopTransforms 以及 lib/Transforms 中实现)负责对程序执行额外的高级且专用于 Swift 的优化,包括(例如)自动引用计数优化、去虚拟化、以及通用的专业化。通过-emit-sil命令生成优化过后的 SIL Opt Canonical SIL。这个也是我们一般阅读的SIL代码;
  • IRGen
  • LLVM IR LLVM IR 生成(LLVM IR Generation):IR 生成阶段(在 lib/IRGen 中实现)将 SIL 弱化为 LLVM LR,此时 LLVM 可以继续优化并生成机器码。 通过IRGen生成 IR;
  • Machine code 最后生成机器码交给机器进行识别。

各步骤编译命令

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

Swift对象内存分配
  • 通过debug追踪方法调用。

xcode工具栏-->Debug-->Debug Workflow-->Always Show Disassembly 运行代码,通过debug断点,能找到包含__allocating_init方法这行,设为断点,并走到这行;

0x10000395c <+108>: callq  0x100004160 ; SwiftTest.CTeacher.__allocating_init(age: Swift.Int, name: wift.String) -> SwiftTest.CTeacher at main.swift:41

然后按住Ctrl键,点击控制台上的Step Into按钮进入方法内部,能看到包含swift_allocObject这行;以此类推一直找下去;我把流程合在一起展示:

// SwiftTest`CTeacher.__allocating_init(age:name:):
0x10000418b <+43>: callq  0x1000075e2 ; symbol stub for: swift_allocObject
// libswiftCore.dylib`swift_allocObject:
0x7ff82a3aeed0 <+16>: leaq   0x2399(%rip), %rcx ; _swift_allocObject_
0x7ff82a3aeee2 <+34>: callq  0x7ff82a3aed50 ; swift_slowAlloc
// libswiftCore.dylib`swift_slowAlloc:
0x7ff82a3aed73 <+35>:  callq  0x7ff82a4311e6 ; symbol stub for: malloc_zone_malloc
// libsystem_malloc.dylib`_malloc_zone_malloc:
  • 通过源码追踪方法调用:

Swift官方源码是开源的: 下载地址 下载后,用VSCode工具打开,搜索swift_allocObject或者HeapObject.cpp文件,发现HeapObject;

HeapObject *swift::swift_allocObject(HeapMetadata const *metadata,
                                     size_t requiredSize,
                                     size_t requiredAlignmentMask) {
  CALL_IMPL(swift_allocObject, (metadata, requiredSize, requiredAlignmentMask));
}
// 
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;
}
// 
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__) && SWIFT_STDLIB_HAS_DARWIN_LIBMALLOC
    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;
}

一路追寻方法调用直到最后,发现熟悉的malloc几个方法,一般情况对象也就生成完了。 总结起来,流程就是: __allocating_init -----> swift_allocObject -----> swift_allocObject -----> swift_slowAlloc -----> Malloc

Swift对象的内存结构

HeapObject (对应OC中的objc_object) ,有两个属性: 一个是 Metadata ,一个是 RefCount ,默认占用 16 字节大小。

struct HeapObject {
  HeapMetadata const * metadata;
  InlineRefCounts refCounts;
}

而OC却只有一个孤独的isa

objc_object{ 
    isa 
}

从上文源码的入参开始追踪HeapMetadata:

namespace swift {

struct InProcess;

template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;
#else
typedef struct HeapMetadata HeapMetadata;
typedef struct HeapObject HeapObject;
#endif
...
}

查看TargetHeapMetadata, 其中SWIFT_OBJC_INTEROP代表继承OC的类所使用的初始化,你能看到熟悉的isa指针。

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

那么入参为MetadataKind类型的就是Swift所使用的的方法了。查看MetadataKind:

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

那么所有类型都在#include "MetadataKind.def"这个文件里了,整理了一下所有类型:

NameValue
Class0x0
Struct0x200
Enum0x201
Optional0x202
ForeignClass0x203
ForeignClass0x203
Opaque0x300
Tuple0x301
Function0x302
Existential0x303
Metatype0x304
ObjCClassWrapper0x305
ExistentialMetatype0x306
HeapLocalVariable0x400
HeapGenericLocalVariable0x500
ErrorObject0x501
LastEnumerated0x7FF

书接上文, 在查看TargetClassMetadata类定义的时候发现一段注释,原来官方已经透露了:

The structure of all class metadata. 所有类元数据的结构

/// The structure of all class metadata.  This structure is embedded
/// directly within the class's heap metadata structure and therefore
/// cannot be extended without an ABI break.
///
/// Note that the layout of this type is compatible with the layout of
/// an Objective-C class.
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
  using StoredPointer = typename Runtime::StoredPointer;
  using StoredSize = typename Runtime::StoredSize;

  TargetClassMetadata() = default;
  constexpr TargetClassMetadata(const TargetAnyClassMetadata<Runtime> &base,
             ClassFlags flags,
             ClassIVarDestroyer *ivarDestroyer,
             StoredPointer size, StoredPointer addressPoint,
             StoredPointer alignMask,
             StoredPointer classSize, StoredPointer classAddressPoint)
    : TargetAnyClassMetadata<Runtime>(base),
      Flags(flags), InstanceAddressPoint(addressPoint),
      InstanceSize(size), InstanceAlignMask(alignMask),
      Reserved(0), ClassSize(classSize), ClassAddressPoint(classAddressPoint),
      Description(nullptr), IVarDestroyer(ivarDestroyer) {}

  // 忽略更多代码
};
using ClassMetadata = TargetClassMetadata<InProcess>;

简化结构如下:

struct Metadata{
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var flags: ClassFlags
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer
    var iVarDestroyer: UnsafeRawPointer
}