阅读视图

发现新文章,点击刷新页面。

Swift底层原理学习笔记

笔记主要记录Swift和OC底层原理差异的地方,OC的底层原理之前的笔记有详细记录。

课程是逻辑教育的,视频基本只看了总结部分,然后结合网上已有笔记进行的重点梳理。

Swift 进阶一:类,对象,属性

类、对象

  • Swift对象的内存结构HeapObject,有两个属性:一个是Metadata,一个是Refcount,默认占用16字节大小,就是对象中没有任何东西也是16字节。
  • OC中实例对象的本质是结构体,是以objc_object为模板继承的,其中有一个isa指针,占8字节。
  • Swift比OC中多了一个refCounted引用计数大小,也就是多了8字节。

  • getClassObject函数是根据kind获取object类型
  • 如果kind(理解为isa指针)是Class类型,则将当前的Metadata强转成ClassMetadata,而ClassMetadataTargetClassMetadata根据类型的别名,其中TargetClassMetadata结构: image.png
  • TargetClassMetadata继承自TargetAnyClassMetadata image.png
  • 结构与OC中的objc_class的结构一样,有isa,有父类,有cacheDataData类似于objc_class中的bits
  • 根据上面分析我们可以得到结论:当metadatakindClass时,有如下的继承关系: image.png

  • 总结,swift的类内存结构可以理解为:
struct Metadata {
    void *kind;               // 类型标识(如类、结构体、枚举)
    void *superClass;         // 父类的 Metadata 指针
    void *cacheData;          // 方法缓存(类似 OC 的 cache_t)
    void *data;               // 指向额外数据的指针
    
    // 实例布局信息
    uint32_t flags;                   // 类型标志位
    uint32_t instanceAddressOffset;   // 实例变量的起始偏移量
    uint32_t instanceSize;            // 实例对象占用的内存大小
    uint16_t instanceAlignMask;       // 实例的对齐掩码
    uint16_t reserved;                // 保留字段
    
    // 类布局信息
    uint32_t classSize;               // 类对象占用的内存大小
    uint32_t classAddressOffset;      // 类变量的起始偏移量
    void *description;                // 类型描述信息
    
    
    // 类对象与元类对象的关键区别
    uint32_t flags;       // 包含类型标志位(如是否为元类)
    void *vtable;         // 类对象的 vtable 指向实例方法表
    void *classVtable;    // 元类对象的 vtable 指向类方法表
    
    // 方法签名表(所有方法),存放在类对象中
    MethodDescriptor* methodDescriptors;
    uint32_t methodCount;
}

// 例子:
class MyClass { 
    func instanceMethod() {} // 实例方法 
    static func classMethod() {} // 类方法 
} 
// 实例方法调用(通过类对象的 vtable) 
let obj = MyClass() obj.instanceMethod() // 类对象 → Metadata → vtable → 方法实现 

// 类方法调用(通过元类对象的 classVtable) 
MyClass.classMethod() // 类对象 → 元类对象 → Metadata → classVtable → 方法实现


属性

    1. 存储属性:有常量存储属性和变量存储属性两种,都占用内存
    1. 计算属性:不占用内存,本质为函数。
    1. 属性观察者
      1. 属性观察可以添加在类的存储属性继承的存储属性继承的计算属性
      1. 父类在调用init中改变属性值不会触发属性观察,子类调用父类的init触发属性观察
      1. 统一属性在父类和子类都添加观察,在触发观察时:
      • willSet方法,先子类后父类
      • didSet方法,先父类后子类
    1. 延迟属性(lazy) :延迟属性必须有初始(可以为nil),只有在访问后内存中才有值,延迟属性对内存有影响,不能保证线程安全
    1. 类型属性:类型属性必须有初始值,内存只分配一次,通过swift_once函数创建,类似dispatch_once,是线程安全的。

    • 可以用于单例:
    class XXX {
      static let share: XXX = XXX()
      private init(){}
    }
    

Swift 进阶二:值类型、引用类型、结构体

结构体,值类型

struct WSPerson { 
    var age: Int = 18 
} 
    
struct WSTeacher { 
    var age: Int 
}

image.png

  • 结构体会自动创建为所有参数赋值的构造函数。
  • 结构体开辟的内存在栈区
  • 结构体的赋值是深拷贝,并且有写时复制的机制。

结构体的属性修改问题

  • 结构体对象self类型为let,即不可以被修改。
  • 结构体中函数修改属性, 需要在函数前添加mutating关键字,本质是给函数的默认参数self添加了inout关键字,将selflet常量改成了var变量。
  • mutating方法修改结构体属性时,采用的是 "in-place" 的方式,也就是直接在当前实例的内存空间里修改属性值,并没有重新创建一个新的实例来替换原来的实例。这一特性和赋值操作有着本质的区别。

