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
从上面的断点跟踪中可以断定出继承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
从上面的断点跟踪中可以断定出纯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,HeapObjec
t有两个属性,一个是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调用。 -
HeapObjec
t和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