Swift进阶(九) —— 协议

·  阅读 4355

协议为方法、属性、以及其他特定的任务需求或功能定义蓝图。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现。满足了协议中需求的任意类型都叫做遵循了该协议。

除了指定遵循类型必须实现的要求外,你可以扩展一个协议以实现其中的一些需求或实现一个符合类型的可以利用的附加功能。

协议基本语法

协议定义

定义协议的方式和类、结构体、枚举类型相似,使用protocol来声明协议

protocol SomeProtocol {
    // protocol definition goes here
}
复制代码

遵循协议

在Swift中, classstructenum 都可以遵循协议

在自定义类型声明时,将协议名放在类型名的冒号之后来表示该类型采纳一个特定的协议。多个协议可以用逗号分开列出:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}
复制代码

若一个类拥有父类,将这个父类名放在其采纳的协议名之前,并用逗号分隔:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}
复制代码

同样,协议也可以遵循协议

protocol SomeProtocol: Protocol1
复制代码

如果你一个协议只想让类可以遵循这个协议,那么你可以让协议继承自AnyObject 截屏2022-04-18 19.06.13.png 当你遵循一个协议时,你必须实现协议中的所有没有没有实现的计算属性和方法,否则编译会报错。 截屏2022-04-18 19.11.58.png

协议里面添加属性

协议可以要求所有遵循该协议的类型提供特定名字和类型的实例属性或类型属性。在协议里面定义一个属性必须明确是可读的可读的和可写的,同时这个属性要求定义为变量属性,在属性名称前面使用var关键字。可读写的属性使用 { get set } 来写在声明后面来明确,使用 { get } 来明确可读的属性。

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}
复制代码

你也可以在协议中定义类型属性,在定义类型属性时往前面添加static关键字就可以了。

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}
复制代码

协议里面定义方法

协议里面定义类方法和实例方法

协议可以要求采纳的类型实现指定的实例方法类方法。这些方法作为协议定义的一部分,书写方式与正常实例和类方法的方式完全相同,但是不需要大括号和方法的主体。允许变量拥有参数,与正常的方法使用同样的规则。但在协议的定义中,方法参数不能定义默认值。

当协议中定义类型方法时,你总要在其之前添加static关键字。即使在类实现时,类型方法要求使用classstatic作为关键字前缀。

protocol SomeProtocol {
    static func someTypeMethod()
}
复制代码

添加实例方法则不需要在前面加上static关键字。

protocol RandomNumberGenerator {
    func random() -> Double
}
复制代码

协议里面定义异变方法

有时一个方法需要改变其所属的实例。在方法的func关键字之前使用mutating关键字,来表示在该方法可以改变其所属的实例,以及该实例的所有属性。这允许结构体枚举类型能采用相应协议并满足方法要求。

在下面Togglable协议的定义中, toggle()方法使用mutating关键字标记,来表明该方法在调用时会改变遵循该协议的实例的状态:

protocol Togglable {
    mutating func toggle()
}
复制代码

现在定义一个名为OnOffSwitch的枚举。这个枚举在两种状态间改变,即枚举成员OnOff。该枚举的toggle实现使用了mutating关键字,以满足Togglable协议需求:

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}

var lightSwitch = OnOffSwitch.off

lightSwitch.toggle()
复制代码

协议里面定义初始化方法

协议可以要求遵循协议的类型实现指定的初始化器。你可以通过实现指定初始化器便捷初始化器来使遵循该协议的类满足协议的初始化器要求。在这两种情况下,你都必须使用required关键字修饰初始化器的实现:

protocol SomeProtocol {
    init(someParameter: Int)
}

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}
复制代码

由于final的类不会有子类,如果协议初始化器实现的类使用了final标记,你就不需要使用required来修饰了。因为这样的类不能被继承子类。

如果一个子类重写了父类指定的初始化器,并且遵循协议实现了初始化器要求,那么就要为这个初始化器的实现添加requiredoverride两个修饰符:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}
复制代码

协议扩展

使用协议扩展提供默认实现

你可以使用协议扩展来给协议的任意方法或者计算属性要求提供默认实现。如果遵循类型给这个协议的要求提供了它自己的实现,那么它就会替代扩展中提供的默认实现。

protocol MyProtocol{
    var age: Int{get}
    func test() 
}
extension MyProtocol{
    var age: Int {return 10}
    func test(){}
}
复制代码

