深入分析Swift 类与结构体的本质

928 阅读9分钟

Swift本身是一门高效、安全的语言,怎么理解?通过对Swift类与结构体的比较,深刻理解这两种类型,运用好类与结构体,写出高效安全的代码。

对于开发过程中类与结构体的选择,Apple有给出建议, 参考 Choosing Between Structures and Classes
  • 默认使用结构体
  • 需要Objective-C互交互时使用类
  • 需要共享实例时使用类【如单例、通过对象引用标识修改属性后被共享】
  • 使用结构体遵循协议来共享协议实现的方法

1. Swift 类与结构体的区别

classstruct备注
类型引用类型值类型引用类型有共享实例的特性;值类型有assign to copy赋值拷贝特性
支持继承YESNO
支持OC调用YES (需继承 NSObject)NO
创建速度struct创建比class快1/3左右参考Demo
内存管理引用类型(ARC)值类型(赋值拷贝)
内存分配位置堆(Heap)栈(Stack)由于栈的访问速度大于堆,所以值类型的创建和访问速度大于引用类型
多线程安全NO 堆的访问不是多线程安全YES 栈访问是多线程安全String, Array, Dictionary虽然是struct,数据存储在struct的一个class属性中,支持copy-on-write ,所以并不是线程读写安全
函数派发方式表派发/静态派发/消息派发静态派发
内存占用class默认会继承SwiftObject, 实例中包含metadata, refCounts等信息, 而struct只是单纯的数据结构

2. Swift 类与结构体的相同点

  • 编译器 LLVM swiftc 【OC 的编译器是 LLVM clang】
  • 支持定义初始化器【struct编译器默认生成一个初始化器,class需要手动写】
  • 支持定义属性和方法
  • 支持extension扩展方法
  • 支持遵循协议

3. 类的初始化流程

为什么类创建比结构体慢,而且占用空间比结构体多,这里看看类的初始化流程。

3.1 继承NSObject的类

class Student: NSObject {
    let id: Int
    let name: String
    init(id: Int, name: String) {
        self.id = id
        self.name = name
        super.init()
    }
}
let student = Student(id: 20, name: "Lili")

断点到__let student = Student(id: 20, name: "Lili")__这行代码,然后查看汇编 Xcode —> Debug —> Debug WorkFlow —> Always Show Disassembly

__allocating_init

imageobjc_allocWithZonepng

objc_msgSendSuper2

从上面的断点跟踪中可以断定出继承NSObject的类初始化流程为 Student.__allocating_init --> objc_allocWithZone -->objc_msgSendSuper2, __objc_allocWithZone__就比较熟悉了,这时进入OC的对象初始化流程。

3.1 纯Swift类

class Student {
    let id: Int
    let name: String
    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}
let student = Student(id: 20, name: "Lili")

断点到__let student = Student(id: 20, name: "Lili")__这行代码,然后查看汇编 Xcode —> Debug —> Debug WorkFlow —> Always Show Disassembly

SwiftTest.Student.__allocating_init

swift_allocObject

swift_slowAlloc

malloc_zone_malloc

从上面的断点跟踪中可以断定出纯Swift类初始化流程为 Student.__allocating_init --> swift_allocObject --> swift_slowAlloc --> malloc_zone_malloc

3.2 纯Swift类源码导读

  • 在HeapObject.cpp文件找到了swift_allocObject的定义, swift_allocObject会调用swift_slowAlloc函数生成HeapObject对象
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);
  return object;
}
  • 我们看看swift_allocObject做了什么事情,简单的调用malloc_zone_malloc函数开辟内存空间, 参考HeapObject.cpp文件
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;
}

源码导读的结果与汇编Debug得出的结果一致

4. 类的实例结构分析

4.1 HeapObject

上面一章节查看Swift源码,初始化流程中,返回了一个heapObject类型,源码分析,swift类实例的内存结构是个HeapObject,HeapObject有两个属性,一个是metadata,一个是refCounts。见HeapObject.h文件

struct HeapObject {
  HeapMetadata const * metadata;
  InlineRefCounts refCounts   // 占8字节
} 
  • InlineRefCounts 定义在RefCount.h文件,
typedef struct {
  __swift_uintptr_t refCounts; // 占8字节,  typedef uintptr_t __swift_uintptr_t;
} InlineRefCountsPlaceholder;
typedef InlineRefCountsPlaceholder InlineRefCounts;
  • HeapMetadata 定义在Metadata.h文件中, HeapMetadata 继承于TargetMetadata
