Swift进阶-协议

Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析

  • class是本质上定义了一个对象是什么;
  • protocol是本质上定义了一个对象有哪些行为。

一、协议的基本语法

  • 协议要求一个属性必须明确是 getget 和 set
protocol MyProtocol { 
    var age: Int { get set } // 必须是var,不能是let
    var name: String { get } 
}

这里需要注意的一点是:并不是说当前声明 get的属性一定是计算属性,例如:

class Teacher: MyProtocol {
    var age: Int = 18 
    var name: String  // 此时的name依旧是存储属性
    init(_ name: String) { 
        self.name = name 
    } 
}
  • 协议中的异变方法,表示在该方法可以改变其所属的实例,以及该实例的所有属性(用于枚 举和结构体),在为实现该方法的时候不需要写 mutating关键字:
protocol Togglable { 
    mutating func toggle() 
}
  • 类在实现协议中的初始化器,必须使用 required 关键字修饰初始化器的实现(类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器)
protocol MyProtocol {
    init(_ age: Int)
}

class Teacher: MyProtocol {
    var age = 10

    required init(_ age: Int) {
        self.age = age
    }
}

// 或者下面这个方式也是没问题的:
final class Teacher: MyProtocol {
    var age = 10

    init(_ age: Int) {
        self.age = age
    }
}
  • 类专用协议(通过添加 AnyObject 关键字到协议的继承列表,你就可以限制协议只能被类的类型采纳):
protocol MyProtocol: AnyObject{}
  • 可选协议:如果我们不想强制让遵循协议的类类型实现,可以使用 optional 作为前缀放在协议的定义。
@objc protocol Incrementable { 
    @objc optional func increment(by: Int) 
}

二、协议方法调度

回顾Swift进阶-类与结构体的部分内容:类的方法调度是通过函数表v-Table的方式。
疑问:类遵循协议后实现了协议方法的方法调度会不会有所变化?

案例一:

protocol MyProtocol {
    func logInfo(_ something: Any)
}

class Teacher: MyProtocol {
    
    func logInfo(_ something: Any) {
        print(something)
    }
}

var t: Teacher = Teacher()
t.logInfo("安安")

编译成sil后,找到main函数调用logInfo的位置:

案例一:sil 的 main函数

来看看官方文档对于 sil文件中 class_method的定义,得出logInfo是通过函数表调度的方式。

sil 的 class_method官方声明

案例一:sil的Teacher类函数表声明

此时logInfo就被声明在v-Table,并且可以看到还产生出了sil_witness_table这个东西,它是什么? 来看看案例二....

案例二:
如果把 var t: Teacher = Teacher() 改成 var t: Myprotocol = Teacher() 又会发生什么变化呢?

// t在编译时类型是Myprotocol(静态类型)
// t在运行时类型是Teacher(动态类型)
var t: Myprotocol = Teacher()
t.logInfo("安安")

编译成sil后,找到main函数调用logInfo的位置:

案例二:sil 的 main函数

那么当前函数的sil调用方式就变成了 witness_method,看看官方声明:

sil 的 witness_method官方声明

意思就是当前调用logInfo就要去协议见证表sil_witness_table(简称PWT)上查找实现。

witness_method其实是记录着类实现这个协议的方法的编码信息

接着我们继续找到sil里的witness_method

案例二:sil的Teacher类sil_witness_table声明
image.png

最终会通过找到这个对象的具体类型的具体实现。

结论:

一个类实现了协议上的方法,并且创建这个类的对象时,
1.如果把这个对象声明成本身类的类型,会通过v-Table的方式调度函数;
2.如果把这个对象声明成协议类型,那么在对象调用实现协议的方法的时候会先去witness_table查找这个对象的具体类型和具体实现(v-Table),并完成当前的方法调度。

ps:
1.注意每一个遵循协议的类并实现了方法,都会为每一个类创建witness_table;
2.举例Person类遵循了ProtocolA并实现了协议方法,而Teacher类继承于Person类且没有遵循ProtocolATeacher类就不会创建witness_table
3.举例Person类遵循了ProtocolAProtocolB并都实现了协议方法,它会为Person类分别创建两个Protocol Witness Table(PWT)

汇编的角度看案例二:
汇编调试 Alaways Show Disassembly 运行到iPhone上

class ViewController: UIViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        let t: MyProtocol = Teacher()
        t.logInfo("安安") // 断点打在这
    }
}

