「这是我参与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码
;