using HeapMetadata = TargetHeapMetadata<InProcess>;   // 表示HeapMetadata是TargetHeapMetadata<InProcess>的别名
struct TargetHeapMetadata : TargetMetadata<Runtime>  // 表示TargetHeapMetadata继承于TargetMetadata<Runtime>
  • TargetHeapMetadata定义在Metadata.h文件中,通过MetadataKind 进行初始化
/// The common structure of all metadata for heap-allocated types.  A
/// pointer to one of these can be retrieved by loading the 'isa'
/// field of any heap object, whether it was managed by Swift or by
/// Objective-C.  However, when loading from an Objective-C object,
/// this metadata may not have the heap-metadata header, and it may
/// not be the Swift type metadata for the object's dynamic type.
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}  // Swift通过`MetadataKind `的初始化函数
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa) 
    : TargetMetadata<Runtime>(isa) {} // 继承NSObject的对象通过isa指针进行初始化的函数
#endif
};
  • MetadataKind 的定义在MetadataValues.h文件,是一个enum class类型, 会导入MetadataKind.def文件, MetadataKind.def文件定义了所有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" // `MetadataKind.def`文件定义了所有`MetadataKind`
  
  /// 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定义,得到如下一个表, 可以推断出,调用TargetHeapMetadata(MetadataKind kind)函数时,如果kind == MetadataKind::Class, 表述初始化一个class;
  • TargetHeapMetadata(MetadataKind kind) 函数传入不同的kind类型会得到不同的Meta数据结构
namevalue
namevalue
Class0x0
Struct0x200
Enum0x201
Optional0x202
ForeignClass0x203
Opaque0x300
Tuple0x301
Function0x302
Existential0x303
Metatype0x304
ObjCClassWrapper0x305
ExistentialMetatype0x306
HeapLocalVariable0x400
HeapGenericLocalVariable0x500
ErrorObject0x501
LastEnumerated0x7FF
  • TargetHeapMetadata继承TargetMetadata, 在TargetMetadata找到这个函数,当kind == MetadataKind::Class时,得到TargetClassMetadata类型
  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;
    }
  }
  • TargetClassMetadata 是所有类基类,TargetClassMetadata 继承TargetAnyClassMetadata
/// 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) {}

  // The remaining fields are valid only when isTypeMetadata().
  // The Objective-C runtime knows the offsets to some of these fields.
  // Be careful when accessing them.

  /// Swift-specific class flags.
  ClassFlags Flags;

  /// The address point of instances of this type.
  uint32_t InstanceAddressPoint;

  /// The required size of instances of this type.
  /// 'InstanceAddressPoint' bytes go before the address point;
  /// 'InstanceSize - InstanceAddressPoint' bytes go after it.
  uint32_t InstanceSize;

  /// The alignment mask of the address point of instances of this type.
  uint16_t InstanceAlignMask;

  /// Reserved for runtime use.
  uint16_t Reserved;

  /// The total size of the class object, including prefix and suffix
  /// extents.
  uint32_t ClassSize;

  /// The offset of the address point within the class object.
  uint32_t ClassAddressPoint;

  // Description is by far the most likely field for a client to try
  // to access directly, so we force access to go through accessors.
private:
  /// An out-of-line Swift-specific description of the type, or null
  /// if this is an artificial subclass.  We currently provide no
  /// supported mechanism for making a non-artificial subclass
  /// dynamically.
  TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;

