「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」
类型属性
我们在之前提到的属性实际上都是实例属性,在Swift中与之对应的还有类型属性又称为类属性。实例属性由类的实例调用,类型属性则直接由类来调用;类型属性使用static或者class关键字来声明。使用static关键字声明的属性也被称为静态属性;对于类的计算属性,如果允许子类对其计算方法进行覆写,则需要使用class关键字来声明;
- 类型属性其实就是一个全局变量
- 类型属性只会被初始化一次
类型属性分析
那么如何声明一个类型属性呢?我们给一个正常的属性age添加上static关键字,它就变成了类型属性:
class Person {
static var age: Int = 18
}
我们访问类型属性时,可以直接使用类名进行访问:
那么类型属性与我们正常的属性有什么区别呢?我们可以通过SIL文件分析类型属性:
从SIL文件中我们看到,类型属性还是一个存储属性,但是比起一般的存储属性多了一个static,而且age属性变成了一个全局变量;通过main函数中的实现:
根绝注释,我们看到Person.age.unsafeMutableAddressor其实是在访问age属性的内存地址,此时调用的是函数s4main6PersonC3ageSivau,我们定位到此函数的实现:
在该函数中,通过函数s4main6PersonC3age_WZ拿到了一个内存地址,经过指针转换,最终返回了age属性的内存地址,我们看一下函数s4main6PersonC3age_WZ的具体实现:
这里构建了一个Int类型的结构体添加到内存中(存放到全局变量age中),也就是在初始化全局变量age ;
类型属性中gcd的运用
我们在上文中SIL文件中看到在获取age属性的内存地址时,通过builtin "once"调用了s4main6PersonC3age_WZ函数,那么s4main6PersonC3age_WZ是如何确保只调用了一次呢?我们将代码降级为IR代码,在IR代码中查看该函数的实现:
在IR代码中使用到了s4main6PersonC3ageSivau,根据命令行,我们可以看到其就是Person.age.unsafeMutableAddressor:
并且,其最终是通过swift_once调用的,我们在Swift源码中可以找到swift_once的实现如下:
在该函数中,最终调用了gcd中的dispatch_once_t来确保全局变量age属性只被初始化一次;
单例类的实现
那么,我们结合static和let就能在Swift中实现单例类:
当然这样还不完美,我们需要将指定初始化器私有化,以确保在外部只能通过shared来访问:
属性在MachO中的位置
我们在之前已经分析过,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
}
其中的typeDescriptor存储在MachO文件中的__swift5_types中,其数据结构如下:
struct TargetClassDescriptor {
ContextDescriptorFlags Flags;
TargetRelativeContextPointer<Runtime> Parent;
TargetRelativeDirectPointer<Runtime, const char, /*nullable*/ false> Name;
TargetRelativeDirectPointer<Runtime, MetadataResponse(...),
/*Nullable*/ true> AccessFunctionPtr;
TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor,
/*nullable*/ true> Fields;
TargetRelativeDirectPointer<Runtime, const char> SuperclassType;
uint32_t MetadataNegativeSizeInWords;
uint32_t MetadataPositiveSizeInWords;
uint32_t NumImmediateMembers;
uint32_t NumFields;
uint32_t FieldOffsetVectorOffset;
}
而在该数据结构中Fields存储我们的属性信息;我们通过MachO文件来验证;
首先,我们在Swift源码中找到FieldDescriptor的结构信息:
class FieldDescriptor {
const RelativeDirectPointer<const char> MangledTypeName;
const RelativeDirectPointer<const char> Superclass;
const FieldDescriptorKind Kind;
const uint16_t FieldRecordSize;
const uint32_t NumFields;
const FieldRecords<FieldRecord>
}
MangledTypeName:混写之后的类型名称;NumFields:当前属性个数;FieldRecords:属性的信息
FieldRecords是一个数组,其中的元素为FieldRecord,其数据结构如下:
class FieldRecord {
const FieldRecordFlags Flags;
const RelativeDirectPointer<const char> MangledTypeName;
const RelativeDirectPointer<const char> FieldName;
}
MangledTypeName:属性的类型信息;FieldName:属性的名称;
基于以上数据结构我们就能够通过MachO文件找到属性在MachO中的位置;
我们将以上代码生成MachO文件:
此处存储的是TargetClassDescriptor,我们通过计算0xFFFFFEC4 + 0x3F5C = 0x100003E20,我们减去虚拟地址之后,得到0x3E20,我们在MachO中定位到该地址:
根据TargetClassDescriptor的数据结构,我们想要找FieldDescriptor需要向后偏移4*4字节:
此处开始存放的就是FieldDescriptor的偏移地址,我们通过0x3E30 + 0x00000104 = 0x3F34,我们定位到该偏移地址在MachO中的位置:
此处存放的就是FieldDescriptor结构体的内容,根据其数据结构,我们可以分析得到想要到找FieldRecords需要向后偏移16字节:
后边这些连续的存储空间就是我们需要查找的FieldRecords的结构体;根据FieldRecords的数据结构可以知道0xFFFFFFDF偏移地址是第一个属性的名字,我们通过计算0xFFFFFFDF + 0x3F44 + 0x8(0x4F44向后偏移的8字节) = 0x100003F2B,我们在MachO中定位到3F2B:
我们在此处找到age和age1;其中61、67、65和31分别对应a、g、e和1的ASCII码;