给协议扩展添加限制

当你定义一个协议扩展,你可以明确遵循类型必须在扩展的方法和属性可用之前满足的限制。在扩展协议名字后边使用where分句来写这些限制。比如说,你可以给Collection定义一个扩展来应用于任意元素遵循上面MyProtocol协议的集合。

extension Collection where Iterator.Element: MyProtocol {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}
复制代码

和OC交互

如果OC类需要遵循协议,需要在协议前面增加关键字@objc,如果想使用OC的optionl的属性,需要在方法或者属性添加关键字@objc optional

@objc protocol MyProtocl {
    //可选的
    @objc optional func func1()
    var age: Int { get }
}
复制代码

协议的方法调度

我们在之前的方法篇中了解过,类的方法通过VTable来调度,而结构体和枚举类型的方法是通过静态派发的方式。那么协议里面的方法是通过什么方式来派发的呢?

protocol Incrementable {
    func increment(by: Int)
}

class LGTeacher: Incrementable {
    func increment(by: Int) {
        print(by)
    }
}
let t: LGTeacher = LGTeacher()
t.increment(by: 20)
复制代码

接下来我们转成sil文件看一下: 截屏2022-04-20 23.01.36.png 截屏2022-04-20 23.02.14.png 可以看到,此时的increment(by: Int)方法还是一个class_method,因此我们可以知道这个方法还是通过VTable来调度的。我们再LGTeacher里面的sil_vtable里也找到了这个方法。

如果我们把变量t声明为协议类型,那么这时候方法是如何调度的呢?

let t: Incrementable = LGTeacher()
复制代码

转成sil文件后,对应的main函数里面的代码如下:

截屏2022-04-20 23.10.34.png 可以看到increment函数不再是class_method,此时变成了witness_method。那这个witness_method是什么呢?我们去swift的sil文档里面查看一下: 截屏2022-04-20 23.14.52.png 文档里面的意思是,从一个遵循协议的类型去寻找协议方法的实现,会通过witness_method这种方式去调度,如果协议使用@objc修饰,会变成objc的调用方式。

同时,我们在sil里面找到witness_table,里面包含了协议方法increment截屏2022-04-20 23.19.00.png 接下来我们通过寻找increment的名称,然后搜索一下,探索是如何找到LGTeacher类里面的increment方法。

截屏2022-04-20 23.22.28.png 可以看到,通过witness_table里面方法名称,直接从遵循这个协议的类里面,找到这个方法的具体实现,同时通过这个类的V_table去调度这个方法。

通过上面的分析我们可以对协议的方法调用做以下总结:

  • 如果实例对象的静态类型就是确定的类型,那么这个协议方法通过 VTalbel 进行调度。

  • 如果实例对象的静态类型是协议类型,那么这个协议方法通过 witness_table 中对应的协议方法,然后通过协议方法去查找遵守协议的类的 VTable 进行调度。

结构体遵循协议

上面分析的是类遵循协议后的方法调度,那如果是结构体struct呢?

把上面的代码改成struct后,我们再进入到sil文件里面探个究竟: 截屏2022-04-21 18.50.15.png 截屏2022-04-21 18.50.49.png 截屏2022-04-21 18.51.15.png 可以发现,和类一样,也是先通过witness_table里面协议方法去寻找遵循协议的结构体里的的方法实现,但是由于结构体里面的方法是静态派发,没有VTable,因此会直接去调用结构体里面的方法。

协议在extension中提供了默认方法实现

如果在一个协议中声明了方法,然后再extension中实现了该方法。那会是什么情况呢?

protocol Incrementable {
    func increment(by: Int)
}

extension Incrementable {
    func increment(by: Int) {
        print("Extension Increment")
    }
}

class LGTeacher: Incrementable {
    func increment(by: Int) {
        print("LGTeacher Increment")
    }
}

class LGStudent: Incrementable {}

let t1: LGTeacher = LGTeacher()
let t: Incrementable = LGTeacher()
let s: LGStudent = LGStudent()
let s1: Incrementable = LGStudent()

t.increment(by: 10)
t1.increment(by: 20)
s.increment(by: 10)
s1.increment(by: 20)
//打印结果
LGTeacher Increment
LGTeacher Increment
Extension Increment
Extension Increment
复制代码