[x8, #0x50] 像极了我们的 函数表调度的实质:就是Metadata + offset

最终在对象调用实现协议的方法的时候会先去witness_table查找这个对象的具体类型和具体实现(v-Table),并完成当前的方法调度。

三、协议的实质

1.引用类型分析协议

疑问:Class在内存中的数据结构是有一个typeDescription类描述器,里面记录着类的信息(属性描述器、方法v-table等);那么协议在内存中又是一个什么样的东西呢?
下面来看看这个案例:

protocol Shape {
    var area: Double { get }
}

class Circle: Shape {
    var radious: Double // 存储型属性 8
    
    init(_ radious: Double) {
        self.radious = radious
    }
    
    // 不是存储型属性
    var area: Double {
        get {
            return radious * radious * Double.pi
        }
    }
}
var circle: Circle = Circle.init(10.0)
// 打印的是Circle大小
print(class_getInstanceSize(Circle.self)) // metadata+refcount + Double = 16 + 8 = 24
// 值类型可以用MemoryLayout输出struct/enum的大小
// circle是引用类型的话,此时代表的是circle这个变量的大小,而不能看出类的大小
// print(MemoryLayout.size(ofValue: circle))  // 8

此时注意MemoryLayout.size(ofValue: circle)输出的是变量circle的大小,因为circle在栈区,引用了堆区的内存。

但如果把变量声明为Shape类型呢?看看这个变量大小却变成40

var circle1: Shape = Circle.init(10.0)
print(MemoryLayout.size(ofValue: circle1)) // 40

所以引用类型的静态动态类型不一致,那变量存储的东西发生了改变。

用格式化输出一下对比class与protocol的内存有什么不一样:

  • class的内存格式化输出
class的内存格式化输出
  • protocol的内存格式化输出
40个字节存储的内容
protocol的内存格式化输出

所以这40个字节分析得出不多了:第一个8字节存放HeapObject实例对象在堆区的内存地址,第二第三个8字节不知道是什么,第四个8字节是动态类型的Metadata(TargetClassMetadata),第五个8字节是witness table.

故此可以得出这样一个初版结论,protocol在内存上的数据结构(最初版):

struct ProtocolBox {
    var heapObject: UnsafeRawPointer
    var unknow1: UnsafeRawPointer
    var unknow2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeRawPointer
}

把下面的代码编译成IR代码依旧能分析出上面结论:

protocol Shape {
    var area: Double { get }
}

class Circle: Shape {
    var radious: Double // 存储型属性 8
    
    init(_ radious: Double) {
        self.radious = radious
    }
    
    // 不是存储型属性
    var area: Double {
        get {
            return radious * radious * Double.pi
        }
    }
}

var circle1: Shape = Circle.init(10.0)
define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  // 获取Circle的Metadata
  %3 = call swiftcc %swift.metadata_response @"$s4main6CircleCMa"(i64 0) #7
  // %4存储Metadata
  %4 = extractvalue %swift.metadata_response %3, 0

  // 调用allocating-init
  %5 = call swiftcc %T4main6CircleC* @"$s4main6CircleCyACSdcfC"(double 1.000000e+01, %swift.type* swiftself %4)

  // s4main7circle1AA5Shape_pvp是circle实例对象。 这一行代码意思是:把Metadata存储到circle
  store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp", i32 0, i32 1), align 8

  // s4main6CircleCAA5ShapeAAWP是protocol witess table它是一个数组([2 x i8*]代表数组)
  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

  store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp" to %T4main6CircleC**), align 8
  ret i32 0
}