public:
  /// A function for destroying instance variables, used to clean up after an
  /// early return from a constructor. If null, no clean up will be performed
  /// and all ivars must be trivial.
  TargetSignedPointer<Runtime, ClassIVarDestroyer * __ptrauth_swift_heap_object_destructor> IVarDestroyer;

  // After this come the class members, laid out as follows:
  //   - class members for the superclass (recursively)
  //   - metadata reference for the parent, if applicable
  //   - generic parameters for this class
  //   - class variables (if we choose to support these)
  //   - "tabulated" virtual methods

  using TargetAnyClassMetadata<Runtime>::isTypeMetadata;

  ConstTargetMetadataPointer<Runtime, TargetClassDescriptor>
  getDescription() const {
    assert(isTypeMetadata());
    return Description;
  }

  typename Runtime::StoredSignedPointer
  getDescriptionAsSignedPointer() const {
    assert(isTypeMetadata());
    return Description;
  }

  void setDescription(const TargetClassDescriptor<Runtime> *description) {
    Description = description;
  }

  // [NOTE: Dynamic-subclass-KVO]
  //
  // Using Objective-C runtime, KVO can modify object behavior without needing
  // to modify the object's code. This is done by dynamically creating an
  // artificial subclass of the the object's type.
  //
  // The isa pointer of the observed object is swapped out to point to
  // the artificial subclass, which has the following properties:
  // - Setters for observed keys are overridden to additionally post
  // notifications.
  // - The `-class` method is overridden to return the original class type
  // instead of the artificial subclass type.
  //
  // For more details, see:
  // https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html

  /// Is this class an artificial subclass, such as one dynamically
  /// created for various dynamic purposes like KVO?
  /// See [NOTE: Dynamic-subclass-KVO]
  bool isArtificialSubclass() const {
    assert(isTypeMetadata());
    return Description == nullptr;
  }
  void setArtificialSubclass() {
    assert(isTypeMetadata());
    Description = nullptr;
  }

  ClassFlags getFlags() const {
    assert(isTypeMetadata());
    return Flags;
  }
  void setFlags(ClassFlags flags) {
    assert(isTypeMetadata());
    Flags = flags;
  }

  StoredSize getInstanceSize() const {
    assert(isTypeMetadata());
    return InstanceSize;
  }
  void setInstanceSize(StoredSize size) {
    assert(isTypeMetadata());
    InstanceSize = size;
  }

  StoredPointer getInstanceAddressPoint() const {
    assert(isTypeMetadata());
    return InstanceAddressPoint;
  }
  void setInstanceAddressPoint(StoredSize size) {
    assert(isTypeMetadata());
    InstanceAddressPoint = size;
  }

  StoredPointer getInstanceAlignMask() const {
    assert(isTypeMetadata());
    return InstanceAlignMask;
  }
  void setInstanceAlignMask(StoredSize mask) {
    assert(isTypeMetadata());
    InstanceAlignMask = mask;
  }

  StoredPointer getClassSize() const {
    assert(isTypeMetadata());
    return ClassSize;
  }
  void setClassSize(StoredSize size) {
    assert(isTypeMetadata());
    ClassSize = size;
  }

  StoredPointer getClassAddressPoint() const {
    assert(isTypeMetadata());
    return ClassAddressPoint;
  }
  void setClassAddressPoint(StoredSize offset) {
    assert(isTypeMetadata());
    ClassAddressPoint = offset;
  }

  uint16_t getRuntimeReservedData() const {
    assert(isTypeMetadata());
    return Reserved;
  }
  void setRuntimeReservedData(uint16_t data) {
    assert(isTypeMetadata());
    Reserved = data;
  }

  /// Get a pointer to the field offset vector, if present, or null.
  const StoredPointer *getFieldOffsets() const {
    assert(isTypeMetadata());
    auto offset = getDescription()->getFieldOffsetVectorOffset();
    if (offset == 0)
      return nullptr;
    auto asWords = reinterpret_cast<const void * const*>(this);
    return reinterpret_cast<const StoredPointer *>(asWords + offset);
  }

  uint32_t getSizeInWords() const {
    assert(isTypeMetadata());
    uint32_t size = getClassSize() - getClassAddressPoint();
    assert(size % sizeof(StoredPointer) == 0);
    return size / sizeof(StoredPointer);
  }

  /// Given that this class is serving as the superclass of a Swift class,
  /// return its bounds as metadata.
  ///
  /// Note that the ImmediateMembersOffset member will not be meaningful.
  TargetClassMetadataBounds<Runtime>
  getClassBoundsAsSwiftSuperclass() const {
    using Bounds = TargetClassMetadataBounds<Runtime>;

    auto rootBounds = Bounds::forSwiftRootClass();

    // If the class is not type metadata, just use the root-class bounds.
    if (!isTypeMetadata())
      return rootBounds;

    // Otherwise, pull out the bounds from the metadata.
    auto bounds = Bounds::forAddressPointAndSize(getClassAddressPoint(),
                                                 getClassSize());

    // Round the bounds up to the required dimensions.
    if (bounds.NegativeSizeInWords < rootBounds.NegativeSizeInWords)
      bounds.NegativeSizeInWords = rootBounds.NegativeSizeInWords;
    if (bounds.PositiveSizeInWords < rootBounds.PositiveSizeInWords)
      bounds.PositiveSizeInWords = rootBounds.PositiveSizeInWords;

    return bounds;
  }