结构体的函数调用

  • 值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用 image.png 这个符号哪里来的?

  • 是从Mach-O文件中的符号表Symbol Tables,但是符号表中并不存储字符串,字符串存储在String Table(字符串表,存放了所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名

方法重载问题

  • Objective-C里,方法重载是不被支持的,不过Swift却支持,这主要是由它们不同的函数签名机制和语言设计理念造成的。
  1. 函数签名机制
  • Objective - C:它的函数签名只依据方法名,和参数类型没有关系。 比如下面这两个方法,在OC看来是一样的,所以无法共存:
- (void)doSomethingWithInt:(int)value; 
- (void)doSomethingWithInt:(NSString *)value; 
  • Swift:它的函数签名是由方法名和参数类型共同组成的。 下面这样的重载在Swift中是被允许的:
func doSomething(value: Int) 
func doSomething(value: String) 

2. 消息传递机制

  • OC:采用的是运行时消息传递机制,方法调用是通过字符串(SEL)来实现的。 像[obj doSomethingWithInt:1]这样的调用,在运行时会被解析为SEL @selector(doSomethingWithInt:),要是有多个同名方法,就会引发冲突。
  • Swift:使用的是静态 dispatch 机制,在编译时就会确定具体要调用哪个方法。
  1. 补充说明
  • Swift 的重载:除了参数类型不同可以重载外,参数数量不同或者参数标签不同也能实现重载。
  • OC 的替代方案:在OC中,如果要实现类似功能,通常会采用命名约定,例如doSomethingWithInt:doSomethingWithString:

总结来说,Swift支持方法重载是其类型系统和编译时检查机制的自然结果,而OC不支持则是受限于其动态特性和历史设计。

Swift 进阶三:内存分区、方法调度、指针

方法调度

  • Swift 类的方法(非final、非static、非@objc修饰的)会被存放在一个名为 vtable 的表中。
  • 只有类能够使用 vtable,结构体和枚举由于不支持继承,所以没有 vtable
内存布局示例:
[实例对象内存]
  ├ isa 指针 ───→ [类对象]
                ├ Metadata 指针 ───→ [Metadata]
                │                  └ vtable 指针 ───→ [vtable 内存区域]
                │                                      ├ 0: init()
                │                                      ├ 1: method1()
                │                                      └ 2: method2()
                └ 其他类数据...
  • 方法调用时的流程,当调用一个类的实例方法时,Swift 运行时会:
    1. 通过实例的 isa 指针找到类对象。
    2. 从类对象中获取 Metadata 指针。
    3. 从 Metadata 中读取 vtable 指针。
    4. 根据方法在 vtable 中的索引,调用对应的函数实现。
  • vtable 仅存储可重写的方法,而类的所有方法(包括不可重写的)仍通过元数据(Metadata)管理。
  • 元类对象(Metaclass Object)的 Metadata 主要存储类方法(static/class 方法)的实现信息。
  • 协议方法的签名和实现由 Witness Table 管理,与类对象 / 元类对象的 Metadata 是分离的。
方法调用总结
  • struct值类型,它的函数调度是直接调用,即静态调度
    • 值类型在函数中如果要修改实例变量的值,则函数前面需要添加Mutating修饰
  • class引用类型,它的函数调度是通过vtable函数,即动态调度
  • extension中的函数是直接调用,即静态调度
  • final修饰的函数是直接调用,即静态调度
  • @objc修饰的函数是methodList函数表调度,如果方法需要在OC中使用,则类需要继承NSObject
  • dynamic修饰的函数调度方式是methodList函数表调度,它是动态可以修改的,可以进行method-swizzling
    • @objc+dynami修饰的函数是通过objc_msgSend来调用的
  • 如果函数中的参数想要被更改,则需要在参数的类型前面增加inout关键字,调用时需要传入参数的地址

Swift 进阶四:弱引用、闭包、元类型

Swift 内存管理

  • swift实例对象的内存中,存在一个Metadata,一个Refcount。后者记录引用计数。
  • Refcount最终可以获得64位整型数组bits,其结构: image.png
// 简化的 Refcount 结构(实际实现可能更复杂)
struct Refcount {
    // 64 位中的高 32 位:强引用计数
    uint32_t strongRefCount: 32;
    
    // 64 位中的低 32 位:
    uint32_t hasWeakRefs: 1;      // 是否有弱引用
    uint32_t hasUnownedRefs: 1;   // 是否有 unowned 引用
    uint32_t isDeiniting: 1;      // 是否正在析构
    uint32_t sideTableMask: 1;    // 是否使用 Side Table
    uint32_t weakRefCount: 28;    // 弱引用计数
};
  • 当引用计数超出直接存储范围时,通过 sideTableMask 标志切换到全局 Side Table 存储。
  • Swift在创建实例对象时的默认引用计数是1,而OCalloc创建对象时是没有引用计数的。

弱引用

  • 为对象增加弱引用时,实际是调用refCounts.formWeakReference,即去操作sideTable表,添加对象的弱引用关系,这里和OC处理是一致的。

swift中的runtime

  • 对于纯swift类来说,没有动态特性dynamic(因为swift是静态语言),方法和属性不加任何修饰符的情况下,已经不具备runtime特性,此时的方法调度,依旧是函数表调度即V_Table调度。
  • 对于纯swift类,方法和属性添加@objc标识的情况下,可以通过runtime API获取到,但是在OC中是无法进行调度的,原因是因为swift.h文件中没有swift类的声明。
  • 对于继承自NSObject类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加@objc关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性。

补充

  • Any:任意类型,包括function类型、optional类型
  • AnyObject:任意类的instance、类的类型、仅类遵守的协议,可以看作是Any的子类
  • AnyClass:任意实例类型,类型是AnyObject.Type
  • T.self:如果T是实例对象,则表示它本身,如果是类,则表示metadata.T.self的类型是T.Type
❌