找到编译IR后的main函数位置,由代码逻辑就能得出上面的结论(protocol在内存上的数据结构(最初版)

我们知道 heapObject = metadata+refcount 且知道 metadata的实质。

那接下来就来研究:
1.最后一个成员witness_table内存结构到底长啥样?
2.为什么第一个成员heapObjectmetadata,而第四个成员还存放一个metadata

从上面的main函数里面可以看出 s4main6CircleCAA5ShapeAAWP 它是一个数组,找到这个数组的声明,它是由两个成员组成的

@"$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

第一个成员 protocol_conformance_descriptor;第二个成员是它 s4main6CircleCAA5ShapeA2aDP4areaSdvgTW 自行还原一下就知道它是存放实现协议的内容。
因此就可以得出witness_table这个数据结构:

// 用这个结构体代替  [2 x i8*] 这个数组,因为他俩大小一致内存结构相等
struct TargetProtocolWitnessTable {
    var protocol_conformance_descriptor: UnsafeRawPointer
    var witnessMethod: UnsafeRawPointer // 协议见证里的方法的起始地址
    // 如果实现了协议的多个方法,则在后面加成员
    // ...
}

分析到这里就需要借助swift源码继续往下去分析 protocol_conformance_descriptor 到底是什么?

找到Metadata.h源码里声明TargetWitnessTable的地方:

/// A witness table for a protocol.
///
/// With the exception of the initial protocol conformance descriptor,
/// the layout of a witness table is dependent on the protocol being
/// represented.
template 
class TargetWitnessTable {
  /// The protocol conformance descriptor from which this witness table
  /// was generated.
  ConstTargetMetadataPointer
    Description;

public:
  const TargetProtocolConformanceDescriptor *getDescription() const {
    return Description;
  }
};

using WitnessTable = TargetWitnessTable;

继续看源码TargetProtocolConformanceDescriptor声明:

template 
struct TargetProtocolConformanceDescriptor final
  : public swift::ABI::TrailingObjects<
             TargetProtocolConformanceDescriptor,
             TargetRelativeContextPointer,
             TargetGenericRequirementDescriptor,
             TargetResilientWitnessesHeader,
             TargetResilientWitness,
             TargetGenericWitnessTable> {

  using TrailingObjects = swift::ABI::TrailingObjects<
                             TargetProtocolConformanceDescriptor,
                             TargetRelativeContextPointer,
                             TargetGenericRequirementDescriptor,
                             TargetResilientWitnessesHeader,
                             TargetResilientWitness,
                             TargetGenericWitnessTable>;
  friend TrailingObjects;

  template
  using OverloadToken = typename TrailingObjects::template OverloadToken;

public:
  using GenericRequirementDescriptor =
    TargetGenericRequirementDescriptor;

  using ResilientWitnessesHeader = TargetResilientWitnessesHeader;
  using ResilientWitness = TargetResilientWitness;
  using GenericWitnessTable = TargetGenericWitnessTable;

private:
  /// The protocol being conformed to.
  TargetRelativeContextPointer Protocol;
  
  // Some description of the type that conforms to the protocol.
  TargetTypeReference TypeRef;

  /// The witness table pattern, which may also serve as the witness table.
  RelativeDirectPointer> WitnessTablePattern;

  /// Various flags, including the kind of conformance.
  ConformanceFlags Flags;

public:
  ConstTargetPointer>
  getProtocol() const {
    return Protocol;
  }

  TypeReferenceKind getTypeKind() const {
    return Flags.getTypeReferenceKind();
  }

  const char *getDirectObjCClassName() const {
    return TypeRef.getDirectObjCClassName(getTypeKind());
  }

  const TargetClassMetadataObjCInterop *const *
  getIndirectObjCClass() const {
    return TypeRef.getIndirectObjCClass(getTypeKind());
  }

  const TargetContextDescriptor *getTypeDescriptor() const {
    return TypeRef.getTypeDescriptor(getTypeKind());
  }

  constexpr inline auto
  getTypeRefDescriptorOffset() const -> typename Runtime::StoredSize {
    return offsetof(typename std::remove_reference::type, TypeRef);
  }

  constexpr inline auto
  getProtocolDescriptorOffset() const -> typename Runtime::StoredSize {
    return offsetof(typename std::remove_reference::type, Protocol);
  }

  TargetContextDescriptor * __ptrauth_swift_type_descriptor *
  _getTypeDescriptorLocation() const {
    if (getTypeKind() != TypeReferenceKind::IndirectTypeDescriptor)
      return nullptr;
    return TypeRef.IndirectTypeDescriptor.get();
  }

  /// Retrieve the context of a retroactive conformance.
  const TargetContextDescriptor *getRetroactiveContext() const {
    if (!Flags.isRetroactive()) return nullptr;

    return this->template getTrailingObjects<
        TargetRelativeContextPointer>();
  }

  /// Whether this conformance is non-unique because it has been synthesized
  /// for a foreign type.
  bool isSynthesizedNonUnique() const {
    return Flags.isSynthesizedNonUnique();
  }

  /// Whether this conformance has any conditional requirements that need to
  /// be evaluated.
  bool hasConditionalRequirements() const {
    return Flags.getNumConditionalRequirements() > 0;
  }

  /// Retrieve the conditional requirements that must also be
  /// satisfied
  llvm::ArrayRef
  getConditionalRequirements() const {
    return {this->template getTrailingObjects(),
            Flags.getNumConditionalRequirements()};
  }

  /// Get the directly-referenced witness table pattern, which may also
  /// serve as the witness table.
  const swift::TargetWitnessTable *getWitnessTablePattern() const {
    return WitnessTablePattern;
  }

  /// Get the canonical metadata for the type referenced by this record, or
  /// return null if the record references a generic or universal type.
  const TargetMetadata *getCanonicalTypeMetadata() const;
  
  /// Get the witness table for the specified type, realizing it if
  /// necessary, or return null if the conformance does not apply to the
  /// type.
  const swift::TargetWitnessTable *
  getWitnessTable(const TargetMetadata *type) const;

  /// Retrieve the resilient witnesses.
  llvm::ArrayRef getResilientWitnesses() const {
    if (!Flags.hasResilientWitnesses())
      return { };

    return llvm::ArrayRef(
        this->template getTrailingObjects(),
        numTrailingObjects(OverloadToken()));
  }

  ConstTargetPointer
  getGenericWitnessTable() const {
    if (!Flags.hasGenericWitnessTable())
      return nullptr;

    return this->template getTrailingObjects();
  }

#if !defined(NDEBUG) && SWIFT_OBJC_INTEROP
  void dump() const;
#endif

#ifndef NDEBUG
  /// Verify that the protocol descriptor obeys all invariants.
  ///
  /// We currently check that the descriptor:
  ///
  /// 1. Has a valid TypeReferenceKind.
  /// 2. Has a valid conformance kind.
  void verify() const;
#endif

private:
  size_t numTrailingObjects(
                        OverloadToken>) const {
    return Flags.isRetroactive() ? 1 : 0;
  }

  size_t numTrailingObjects(OverloadToken) const {
    return Flags.getNumConditionalRequirements();
  }

  size_t numTrailingObjects(OverloadToken) const {
    return Flags.hasResilientWitnesses() ? 1 : 0;
  }

  size_t numTrailingObjects(OverloadToken) const {
    return Flags.hasResilientWitnesses()
      ? this->template getTrailingObjects()
          ->NumWitnesses
      : 0;
  }

  size_t numTrailingObjects(OverloadToken) const {
    return Flags.hasGenericWitnessTable() ? 1 : 0;
  }
};
using ProtocolConformanceDescriptor
  = TargetProtocolConformanceDescriptor;

第一个成员 TargetRelativeContextPointer Protocol; 是一个相对类型的指针,存储的是相对偏移量(在这篇文章介绍过我就不再讲解了)。

还需要看TargetProtocolDescriptor的声明:

/// A protocol descriptor.
///
/// Protocol descriptors contain information about the contents of a protocol:
/// it's name, requirements, requirement signature, context, and so on. They
/// are used both to identify a protocol and to reason about its contents.
///
/// Only Swift protocols are defined by a protocol descriptor, whereas
/// Objective-C (including protocols defined in Swift as @objc) use the
/// Objective-C protocol layout.
template
struct TargetProtocolDescriptor final
    : TargetContextDescriptor,
      swift::ABI::TrailingObjects<
        TargetProtocolDescriptor,
        TargetGenericRequirementDescriptor,
        TargetProtocolRequirement>
{
private:
  using TrailingObjects
    = swift::ABI::TrailingObjects<
        TargetProtocolDescriptor,
        TargetGenericRequirementDescriptor,
        TargetProtocolRequirement>;

  friend TrailingObjects;

  template
  using OverloadToken = typename TrailingObjects::template OverloadToken;

public:
  size_t numTrailingObjects(
            OverloadToken>) const {
    return NumRequirementsInSignature;
  }

  size_t numTrailingObjects(
            OverloadToken>) const {
    return NumRequirements;
  }


  /// The name of the protocol.
  TargetRelativeDirectPointer Name;

  /// The number of generic requirements in the requirement signature of the
  /// protocol.
  uint32_t NumRequirementsInSignature;

  /// The number of requirements in the protocol.
  /// If any requirements beyond MinimumWitnessTableSizeInWords are present
  /// in the witness table template, they will be not be overwritten with
  /// defaults.
  uint32_t NumRequirements;

  /// Associated type names, as a space-separated list in the same order
  /// as the requirements.
  RelativeDirectPointer AssociatedTypeNames;

  ProtocolContextDescriptorFlags getProtocolContextDescriptorFlags() const {
    return ProtocolContextDescriptorFlags(this->Flags.getKindSpecificFlags());
  }

  /// Retrieve the requirements that make up the requirement signature of
  /// this protocol.
  llvm::ArrayRef>
  getRequirementSignature() const {
    return {this->template getTrailingObjects<
                             TargetGenericRequirementDescriptor>(),
            NumRequirementsInSignature};
  }

  /// Retrieve the requirements of this protocol.
  llvm::ArrayRef>
  getRequirements() const {
    return {this->template getTrailingObjects<
                             TargetProtocolRequirement>(),
            NumRequirements};
  }

  constexpr inline auto
  getNameOffset() const -> typename Runtime::StoredSize {
    return offsetof(typename std::remove_reference::type, Name);
  }

  /// Retrieve the requirement base descriptor address.
  ConstTargetPointer>
  getRequirementBaseDescriptor() const {
    return getRequirements().data() - WitnessTableFirstRequirementOffset;
  }

#ifndef NDEBUG
  LLVM_ATTRIBUTE_DEPRECATED(void dump() const,
                            "only for use in the debugger");
#endif

  static bool classof(const TargetContextDescriptor *cd) {
    return cd->getKind() == ContextDescriptorKind::Protocol;
  }
};

TargetProtocolDescriptor继承自TargetContextDescriptor

/// Base class for all context descriptors.
template
struct TargetContextDescriptor {
  /// Flags describing the context, including its kind and format version.
  ContextDescriptorFlags Flags;
  
  /// The parent context, or null if this is a top-level context.
  TargetRelativeContextPointer Parent;

  bool isGeneric() const { return Flags.isGeneric(); }
  bool isUnique() const { return Flags.isUnique(); }
  ContextDescriptorKind getKind() const { return Flags.getKind(); }

  /// Get the generic context information for this context, or null if the
  /// context is not generic.
  const TargetGenericContext *getGenericContext() const;

  /// Get the module context for this context.
  const TargetModuleContextDescriptor *getModuleContext() const;

  /// Is this context part of a C-imported module?
  bool isCImportedContext() const;

  unsigned getNumGenericParams() const {
    auto *genericContext = getGenericContext();
    return genericContext
              ? genericContext->getGenericContextHeader().NumParams
              : 0;
  }

  constexpr inline auto
  getParentOffset() const -> typename Runtime::StoredSize {
    return offsetof(typename std::remove_reference::type, Parent);
  }

#ifndef NDEBUG
  LLVM_ATTRIBUTE_DEPRECATED(void dump() const,
                            "only for use in the debugger");
#endif

private:
  TargetContextDescriptor(const TargetContextDescriptor &) = delete;
  TargetContextDescriptor(TargetContextDescriptor &&) = delete;
  TargetContextDescriptor &operator=(const TargetContextDescriptor &) = delete;
  TargetContextDescriptor &operator=(TargetContextDescriptor &&) = delete;
};

根据源码就能分析出ProtocolBox的最后一个成员witness_table的数据结构:

// var circle1: Shape = Circle.init(10.0) 
// print(MemoryLayout.size(ofValue: circle1)) // 40 打印出circle1占据40个字节
struct ProtocolBox {
    var heapObject: UnsafeRawPointer
    var unknow1: UnsafeRawPointer
    var unknow2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    // IR分析出:它本是一个数组,就用一样大小和结构相同的结构体代替
    var witness_table: UnsafeMutablePointer 
}

struct TargetProtocolWitnessTable {
    var protocol_conformance_descriptor: UnsafeMutablePointer
    var witnessMethod: UnsafeRawPointer // 协议见证里的方法
}

struct TargetProtocolConformanceDescriptor {
    // TargetRelativeDirectPointer相对类型的指针,实质存放偏移量
    var protocolDescriptor: TargetRelativeDirectPointer
    var typeRef: UnsafeRawPointer
    var WitnessTablePattern: UnsafeRawPointer
    var flags: UInt32
}

struct TargetProtocolDescriptor {
    var flags: UInt32
    var parent: TargetRelativeDirectPointer
    var Name: TargetRelativeDirectPointer
    var NumRequirementsInSignature: UInt32
    var NumRequirements: UInt32
    var AssociatedTypeNames: TargetRelativeDirectPointer
}

struct TargetRelativeDirectPointer{
    var offset: Int32

    mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer{
        let offset = self.offset

        return withUnsafePointer(to: &self) { p in
           return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
        }
    }
}
  • 验证协议的本质:
var circle1: Shape = Circle.init(10.0)
// 拿到circle1堆区地址,然后内存绑定ProtocolBox类型
let circle1_ptr = withUnsafePointer(to: &circle1) { ptr in
    return ptr.withMemoryRebound(to: ProtocolBox.self, capacity: 1) { pointer in
        return pointer
    }
}

let desc = circle1_ptr.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.protocolDescriptor.getmeasureRelativeOffset()
print(String(cString: desc.pointee.Name.getmeasureRelativeOffset())) // Shape
print(circle1_ptr.pointee.witness_table.pointee.witnessMethod) // 0x0000000100004e40
print("end")

打印出 witnessMethod的地址是 0x0000000100004e40就是Circle类实现协议Shape的属性area的地址,如何证明呢?在Mach-O上能找到;又或者通过下面这个命令还原出来:

// 注意地址在Mach-O上是不带0x的
$ nm -p 可执行文件的路径 | grep 0000000100004e40 
// 输出了一个混写的名称 s11SwiftTest6CircleCAA5ShapeA2aDP4areaSdvgTW,把混写名称还原
$ xcrun swift-demangle s11SwiftTest6CircleCAA5ShapeA2aDP4areaSdvgTW 
// 最后得到下面的内容
// $s11SwiftTest6CircleCAA5ShapeA2aDP4areaSdvgTW ---> protocol witness for SwiftTest.Shape.area.getter : Swift.Double in conformance SwiftTest.Circle : SwiftTest.Shape in SwiftTest

总结:

  • 每个遵守了协议的类,都会有自己的Protocol Witness Table (PWT),遵守的协议越多,PWT中存储的函数地址就越多;
  • PWT的本质是一个指针数组,第一个成员存储TargetProtocolConformanceDescriptor,其后面存储的是实现协议函数的地址;
  • PWT的数量与协议数量一致。
2.ProtocolBox到底有什么作用呢?

先给总结:

  • Existential Container(即上面说的 ProtocolBox)是编译器生成的一种特殊的数据类型,用于管理class/struct/enum等遵守了同一个协议类型,因为这些class/struct/enum等类型的内存大小不一致,所以通过当前的Existential Container统一管理;
  • 对于小容量的数据,直接存储在Value Buffer;(小于等于24字节)
  • 对于大容量的数据,通过堆区分配,存储堆空间的地址。(大于24字节)

来看看下面这个案例,把Circle换成struct:

protocol Shape {
    var area: Double { get }
}

struct Circle: Shape {
    var radious: Double // 存储型属性 8
    
    init(_ radious: Double) {
        self.radious = radious
    }
    
    // 不是存储型属性
    var area: Double {
        get {
            return radious * radious * Double.pi
        }
    }
}

let circle: Shape = Circle(10.0)
print(MemoryLayout.size(ofValue: circle)) // 40

打印得出circle对象依旧是40个字节。

因为当实例变量声明为协议类型的时候,程序在编译时并不能推导出实例变量的真实类型,那就导致没有办法为实例变量分配具体的内存空间。
于是就不管它到底需要多大是什么类型统一塞到40字节的存在容器统一管理。
所以编译器就用一种特殊的数据类型Existential Container去统一管理遵循协议的class/struct/enum等(Existential Container本质是上面分析出的ProtocolBox)。

而我们ProtocolBox的前24个字节(前3个成员)是ValueBuffer,它就是负责存放值的连续内存空间。来看:

ValueBuffer1

可以看到第四个8字节存放的是依旧是metadata,是因为对于ProtocolBox它依旧需要记录实例变量真实类型的信息,此时它的metadata 就是 TargetStructMetadata,在真实调用实现协议的方法的时候就会找到这个metadata。

第一个8字节存放了radious的值是10,那如果我们还为Cirecle类添加两个成员呢?那就会沾满那前24字节ValueBuffer区域:

ValueBuffer2

那如果为Cirecle类添加超过三个成员变量那24字节必然是装不下的,又会有什么不一样?

ValueBuffer3

可以看到如果超过24个字节的话,那就需要把所有的内容移至堆区开辟一块内存空间去存放,ProtocolBox第一个成员就变成了一块堆区的地址,它存放了那五个成员变量的值。

以上就是分析协议的相关内容。

你可能感兴趣的:(Swift进阶-协议)