Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析
-
class
是本质上定义了一个对象是什么; -
protocol
是本质上定义了一个对象有哪些行为。
一、协议的基本语法
- 协议要求一个属性必须明确是
get
或get 和 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文件中 class_method
的定义,得出logInfo
是通过函数表调度的方式。
此时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调用方式就变成了 witness_method
,看看官方声明:
意思就是当前调用logInfo
就要去协议见证表sil_witness_table
(简称PWT)上查找实现。
witness_method
其实是记录着类实现这个协议的方法的编码信息
接着我们继续找到sil里的witness_method
最终会通过找到这个对象的具体类型的具体实现。
结论:
一个类实现了协议上的方法,并且创建这个类的对象时,
1.如果把这个对象声明成本身类的类型,会通过v-Table
的方式调度函数;
2.如果把这个对象声明成协议类型,那么在对象调用实现协议的方法的时候会先去witness_table
查找这个对象的具体类型和具体实现(v-Table
),并完成当前的方法调度。
ps:
1.注意每一个遵循协议的类并实现了方法,都会为每一个类创建witness_table
;
2.举例Person类
遵循了ProtocolA
并实现了协议方法,而Teacher类
继承于Person类
且没有遵循ProtocolA
,Teacher类
就不会创建witness_table
;
3.举例Person类
遵循了ProtocolA
和ProtocolB
并都实现了协议方法,它会为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的内存格式化输出
- 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.为什么第一个成员heapObject
有metadata
,而第四个成员还存放一个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
,它就是负责存放值的连续内存空间。来看:
可以看到第四个8字节存放的是依旧是metadata
,是因为对于ProtocolBox
它依旧需要记录实例变量真实类型的信息,此时它的metadata
就是 TargetStructMetadata
,在真实调用实现协议的方法的时候就会找到这个metadata。
第一个8字节存放了radious的值是10,那如果我们还为Cirecle类添加两个成员呢?那就会沾满那前24字节ValueBuffer
区域:
那如果为Cirecle类添加超过三个成员变量那24字节必然是装不下的,又会有什么不一样?
可以看到如果超过24个字节的话,那就需要把所有的内容移至堆区开辟一块内存空间去存放,ProtocolBox
第一个成员就变成了一块堆区的地址,它存放了那五个成员变量的值。
以上就是分析协议的相关内容。