Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析
类与结构体的异同点
// 定义一个类或结构体
class/struct Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
deinit { // class
}
}
相同点:
- 定义存储的属性
- 定义方法
- 定义下标,使用下标语法(subscript)提供对其值的访问
- 定义初始化器(init)
- 使用extension来拓展功能
- 遵循Protocol来提供某种功能
不同点:
- class有继承的特性,struct没有继承特性
- 类型转换使您能在运行时检查和解释class的实例对象的类型
- class有析构函数用来释放其占用的资源
- 引用计数允许对一个class实例有多个引用
- class是引用类型,struct是值类型
- 一般情况下,class存储在堆区;struct存储在栈区
引用类型
class Person {
var name: String
init(name: String) {
self.name = name
}
}
image.png
这里我们借助两个指令来查看当前变量的内存结构:
引用类型特征图p / po 的区别在于:
po 只会输出对应的值;
p 则会返回值的类型以及命令结果的引用名。
x/8g (16进制地址): 读取内存中的值(8g: 8字节格式输出)
看到p1
与p2
变量都引用了同一个Person
的实例地址。
所以引用类型存储的是实例内存地址的引用。
而p1
和p2
两个变量本身的地址是不一样的,而他俩内存是挨着的,刚好相差8个字节,这两变量的内存地址存储的就是当前实例对象的内存地址:
值类型
struct Person {
var name: String
init(name: String) {
self.name = name
}
}
image.png
看到输出的内容就和class的不一样了。
值类型存储的就是具体实例(或者说具体的值)。
引用类型 和 值类型 的存储位置
一般情况下,值类型存储在栈上,引用类型存储在堆上。
先来了解一下我们的内存模型:
我们把系统分配给app的可操作性的内存空间人为地分成五大区:指令区、常量区、全局(静态)区、堆区、栈区
栈区(stack): 局部变量和函数运行过程中的上下文
堆区(Heap): 存储所有对象
Global: 存储全局变量;常量;代码区
Segment & Section
: Mach-O
文件有多个段( Segment
),每个段有不同的功能。然后每 个段又分为很多小的 Section
name | value |
---|---|
TEXT.text | 机器码 |
TEXT.cstring | 硬编码的字符串 |
TEXT.const | 初始化过的常量 |
DATA.data | 初始化过的可变的(静态/全局)数据 |
DATA.const | 没有初始化过的常量 |
DATA.bss | 没有初始化的(静态/全局)变量 |
DATA.common | 没有初始化过的符号声明 |
LLDB调试内存分布
frame variable -L xxx
结构体的内存分布:
struct Person {
var age = 18
var name = "a"
}
frame variable -L 指令调试
struct是值类型,所以存放第一个首地址是指向age的,并且很明显struct存储在栈区,因为地址是连续的,age到name刚好中间相差8个字节。
当前结构体在内存中分布示意图如果struct的成员包含了一个引用类型呢?
struct Person {
var age: Int = 18
var name: String = "a"
var t = Teacher()
}
class Teacher {
var age = 10
var name = "tearcher"
}
image.png
age、name和t在栈区这没有问题,而Teacher开辟的地址0x0000600000ec1080在堆区。
类的内存分布:
class Person {
var age = 18
var name = "a"
}
frame variable -L 指令调试
我们知道p1存储在方法栈上,Person实例的地址是0x00006000024a9b90,仍然要在堆区中开辟内存空间。
当前类的实例在内存中分布示意图类在内存分配的时候,会在堆空间上找到合适的内存区域,找到和内存区域后就会把这个内存地址拷贝到堆,然后栈区的内存地址指向这个当前的堆区。
离开作用域的时候,势必要回收内存空间,这个时候先查找类的内存空间,并把内存块归重新插入到堆空间中,栈区地址不再指向堆区。
对于引用类型来说,创建和销毁都必须有一个查找的过程,会有时间和速度上的损耗,并且对于引用计数的计算也是消耗性能的
举例:
一个聊天室创建一个聊天气泡(makeBalloon),通过Color、Orientation、Tail来作为字符串的key从缓存中取出气泡img
enum Color { case blue, green, gray}
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [String: UIImage]()
func makeBalloon(_ color: Color, _ orientation: Orientation, _ tail: Tail) {
let key = "\(color):\(orientation):\(tail)"
if let image = cache[key] {
return image
}
...
}
上面这段代码虽然我们做了image的缓存,但是key是一个字符串(是一个表型为值类型的引用类型)存储在堆区,在每次调用makeBalloon
时候,仍然要从堆空间中不断地分配/销毁内存。
优化后的代码:
enum Color { case blue, green, gray}
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
struct Ballon: Hashable {
var color: Color
var orientation: Orientation
var tail: Tail
}
func makeBalloon(_ ballon: Ballon) {
if let image = cache[ballon] {
return image
}
...
}
在我们实际开发当中尽可能地使用struct代替class,如果诸如继承这些关系,那可以选用class。
初始化器
结构体不需要声明初始化器,系统默认自动提供成员变量初始化器。
struct Person {
var age: Int
var name: String
}
类在声明的时候必须给予一个指定初始化器
,同时我们也可以提供便捷初始化器
、可失败初始化器
和必要初始化器
:
class Person {
var age: Int
var name: String
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
convenience init(_ age: Int) {
self.init(age, "名称")
}
}
class Son: Person {
var subName: String
init(_ subName: String) {
self.subName = subName
super.init(18, "wj")
}
}
可失败初始化器:
class Person {
var age: Int
var name: String
init?(_ age: Int, _ name: String) {
if age < 18 {return nil}
self.age = age
self.name = name
}
convenience init?(_ age: Int) {
self.init(age, "名称")
}
}
必要初始化器(继承下去的子类必须实现该初始化器):
class Person {
var age: Int
var name: String
required init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
convenience init(_ age: Int) {
self.init(age, "名称")
}
}
类的生命周期
iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:
image.pngOC 通过 clang 编译器编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码);
Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。
// 语法分析分析输出AST(抽象语法树)
swiftc main.swift -dump-parse
// 语义分析并且检查类型输出AST
swiftc main.swift -dump-ast
// 生成swift中间体语言(SIL)未优化
swiftc main.swift -emit-silgen
// 生成swift中间体语言(SIL)已优化
swiftc main.swift -emit-sil
// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir
// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc
// 生成汇编
swiftc main.swift -emit-assembly
// 编译生成可执行.out文件 (x86、arm64....)
swiftc -o main.o main.swift
可以通过上面的命令自行尝试编译过程。
// 还原类名
xcrun swift-demangle xxx // xxx是经过混写规则的类名
class Person{
var age = 18
var name = "LGMan"
}
var p = Person()
Person()创建的时候打个断点调试,Debug->Debug Workflow -> Always Show Disassembly
Person是纯swift类
,在创建实例的时候,会调用SwiftTest.Person.__allocating_init()
; 底层会调用swift_allocObject
和SwiftTest.Person.init()
。
Person是继承NSObject的类
,在创建实例的时候,会调用SwiftTest.Person.__allocating_init()
;底层会调用objc_allocWithZone
和objc_msgSend 发送init消息
。
来看swift源码 swift_allocObject
底层调用分配内存
全局搜索swift_allocObject
,在HeapObject.cpp
文件找到swift_allocObject
,它会调用_swift_allocObject_
函数:
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);
// If leak tracking is enabled, start tracking this object.
SWIFT_LEAKS_START_TRACKING_OBJECT(object);
SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);
return object;
}
里面调用了swift_slowAlloc
函数返回了一个 HeapObject 泛型对象,全局搜索这个函数,来到Heap.cpp
里的swift_slowAlloc
:
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;
}
看到这行熟悉代码 p = malloc(size);
进行了内存分配,最后并返回了p
Swift 对象内存分配: __allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc
再回来 _swift_allocObject_
函数看接下来的逻辑
对object进行初始化,可以来看看HeapObject的结构:
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{ }
Swift 对象的内存结构: HeapObject
(OC objc_object) ,有两个属性各占8字节: Metadata
和 RefCount
,默认占用 16 字节大小。
RefCount是一个64位的引用计数,那么HeapMetadata
到底是什么呢?
源码分析class的数据结构
HeapMetadata
HeapMetadata给 HeapMetadata
起了别名 TargetHeapMetadata
所以swift类的 HeapMetadata / TargetHeapMetadata
是通过 kind
进行初始化的。
MetadataKind
的定义:
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 |
TargetMetadata
里面有一个函数:
这里的this
就是TargetMetadata
实例,可以看成objc里的isa,而
TargetClassMetadata
是swift所有类型元类的最终基类(OC objc_class)。
/// 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;
}
};
using ClassMetadata = TargetClassMetadata<InProcess>;
而TargetClassMetadata
的父类TargetAnyClassMetadata
就有熟悉的感觉了:
/// The portion of a class metadata object that is compatible with
/// all classes, even non-Swift ones.
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
using StoredPointer = typename Runtime::StoredPointer;
using StoredSize = typename Runtime::StoredSize;
#if SWIFT_OBJC_INTEROP
constexpr TargetAnyClassMetadata(TargetAnyClassMetadata<Runtime> *isa,
TargetClassMetadata<Runtime> *superclass)
: TargetHeapMetadata<Runtime>(isa),
Superclass(superclass),
CacheData{nullptr, nullptr},
Data(SWIFT_CLASS_IS_SWIFT_MASK) {}
#endif
constexpr TargetAnyClassMetadata(TargetClassMetadata<Runtime> *superclass)
: TargetHeapMetadata<Runtime>(MetadataKind::Class),
Superclass(superclass)
#if SWIFT_OBJC_INTEROP
, CacheData{nullptr, nullptr},
Data(SWIFT_CLASS_IS_SWIFT_MASK)
#endif
{}
#if SWIFT_OBJC_INTEROP
// Allow setting the metadata kind to a class ISA on class metadata.
using TargetMetadata<Runtime>::getClassISA;
using TargetMetadata<Runtime>::setClassISA;
#endif
// Note that ObjC classes do not have a metadata header.
/// The metadata for the superclass. This is null for the root class.
TargetSignedPointer<Runtime, const TargetClassMetadata<Runtime> *
__ptrauth_swift_objc_superclass>
Superclass;
#if SWIFT_OBJC_INTEROP
/// The cache data is used for certain dynamic lookups; it is owned
/// by the runtime and generally needs to interoperate with
/// Objective-C's use.
TargetPointer<Runtime, void> CacheData[2];
/// The data pointer is used for out-of-line metadata and is
/// generally opaque, except that the compiler sets the low bit in
/// order to indicate that this is a Swift metatype and therefore
/// that the type metadata header is present.
StoredSize Data;
static constexpr StoredPointer offsetToData() {
return offsetof(TargetAnyClassMetadata, Data);
}
#endif
/// Is this object a valid swift type metadata? That is, can it be
/// safely downcast to ClassMetadata?
bool isTypeMetadata() const {
#if SWIFT_OBJC_INTEROP
return (Data & SWIFT_CLASS_IS_SWIFT_MASK);
#else
return true;
#endif
}
/// A different perspective on the same bit
bool isPureObjC() const {
return !isTypeMetadata();
}
};
using AnyClassMetadata =
TargetAnyClassMetadata<InProcess>;
using ClassIVarDestroyer =
SWIFT_CC(swift) void(SWIFT_CONTEXT HeapObject *);
TargetAnyClassMetadata
的数据结构里有我们所熟悉的 Superclass
、ClassISA
、CacheData
、Data
这里面的数据结构就是我们的最终答案
经过源码分析我们不难得出 swift 类的数据结构
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
}
异变方法
swift中class和struct都能定义方法func。但是区别在于:
默认情况下,值类型的属性不能被自身的实例方法修改的。
来看看下面这个案例:
此时self
就是结构体自己,指代x, y
。当调用moveBy
方法时候,就相当于p在修改自己,此时是不被允许的。
改变自身的实例方法前面需要添加mutating
修饰,此时才能编译成功:
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x: Double, y: Double) {
self.x += x
self.y += y
}
}
一个用mutating
修饰,一个没有的情况,编译成swift中间代码 sil
来看一下:
struct Point {
var x = 0.0, y = 0.0
func test() {
let tmp = self.x
}
mutating func moveBy(x: Double, y: Double) {
self.x += x
self.y += y
}
}
上面已有编译命令,自行使用。下面这个是输出sil的Point结构体:
Point结构体 sil sil的Point的关注的方法swift中函数的参数默认在最后是传递self的,而objective-c是在方法列表前面默认传递self
和cmd
。
找到test
和moveBy
方法,可以看出moveBy
方法前加了mutating
修饰其参数后面加了个@inout
。
SIL 文档的解释 @inout
:
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
也就是说moveBy
的默认参数 @inout Point
其实是一个地址,而test
的默认参数就是一个结构体的值。
再来关注这两句代码
test:
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
相当于是伪代码:let self = Point
,let在swift中是不可修改的,并且self取的是一个值。
moveBy:
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
相当于是伪代码:var self = &Point
,var在swift中是可修改的,并且self取的地址。
区别举例:
区别举例值类型的实例方法前面添加了mutating
关键字,其默认参数会增加一个@inout,而这个@inout修饰的参数相当于传递一个地址。
案例一:
var age = 10
func modify(_ age: inout Int) {
var tmp = age
tmp += 1
}
modify(&age)
print(age) // 10
我们传递了age地址进去了,此时的age打印的还是10,为什么?
编译后的sil方法体里的age是一个值,这个值取的是外部变量age的地址的值。所以
var tmp = age
是值类型的赋值,并不会外部变量age那个地址存储的值。
伪代码:
var age = &age
var tmp = (withUnsafePoint(to: &age) {return $0}).pointee
案例二:
func modify(_ age: inout Int) {
age += 1
}
modify(&age)
print(age) // 11
此时age += 1就变得好使了,age是11,因为内部age取的是外部age的地址,可以改变地址的值。
方法调度
类的方法调度
objective-c的方法调度是以消息发送的方式。swift的方法调度是以什么方式?
首先我们来了解一下常见汇编指令
,然后再汇编下调试方法调度的过程。
mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器
与常量之间传值,不能用于内存地址),如:
mov x1, x0 // 将寄存器x0的值赋值到寄存器x1中
add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中,如:
add x0, x1, x2 // 将寄存器x1和x2的值相加后,保存到x0中
sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中:
sub x0, x1, x2 // 将寄存器x1和x2的 值相减后,保存到x0中
and: 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中,如:
and x0, x0, #0x1 // 将寄存器x0的值和常量1 按位 与 之后保存到寄存器x0中
orr: 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中,如:
orr x0, x0, #0x1 // 将寄存器x0的值和常量1 按位 或 之后保存到集成器x0中
str : 将寄存器中的值写入到内存中,如:
str x0, [x0, x8] // 将寄存器x0的值保存到栈内存 [x0 + x8]处
ldr: 将内存中的值读取到寄存器中,如:
ldr x0, [x1, x2] 将寄存器x1和x2的值相加作为地址,取该内存地址的值,放入寄存器x0中
cbz: 和 0 比较,如果结果为零就转移(只能跳到后面的指令)
cbnz: 和非 0 比较,如果结果非零就转移(只能跳到后面的指令)
cmp: 比较指令
blr: (branch)跳转到某地址(无返回)
bl: 跳转到某地址(有返回)
ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中
小tip:在看方法调用的时候,关注bl和blr。
新建一个工程,在ViewController.swift
// ViewController.swift
import UIKit
class Teacher {
func teach() {
print("teach")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = LGTeacher()
t.teach()
}
}
设置汇编调试,然后在真机上运行代码(arm64的汇编):
设置汇编调试断点打在 t.teach() 来看看汇编
汇编在__allocating_init()
和swift_release
之间的 blr x8
就是teach方法的调用,然后我们来看看blr x8
的调用里面是啥,推测对不对:
那么swift在方法调用的时候是怎么调用的呢,我这里给Teacher扩充两个方法:
class Teacher{
func teach() {
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
在viewDidLoad都去调用运行,看汇编找到这三个方法调用
image.png image.png可以看到三个函数的内存是连续的,并且都相差8个字节。
分析第一个teach:
__allocating_init
的返回值放在x0寄存器里,它现在存的是实例对象
mov x20, x0 // 将x0的值赋值给x20
str x20, [sp, #0x8] // #0x8入栈,将x20的值保存到栈内存
str x20, [sp, #0x10] // #0x10入栈,将x20的值保存到栈内存
ldr x8, [x20] // 取x20地址的值给到 x8寄存器,这里[x20]取地址就是对象的前8个字节:metadata
ldr x8, [x8, #0x50] // 寄存器x8(metadata address)和地址#0x50的值相加,取地址存放到x8
ldr x8, [x20]
这里[x20]取地址就是对象的前8个字节:metadata
。
执行后 x8到底是不是metadata
:
验证结果是metadata
接着再执行 ldr x8, [x8, #0x50]
相当于是 (metadata address value) + (0x50 value) = teach
。最后就是执行teach了。
ps:0x50是编译的时候,系统就确定了的。
而三个teach函数在方法栈的内存中是连续内存空间,并且刚好相差了8个字节(函数指针的大小):0x50、0x58、0x60
所以teach方法的调用过程:找到Metadata
,确定函数地址(metadata + 偏移量
),执行函数。
验证函数表调度
上面可以看出,swift其中的一种方法调用方式:函数表调度。
把上面那个ViewController.swift
编译成sil
文件,打开并拖拽文件最后:
这个sil_vtable
就是class
自己的函数表。不相信?好吧,继续验证。
上面分析出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
,不管是class/struct/enum
都有自己的Descriptor
,它就是对类的一个详细描述。
找到swift源码 TargetClassMetadata
找到 Description
成员变量
TargetClassDescriptor
就是上面说的描述,经过分析得出其数据结构:
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32 // class/struct/enum 的名称
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
// var size: UInt32
// V-Table (methods)
}
TargetClassDescriptor
本身的结构是没有 V-Table
的,进而我从源码里面找推测出来的,下面就开始推测流程:
继续找全局搜TargetClassDescriptor呗:
using ClassDescriptor = TargetClassDescriptor<InProcess>;
再全局搜这个别名ClassDescriptor
,定位到一个类描述生成器里面:
ClassContextDescriptorBuilder
这个类是用来创建当前的Matedata和Descriptor用的。然后找到layout
函数:
首先来看看 super::layout()
做了啥:
这里的各种add是不是与上面的TargetClassDescriptor
数据结构有点类似了,这个layout
就是在创建Descriptor
进行赋值操作!
再回来看看ClassContextDescriptorBuilder
的layout
函数:
addVTable();
添加虚函数表
addOverrideTable();
添加重载虚函数表
此时此刻在源码中剖析的TargetClassDescriptor
数据结构里有V-Table
也只是猜测,接下来我从Match-O
文件进行验证。
Mach-O介绍
Mach-O
其实是Mach Object
文件格式的缩写,是 mac
以及 iOS
上可执行文件的格式。常见的 .o,.a .dylib Framework,dyld .dsym
。
- 文件头
Header
,表明该文件是Mach-O
格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排。 -
Load commands
是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
-
Data
区主要就是负责代码和数据记录的。Mach-O
是以Segment
这种结构来组织数据的,一个Segment
可以包含 0 个或多个Section
。根据Segment
是映射的哪一个Load Command
,Segment
中section
就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据Segment
做内存映射的。
拿到Mach-O
文件的步骤:
1.编译工程,找到Products
目录里
2.找到应用程序,右键显示包内容,找到可执行文件exec
image.png3.打开软件 MachOView
将可执行文件拖拽进去后
从Match-O验证TargetClassDescriptor结构里有V-Table
案例代码 ViewController.swift:
class Teacher{
func teach() {
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = Teacher()
t.teach()
t.teach1()
t.teach2()
//metadata + offset
}
}
编译后把可执行文件拖拽到MachOView
在Mach-O的data区里的__TEXT,__swift5_types
就是存放 所有的struct/enum/类的Descriptor
的地址信息;以每4个字节来做区分。第一个4字节就是Teacher的Descriptor:
掏出计算器,所以Teacher的Descriptor在mach-o上的地址:
0xFFFFFBF0 + 0xBC58 = 0x10000B848 // Descriptor
而0x100000000是Mach-O文件叫虚拟内存的基地址,在Mach-O中也能找到:
虚拟内存的基地址所以Descriptor在mach-o的data区的偏移量:
0x10000B848 - 0x100000000 = 0xB848
然后再data区找到 __TEXT,__const
里边,去找0xB848的位置:
在0xB848后面开始算起就是Teacher的Descriptor的内容(到哪里结束先不关心),再回来看Descriptor的数据结构:
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32 // class/struct/enum 的名称
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
// var size: UInt32
// V-Table (methods)
}
可以看出它的成员已有12个,所以我们再向后偏移12个4字节,再往后的4个字节里的内容就是size
:
所以size后面的8个就是teach()方法的内容,再往后8个就是teach1()方法的内容,再往后8个就是teach2()的内容:
image.png来验证一下红色画线的地方就是teach()方法的内容,而我们红色画线的开始位置是0xB87C,就是我们程序运行时在内存时的偏移量,它需要加上程序运行的基地址。
获取程序运行的基地址:
程序运行的基地址0x000000010297c000 是我们当前的程序运行时候的基地址。
而上线红色画线的teach方法的内容在内存中的地址:
0x000000010297c000 + 0xB87C = 0x10298787C
0x10298787C就是teach()方法的内容TargetMethodDescriptor
,那么方法里的内容有什么呢?
来看看swift源码里方法在内存中的数据结构:
flags是标识这个方法是一个什么方法,是初始化方法还是getter还是什么方法,它占用4个字节。
另外一个是imp的指针,如果我们要找到imp指针那还需要偏移;
首先把 0x10298787C 偏移flags那4个字节
0x10298787C + 0x4 = 0x102987880
注意imp指针其实是一个相对指针,它存储的其实是offset,所以我们还需要用 0x102987880加上offset,得到的就是teach的实际的方法实现!!
那这个offset是什么呢:
0x102987880 + 0xFFFFB9D8 = 0x202983258
注意0x202983258 还需减去 Mach-O的基地址:
0x202983258 - 0x100000000 = 0x102983258
0x102983258就是teach()方法的imp在内存中的地址。
继续回到我们的工程,输出teach()的内存地址,如果他俩匹配,所以上面的猜测验证成功!
从刚才的工程运行的断点,我们进入汇编调试Alaways Show Disassembly
进入到汇编,找到__allocating_init()
后面的第一个blr
,打印那个寄存器的地址 register read x8
一起见证时刻
所以验证了我们的猜想:
**TargetClassDescriptor
数据结构里有 size
和 V-Table
**
来swift源码里看看V-Table是怎么创建的:
创建V-Table通过Metadata
来获取当前的描述Descriptor
,把Descriptor
的method
加载到了对应的位置。这个vtableOffset
是程序编译的时候就决定了。
swift里类的默认调度方式是函数表调度
函数表调度的实质:就是Metadata + offset
但是swift里类在extension
里的派发方式是直接派发
个人理解(官方没有声明):因为当前的类已经生成的VTable,此时如果要从extension里的方法添加到VTable的话,需要通过大量的计算offset,这样会大量浪费cpu资料,没有这个必要,所以苹果自动给优化成了extension的方法调度是直接派发。
结构体的方法调度
struct Teacher{
func teach() {
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = Teacher()
t.teach()
t.teach1()
t.teach2()
}
}
在 t.teach()处断点进入汇编调试,运行程序,看看和class的有什么不一样的
struct汇编调试struct在方法调用的时候直接拿到方法地址直接调用了。
结构体的方法调用方式是直接派发
默认方法调度方式总结:
Swift默认派发影响函数派发的方式
-
final:
允许类里面的函数使用直接派发,这个修饰符会让函数失去动态性。任何函数都可以使用这个修饰符,就算是 extension 里本来就是直接派发的函数。这也会让 Objective-C 的运行时获取不到这个函数,不会生成相应的 selector。 -
dynamic:
可以让类里面的函数使用消息机制派发。使用 dynamic必须导入 Foundation 框架,里面包括了 NSObject 和 Objective-C 的运行时。dynamic 可以让声明在 extension 里面的函数能够被 override。dynamic 可以用在所有 NSObject 的子类和 Swift 的原声类。 -
@objc 或 @nonobjc:
都可以显式地声明了一个函数是否能被 Objective-C 的运行时捕获到。但使用 @objc 的典型例子就是给 selector 一个命名空间 @objc(abc_methodName),让这个函数可以被 Objective-C 的运行时调用。@nonobjc会改变派发的方式,可以用来禁止消息机制派发这个函数,不让这个函数注册到 Objective-C 的运行时里。我不确定这跟 final 有什么区别,因为从使用场景来说也几乎一样。我个人来说更喜欢 final,因为意图更加明显。 -
final 与 @objc同时使用:
可以在标记为 final 的同时,也使用 @objc 来让函数可以使用消息机制派发。这么做的结果就是,调用函数的时候会使用直接派发,但也会在 Objective-C 的运行时里注册响应的 selector。函数可以响应 perform(selector:) 以及别的 Objective-C 特性,但在直接调用时又可以有直接派发的性能。 -
@inline:
Swift 也支持 @inline,告诉编译器可以使用直接派发。有趣的是,dynamic @inline(__always) func dynamicOrDirect() {} 也可以通过编译!但这也只是告诉了编译器而已,实际上这个函数还是会使用消息机制派发。这样的写法看起来像是一个未定义的行为,应该避免这么做。
将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内 联函数作为优化。
always
- 将确保始终内联函数。通过在函数前添加@inline(__always)
来实现此行为
never
- 将确保永远不会内联函数。这可以通过在函数前添加@inline(never)
来实现。
如果函数很长并且想避免增加代码段大小,请使用@inline(never)
关键字影响函数派发方式如果对象只在声明的文件中可见,可以用 private 或 fileprivate 进行修饰。编译器会对
private
或fileprivate
对象进行检查,确保没有其他继承关系的情形下,自动打上final
标记,进而使得 对象获得静态派发
的特性
(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明 中访问)
可见的都会被优化 (Visibility Will Optimize)
Swift 会尽最大能力去优化函数派发的方式. 例如, 如果你有一个函数从来没有 override, Swift 就会检车并且在可能的情况下使用直接派发. 这个优化大多数情况下都表现得很好, 但对于使用了 target / action 模式的 Cocoa 开发者就不那么友好了.
另一个需要注意的是, 如果你没有使用 dynamic 修饰的话,这个优化会默认让 KVO 失效
。如果一个属性绑定了 KVO 的话,而这个属性的 getter 和 setter 会被优化为直接派发,代码依旧可以通过编译,不过动态生成的 KVO 函数就不会被触发。
class Teacher {
dynamic func teach() {
print("teach")
}
}
extension Teacher {
@_dynamicReplacement(for: teach)
func teach1() {
print("teach1")
}
}
let t = Teacher()
t.teach1() // 实际调用teach,而调用teach()还是打印teach
派发总结 (Dispatch Summary):
image.png对于函数派发这里仅仅只做一些粗浅的总结。
如果您想要了解详细的函数派发,可以看看我之前分享的一片文章:Swift的函数派发
里面有更详细的demo演示举例
喜欢的老铁❤一个,感谢支持!
网友评论