Swift 类与结构体

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

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

1. Swift 类与结构体的区别

class struct 备注
支持继承 YES NO
支持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

objc_allocWithZone

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类源码导读

  • swift_allocObject 定义在HeapObject.cpp文件
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 定义在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<Runtime>
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文件
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定义,得到这样一个表, 可以推断出,调用TargetHeapMetadata(MetadataKind kind)函数时,如果kind == MetadataKind::Class, 表述初始化一个class
name value
name value
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
LastEnumerated 0x7FF
  • TargetHeapMetadata继承TargetMetadata<Runtime>, 在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<Runtime> *isa) 函数,将isa指针作为参数,初始化一个TargetHeapMetadata结构体,与纯Swift类的数据结构基本保持一致,所以能被OC调用。
  • HeapObject和Metadata的描述占用大量内存空间, 所以类占用的空间比结构体大。
  • HeapObjec.refCounts 虽然加锁保证线程安全,但是频繁读写也会消耗资源,也是类比结构体慢的原因之一。

参考文档:
copy-on-write: https://github.com/apple/swift/blob/main/docs/OptimizationTips.rst#advice-use-copy-on-write-semantics-for-large-values
Swift写时复制(copy-on-write): https://www.jianshu.com/p/e8b1336d9d5d
Choosing Between Structures and Classes: https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Swift创建类和结构体时,不需要将声明文件和实现文件分开 一个类的实例通常被称为对象。 结构体和类对比 共同点都...
    Sunday_David阅读 491评论 0赞 0
  • 我的博客[https://dengfeng520.github.io/] 1、值类型和引用类型 在iOS中虚拟内存...
    小時間光阅读 1,036评论 0赞 2
  • 类和结构体 结构体和类作为一种通用而又灵活的结构,成为了人们构建代码的基础。你可以使用定义常量、变量和函数的语法,...
    xiaofu666阅读 299评论 0赞 0
  • 在面向过程的语言中,要想实现类似类的功能只能借助结构体,其实从OC源码也能看出来,类的组成本就是复杂的结构体实现的...
    如风如花不如你阅读 8,406评论 2赞 6
  • Swift进阶-类与结构体[https://www.jianshu.com/p/347bafbb3cf8]Swif...
    顶级蜗牛阅读 2,290评论 0赞 11

友情链接更多精彩内容