截屏2022-04-21 19.12.50.png 截屏2022-04-21 19.11.23.png 可以看到,当遵守协议的类实现了协议方法,那么会去走VTable调用方法。如果遵循协议的类没有实现协议方法,而且这个协议的extension提供了方法的默认实现,那么这个类会通过witness_table直接去静态调用extension里面的方法实现。

协议中没有声明方法

如果在协议中没有声明方法,然后在extension中声明方法,那又会怎么样呢?

protocol Incrementable {
}

extension Incrementable {
    func increment(by: Int) {
        print("Extension Increment")
    }
}

class LGTeacher: Incrementable {
    func increment(by: Int) {
        print("LGTeacher Increment")
    }
}

class LGStudent: Incrementable {}

let t1: LGTeacher = LGTeacher()
let t: Incrementable = LGTeacher()
let s: LGStudent = LGStudent()
let s1: Incrementable = LGStudent()

t.increment(by: 10)
t1.increment(by: 20)
s.increment(by: 10)
s1.increment(by: 20)
//打印结果
Extension Increment
LGTeacher Increment
Extension Increment
Extension Increment
复制代码

打印出来sil文件查看 截屏2022-05-02 11.48.27.png 截屏2022-05-02 11.52.39.png 可以看到,当协议里面没有声明方法时,witness_table里面没有任何方法,所以无法通过 witness_table调用方法。而LGTeacher类里面有实现方法,所以t1会通过V_Table直接派发。LGStudent里面没有实现方法,所以是通过extension静态派发的方式来调用方法。

总结

对于确定类型则并且提供了方法实现的和没有遵守协议的时候一样 对于协议类型,则声明之后就需要通过PWT方法,再根据实例对象的类型和对象类型中是否实现方法决定调度方式 V-Table 派发还是静态派发。

协议中声明协议中未声明
确定类型 + 实现V-TableV-Table
确定类型 + 未实现静态派发静态派发
协议类型 + 实现PWT+V-Table静态派发
协议类型 + 未实现PWT+静态派发静态派发

witness_table和类型的关系

  • 当一份协议被多个类型遵守的时候
protocol Incrementable {
    func increment(by: Int) -> Int
}

class test : Incrementable {
    func increment(by: Int) -> Int {
        return by + 1
    }
}

class test1: Incrementable {
    func increment(by: Int) -> Int {
        return by + 2
    }
}
复制代码

截屏2022-05-02 23.21.48.png 可以看到,每个遵循协议的类型都会有一个自己的witness_table

  • 当一个类遵守多份协议的时候
protocol Incrementable {
    func increment(by: Int) -> Int
}

protocol myProtocol {
    func test()
}

class test : Incrementable, myProtocol {
    func increment(by: Int) -> Int {
        return by + 1
    }    

    func test() {
        print("123")
    }
}
复制代码

截屏2022-05-02 23.25.58.png 可以看到,一个类遵循多个协议,会为每一个协议增加一个witness_table,一个类中witness_table的数量取决于这个类遵循了多少个协议。

  • 当一个类遵循了协议,同时有子类。
protocol Incrementable {
    func increment(by: Int) -> Int
}

protocol myProtocol {
    func test()
}

class test : Incrementable, myProtocol {
    func increment(by: Int) -> Int {
        return by + 1
    }    

    func test() {
        print("123")
    }
}

class test1: test {
    override func increment(by: Int) -> Int {
        return by + 2
    }
}

class test2: test {}
复制代码

截屏2022-05-02 23.31.01.png 可以看到,这个类遵循一个协议,就会有一个witness_table,如果这个类有子类,那么子类和父类会共用一个witness_table

协议的内存结构的底层布局

协议的底层存储

我们在上面分析了协议如何调度方法,以及遵循协议的类型和witness_table的关系,现在我们来看一下协议是怎么存储的,首先我们来看一下遵循协议的类型的内存大小。

protocol Shape {
    var area: Double { get }
}

class Circle: Shape {
    var radious: Double    
    init(_ radious: Double) {
        selfradious = radious
    }
    var area: Double {
        get {
            return radious * radious
        }
    }
}

var circle: Circle = Circle.init(10.0)
var circle1: Shape = Circle.init(10.0)

var t = type(of: circle1)

print(class_getInstanceSize(Circle.self)) //24
print(class_getInstanceSize((t as! AnyClass))) //24

print(MemoryLayout.size(ofValue: circle)) //8
print(MemoryLayout.size(ofValue: circle1)) //40