#if SWIFT_OBJC_INTEROP
  /// Given a statically-emitted metadata template, this sets the correct
  /// "is Swift" bit for the current runtime. Depending on the deployment
  /// target a binary was compiled for, statically emitted metadata templates
  /// may have a different bit set from the one that this runtime canonically
  /// considers the "is Swift" bit.
  void setAsTypeMetadata() {
    // If the wrong "is Swift" bit is set, set the correct one.
    //
    // Note that the only time we should see the "new" bit set while
    // expecting the "old" one is when running a binary built for a
    // new OS on an old OS, which is not supported, however we do
    // have tests that exercise this scenario.
    auto otherSwiftBit = (3ULL - SWIFT_CLASS_IS_SWIFT_MASK);
    assert(otherSwiftBit == 1ULL || otherSwiftBit == 2ULL);

    if ((this->Data & 3) == otherSwiftBit) {
      this->Data ^= 3;
    }

    // Otherwise there should be nothing to do, since only the old "is
    // Swift" bit is used for backward-deployed runtimes.
    
    assert(isTypeMetadata());
  }
#endif

  bool isStaticallySpecializedGenericMetadata() const {
    auto *description = getDescription();
    if (!description->isGeneric())
      return false;

    return this->Flags & ClassFlags::IsStaticSpecialization;
  }

  bool isCanonicalStaticallySpecializedGenericMetadata() const {
    auto *description = getDescription();
    if (!description->isGeneric())
      return false;

    return this->Flags & ClassFlags::IsCanonicalStaticSpecialization;
  }

  static bool classof(const TargetMetadata<Runtime> *metadata) {
    return metadata->getKind() == MetadataKind::Class;
  }
}
  • 通过整理TargetClassMetadata 的所有属性,得到以下这样一个数据结构
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
}

4.2 通过Swift代码还原Metadata的数据结构

  • 通过4.1小节的分析和总结,将HeapObject通过Swift数据结构进行描述,然后bindMemory进行内存数据进行映射,来验证自己的假设
struct HeapObject{
    var metadata: UnsafeRawPointer
    var refCounts: UInt64
}
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
}

// 定义一个class
class Student {
    let id = 10
    let name = "Lili"
}

let student = Student()
// 获取`student`实实例的原始指针
let pRawHeapObject = Unmanaged.passUnretained(student).toOpaque()

// 将`pRawHeapObject`指向的内容映射到`HeapObject`数据结构
let heapObject = pRawHeapObject.bindMemory(to: HeapObject.self, capacity: 1).pointee
print(heapObject)

// 将`heapObject.metadata`映射到`Metadata`数据结构
let metadata = heapObject.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee
print(metadata)
print("Student instance size is:", class_getInstanceSize(Student.self))

在调试窗口查看运行结果: “superClass: _TtCs12_SwiftObject “, “instanceSize”: 40 符合预期

HeapObject(metadata: 0x00000001000081a8, refCounts: 3)
Metadata(kind: 4295000432, superClass: _TtCs12_SwiftObject, cacheData: (7402896512, 140943646785536), data: 4302401618, classFlags: 2, instanceAddressPoint: 0, instanceSize: 40, instanceAlignmentMask: 7, reserved: 0, classSize: 120, classAddressPoint: 16, typeDescriptor: 0x0000000100003cf0, iVarDestroyer: 0x0000000000000000)
Student instance size is: 40

5 总结

  • Swift 类最终通过malloc_zone_malloc 分配在堆空间, 堆空间的分配效率比栈低、堆空间的读写非线程安全。
  • 继承NSObject的类,通过调用TargetHeapMetadata(TargetAnyClassMetadata *isa) 函数,将isa指针作为参数,初始化一个TargetHeapMetadata结构体,与纯Swift类的数据结构基本保持一致,所以能被OC调用。
  • HeapObjectMetadata的描述占用大量内存空间, 所以类占用的空间比结构体大。
  • HeapObjec.refCounts 虽然加锁保证线程安全,但是频繁读写也会消耗资源,也是类比结构体慢的原因之一。

参考文档: