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 <typename Runtime>
class TargetWitnessTable {
  /// The protocol conformance descriptor from which this witness table
  /// was generated.
  ConstTargetMetadataPointer<Runtime, TargetProtocolConformanceDescriptor>
    Description;

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

using WitnessTable = TargetWitnessTable<InProcess>;

继续看源码TargetProtocolConformanceDescriptor声明:

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

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

  template<typename T>
  using OverloadToken = typename TrailingObjects::template OverloadToken<T>;

public:
  using GenericRequirementDescriptor =
    TargetGenericRequirementDescriptor<Runtime>;

  using ResilientWitnessesHeader = TargetResilientWitnessesHeader<Runtime>;
  using ResilientWitness = TargetResilientWitness<Runtime>;
  using GenericWitnessTable = TargetGenericWitnessTable<Runtime>;

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

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

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

public:
  ConstTargetPointer<Runtime, TargetProtocolDescriptor<Runtime>>
  getProtocol() const {
    return Protocol;
  }

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

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

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

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

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

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

  TargetContextDescriptor<Runtime> * __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<Runtime> *getRetroactiveContext() const {
    if (!Flags.isRetroactive()) return nullptr;

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

  /// 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<GenericRequirementDescriptor>
  getConditionalRequirements() const {
    return {this->template getTrailingObjects<GenericRequirementDescriptor>(),
            Flags.getNumConditionalRequirements()};
  }

  /// Get the directly-referenced witness table pattern, which may also
  /// serve as the witness table.
  const swift::TargetWitnessTable<Runtime> *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<Runtime> *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<Runtime> *
  getWitnessTable(const TargetMetadata<Runtime> *type) const;

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

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

  ConstTargetPointer<Runtime, GenericWitnessTable>
  getGenericWitnessTable() const {
    if (!Flags.hasGenericWitnessTable())
      return nullptr;

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

#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<TargetRelativeContextPointer<Runtime>>) const {
    return Flags.isRetroactive() ? 1 : 0;
  }

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

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

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

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

第一个成员 TargetRelativeContextPointer<Runtime, TargetProtocolDescriptor> 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<typename Runtime>
struct TargetProtocolDescriptor final
    : TargetContextDescriptor<Runtime>,
      swift::ABI::TrailingObjects<
        TargetProtocolDescriptor<Runtime>,
        TargetGenericRequirementDescriptor<Runtime>,
        TargetProtocolRequirement<Runtime>>
{
private:
  using TrailingObjects
    = swift::ABI::TrailingObjects<
        TargetProtocolDescriptor<Runtime>,
        TargetGenericRequirementDescriptor<Runtime>,
        TargetProtocolRequirement<Runtime>>;

  friend TrailingObjects;

  template<typename T>
  using OverloadToken = typename TrailingObjects::template OverloadToken<T>;

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

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


  /// The name of the protocol.
  TargetRelativeDirectPointer<Runtime, const char, /*nullable*/ false> 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<const char, /*Nullable=*/true> AssociatedTypeNames;

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

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

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

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

  /// Retrieve the requirement base descriptor address.
  ConstTargetPointer<Runtime, TargetProtocolRequirement<Runtime>>
  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<Runtime> *cd) {
    return cd->getKind() == ContextDescriptorKind::Protocol;
  }
};

TargetProtocolDescriptor继承自TargetContextDescriptor

/// Base class for all context descriptors.
template<typename Runtime>
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<Runtime> 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<Runtime> *getGenericContext() const;

  /// Get the module context for this context.
  const TargetModuleContextDescriptor<Runtime> *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<decltype(*this)>::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<TargetProtocolWitnessTable> 
}

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

struct TargetProtocolConformanceDescriptor {
    // TargetRelativeDirectPointer相对类型的指针,实质存放偏移量
    var protocolDescriptor: 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))
        }
    }
}
  • 验证协议的本质:
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第一个成员就变成了一块堆区的地址,它存放了那五个成员变量的值。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容