print(MemoryLayout<Circle>.size) //8
print(MemoryLayout<Shape>.size) //40

复制代码

可以看到,遵循协议的具体类的实例变量的内存大小和遵循协议的变量的内存大小是不一样的。所以它们在底层的内存结构也是不一样的。我们先来看一下circle:Cricle的内存结构。 截屏2022-05-04 11.00.38.png 通过lldb命令把属性值转成float类型,可以看到,属性值正是10。 截屏2022-05-04 11.09.54.png 接下来我们看一下circle1:Shape的内存结构: 截屏2022-05-04 15.15.35.png 从上面的内存分析中,我们可以得到协议的大概存储结构:

  • 0-7:实例对象的堆空间地址
  • 8-23:未知
  • 24-31: 实例对象的metadata
  • 32-40: 协议的protocol witness table 由此,我们可以得到协议类型的大致结构:
struct LGProtocolBox {
    var heapObject: UnsafeRawPointer
    var unknow1: UnsafeRawPointer
    var unknow2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeRawPointer
}
复制代码

接着我们把代码转成IR代码看一下:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
// %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
  %2 = bitcast i8** %1 to i8*
  // 创建Circle类的metadata
  %3 = call swiftcc %swift.metadata_response @"$s4main6CircleCMa"(i64 0) #4
  %4 = extractvalue %swift.metadata_response %3, 0
  
  // 创建一个Circle类的实例对象 s4main6CircleCACycfC == __allocating_init
  %5 = call swiftcc %T4main6CircleC* @"$s4main6CircleCACycfC"(%swift.type* swiftself %4)
  
  // 把metadata存放的%T4main5ShapeP数组的第二个成员变量里。
  // %T4main5ShapeP = type { [24 x i8], metadata, i8** }
  store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp", i32 0, i32 1), align 8
  
  //s4main6CircleCAA5ShapeAAWP = protocol witness table for main.Circle
  //把protocol witness table存储到数组的第三个成员变量里
  // %T4main5ShapeP = type { [24 x i8], metadata, witness_table }
  store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"$s4main6CircleCAA5ShapeAAWP", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp", i32 0, i32 2), align 8
  
  //把实例对象的地址存放到数组的第一个成员变量里
  //%T4main5ShapeP = type { [heapObject, Unknown, Unknown], metadata, witness_table }
  store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp" to %T4main6CircleC**), align 8
  ret i32 0
}
复制代码

接下来我们看一下witness_table的内存结构

//s4main6CircleCAA5ShapeAAWP = protocol witness table for main.Circle, 
//witness_table的内存结构
@"$s4main6CircleCAA5ShapeAAWP" = hidden constant [2 x i8*] [i8* bitcast (%swift.protocol_conformance_descriptor* @"$s4main6CircleCAA5ShapeAAMc" to i8*), i8* bitcast (double (%T4main6CircleC**, %swift.type*, i8**)* @"$s4main6CircleCAA5ShapeA2aDP4areaSdvgTW" to i8*)], align 8
复制代码

截屏2022-05-04 16.19.55.png 这里面存储了两个变量,一个是protocol_conformance_descriptor,一个是遵循了shape协议实现了area属性的protocol witness 由此我们可以得到witness_tabel的大概数据结构

struct TargetWitnessTable {
    var protocol_conformance_descriptor: UnsafeRawPointer
    var protocol_witness: UnsafeRawPointer
}
struct LGProtocolBox {
    var heapObject: UnsafeRawPointer
    var unknow1: UnsafeRawPointer
    var unknow2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>

}
复制代码

接下来,我们去源码里面寻找witness_tabel的具体细节。首先我们全局搜索TargetWitnessTable,找到的代码如下: 截屏2022-05-05 08.36.12.png TargetWitnessTable里面有一个TargetProtocolConformanceDescriptor类型的description属性。我们再去查看TargetProtocolConformanceDescriptor类。 截屏2022-05-08 10.18.48.png 可以看到,这个类里面有四个属性:

TargetRelativeContextPointer<Runtime, TargetProtocolDescriptor> Protocol;
TargetTypeReference<Runtime> TypeRef;
RelativeDirectPointer<const TargetWitnessTable<Runtime>> WitnessTablePattern;
ConformanceFlags Flags;
复制代码

接下来我们对这四个属性进行分析,我们先看protocol属性,这是一个指向TargetProtocolDescriptor类型的TargetRelativeContextPointer。我们先看一下TargetRelativeContextPointer截屏2022-05-08 10.29.44.png 截屏2022-05-08 15.14.39.png 可以看到其实和 # Swift进阶(四)—— 指针中的TargetRelativeDirectPointer其实是一样的功能就是获取相对地址绝对地址,可以直接用 TargetRelativeDirectPointer代替。

接下来我们看一下TargetProtocolDescriptor,代码如下: 截屏2022-05-09 18.49.36.png 我们把TargetProtocolDescriptor还原成下面的结构体:

struct TargetProtocolDescriptor {
    var flags: UInt32
    var parent: TargetRelativeDirectPointer<UnsafeRawPointer>
    var name: TargetRelativeDirectPointer<CChar>
    var NumRequirementsInSignature: UInt32
    var NumRequirements: UInt32
    var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>
}
复制代码

witness_table的内存结构

通过上面的分析我们可以把witness_table的内存结构还原出来,代码如下:

struct LGProtocolBox {
    var heapObject: UnsafeRawPointer
    var unknow1: UnsafeRawPointer
    var unknow2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}

struct TargetWitnessTable {
    var protocol_conformance_descriptor: UnsafePointer<TargetProtocolConformanceDescriptor>
    var protocol_witness: UnsafeRawPointer
}

struct TargetProtocolConformanceDescriptor {
    var ptotocolDesc: TargetRelativeDirectPointer<TargetProtocolDescriptor>
    var typeRef: UnsafeRawPointer
    var WitnessTablePattern: UnsafeRawPointer
    var flags: UInt32
}

struct TargetProtocolDescriptor {
    var flags: UInt32
    var parent: TargetRelativeDirectPointer<UnsafeRawPointer>
    var name: TargetRelativeDirectPointer<CChar>
    var NumRequirementsInSignature: UInt32
    var NumRequirements: UInt32
    var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>
}

struct TargetRelativeDirectPointer<Pointee> {
    var offset: Int32    
    mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer<Pointee> {
        let offset = self.offset        
        return withUnsafePointer(to: &self) { p in
            return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
        }
    }
}
复制代码

接下来我们来验证witness_table的内存结构。代码如下:

var circle: Shape = Circle.init(10.0)

withUnsafePointer(to: &circle) { ptr **in**
    ptr.withMemoryRebound(to: LGProtocolBox.**self**, capacity: 1) { pointer **in**

        print(pointer.pointee)

        let descPtr = pointer.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.ptotocolDesc.getmeasureRelativeOffset()
        print("协议名称:\(String(cString: descPtr.pointee.name.getmeasureRelativeOffset()))")
        print("协议方法的数量:\(descPtr.pointee.NumRequirements)")
        print("witnessMethod:\(pointer.pointee.witness_table.pointee.protocol_witness)")

    }
}
复制代码

打印结果如下: 截屏2022-05-18 23.39.26.png 我们在分析 IR 代码的时候,应该有注意到 TargetWitnessTableprotocol_witness,这一个其实存储的就是我们的 witnessMethod,在上面的 IR 代码中其实已经写的很清楚了,但我们还是来验证一下。

  • 在终端使用 nm -p <可执行文件> | grep <内存地址> 打印出这个方法的符号信息。
  • 接着用 xcrun swift-demangle <符号信息> 还原这个符号信息。 还原结果如下:

截屏2022-05-18 23.46.01.png

所以,这个协议见证表(witness_table)的本质其实就是 TargetWitnessTable。第一个元素存储的是一个 descriptor,记录协议的一些描述信息,例如名称和方法的个数等。那么从第二个元素的指针开始存储的就是函数的指针

从上面的IR代码中,我们知道witness_table 变量是一个连续的内存空间,所以这个 witness_table变量存放的可能是很多个协议的见证表。

存放多个协议见证表的因素取决于变量的静态类型,如果这个变量的类型是协议组合类型,那么 witness_table 存放的就是协议组合中所有协议的见证表,如果这个变量的类型是指定单独的某个协议,那么 witness_table 存放的只有这个协议的见证表。

Existential Container

我们在上面的代码中知道了witness_table的内存结构,那么存储了witness_tableLGProtocolBox又是什么东西呢?

在Swift里面,它有一个名称叫做Existential Container(存在容器)。这是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过当前的 Existential Container 统一管理。规则如下:

  • 对于小容量的数据,直接存储在 Value Buffer 
  • 对于大容量的数据,通过堆区分配,存储堆空间的地址

那这个Existential Container(存在容器) 是怎么实现的呢?我们通过代码来观察一下。

通过上面分析,我们知道,当遵循协议的类型是引用类型的时候,它的第一个内存存储的是实例对象的堆空间地址。那当遵循协议的类型是值类型时,那这个Existential Container(存在容器) 是怎么存储的呢?

首先,我们定义一个遵循shape协议的struct类。然后给这个结构体添加属性。代码如下:

protocol Shape {
    var area: Double { get }
}

struct Circle: Shape {
    var radious: Double
    var width: Double = 20
    var height: Double = 30

    init_ radious: Double) {
        self.radious = radious
    }

    var area: Double {
        get {
            return 10.0
        }
    }

    func getter() {
        print( #function)
    }
}

var circle: Shape = Circle.init(10.0)
print(MemoryLayout.size(ofValue: circle))

//打印结果
40
复制代码

可以看到,遵循协议的结构体实例对象的内存大小和类实例对象一样,都是40。然后我们去查看一下内存结构。 截屏2022-06-04 23.09.28.png 可以看到,和引用类型的内存结构有所不一样,第二个8字节和第三个8字节都有存储值。而且第一个8字节里面存储的也不是引用地址。通过expr命令解析。结果如下: 截屏2022-06-04 23.10.08.png 可以看到,里面刚好存储的是值类型的3个属性值。而这就是上面讲的Existential Container(存在容器) 的第一条管理规则:对于小容量的数据,直接存储在 Value Buffer 。

如果我们给Circle结构体再增加一个属性值,会变成什么样呢?结果如下: 截屏2022-06-04 23.17.26.png 我们发现,当结构体的属性值比较多的时候,它的内存结构又变了。变成和类实例对象的内存结构一样。

截屏2022-06-04 23.20.10.png 通过分析第一个字节存储的内存地址,我们发现,当值类型的属性比较多的时候,编译器会专门在堆区分配一个空间用来存储这些属性值,同时把这个堆空间的内存地址存储在实例对象的内存结构中,这就是上面讲的Existential Container(存在容器) 的第二条管理规则:对于大容量的数据,通过堆区分配,存储堆空间的地址。

写时复制

上面讲过,当值类型的数据比较大的时候,会在堆空间分配存储空间用来存储,那这样还会保持值类型的特性吗?

protocol Shape {
    var area: Double {get}
    var radious: Double{get set}
}
class Circle: Shape{
    var radious: Double
    var width: Double = 20
    var height: Double = 30
    var height1: Double = 50
    init(_ radius: Double) {
        self.radius = radius
    }

    var area: Double{
        get{
            return radius * radius * 3.14
        }
    }
    var area1: Double{
        get{
            return radius * radius * 3.14
        }
    }
}
var circle1 : Shape = Circle.init(10.0)
var c = circle1
c.radious = 20
print(circle1.radious) // 10.0
print(c.radious)//20.0
复制代码

可以看到,当cradius属性值改变的时候,circle的属性值没变。还是保持着值类型的特性,那这个是怎么实现的呢?我们通过内存结构来看一下。

截屏2022-06-04 23.44.15.png 我们先把断点打在cradius属性值未发生改变的地方,然后看下ccircle有没有发生变化。 截屏2022-06-04 23.46.19.pngcradius属性值未发生改变时,ccircle存储的堆空间地址都是一样的。

然后,我们对cradius属性值进行修改,会发生什么呢? 截屏2022-06-04 23.48.48.png 截屏2022-06-04 23.50.25.png 可以看到,当对cradius属性值进行修改后,c存储的堆空间地址发生了改变,而这个新的堆空间地址存放着修改后的属性值。

针对遵循协议的拥有比较大的数据的值类型,Swift采用了一种写时复制的技术,即会去判断这个堆空间的引用计数,当引用计数大于2的时候,也就是有多个实例变量在引用这个堆空间的地址,当其中一个实例变量发生属性值改变的时候,就会在堆空间重新复制一个新的存储空间,并把这个新的空间地址,传给要改变属性值的实例变量,用来保持修改后的实例值,这样做除了会保持值类型的特性外,还减少了内存分配的消耗,避免了创建了一些没有用的存储空间。

分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改