Swift类与结构体(二)

707 阅读6分钟

一、异变方法

Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况下,struct内属性不能被自身的实例方法修改。

struct Point {
    var x = 0.0, y = 0.0
    func movePoint(x deltaX: Double, y deltaY: Double) {
        x += deltaX 
        y += deltaY
    }
}

编译运行,会看到错误警告“Left side of mutating operator isn't mutable: 'self' is immutable”。此时可以在方法前加“mutating”关键字来进行修改。

//添加 mutating关键字
struct Point {
    var x = 0.0, y = 0.0
    mutating func movePoint(x deltaX: Double, y deltaY: Double) {
        x += deltaX 
        y += deltaY
    }
}

我们通过生成的.sil文件来看下添加mutating后的方法与之前的方法有什么区别。(生成Swift sil文件的脚本命令:swiftc main.swift -emit-sil),可以使用Xcode打开生成的.sil文件。

1、未添加“mutating”关键字 2.jpg 2、添加关键字“mutating”关键字后 1.jpg 可以看到参数内都会有个Point,但是添加关键字“mutating”后Point前多了一个@inout ,@inout是什么呢,sil文档的说明:An @inout parameter is indirect. The address must be of an initialized object.(当前参数类型是间接的,传递的是已经初始化过的地址)。接着看箭头%5所指的地方,未添加“mutating”前是let self = Ponit,添加“mutating”后是var self = &Point。

通过以上分析,我们可以得出异变方法的本质:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切。

二、输入输出参数

如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。在形式参数定义开始的时候在前边 添加一个"inout"关键字可以定义一个输入输出形式参数:

var age = 26
func editeAge(age: inout Int)
{
    //修改完后 会直接影响外部age的数值
    age = 18;
}

三、方法调用

我们知道OC中的方法调用本质是objc_msgSend, 那Swift中的方法调用是个什么过程呢?

class Teacher{
    func teach(){
        print("teach")
    }
    func teach1(){
        print("teach1")
    }
    func teach2(){
        print("teach2")
    }
}

我们看下生成的.sil文件 image.png 可以看到3个方法是在vtable中,我们打开Swift源码文件。metadata数据结构如下

struct Metadata{
    var kind: Int
    var superClass: Any.Type 
    var cacheData: (Int, Int) 
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer 
    var iVarDestroyer: UnsafeRawPointer

这里我们有一个东⻄需要关注“typeDescriptor”,不管是 Class,Struct, Enum 都有自己的 Descriptor,就是对类的一个详细描述

struct TargetClassDescriptor{ 
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32 
    var metadataPositiveSizeInWords: UInt32 
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32 
    var Offset: UInt32
    var size:UInt32
    //V-Table
}

1、打开Swift源码后我们先找到metadata

struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> 
{
  using StoredPointer = typename Runtime::StoredPointer;
  using StoredSize = typename Runtime::StoredSize;
  ......
private:
  /// An out-of-line Swift-specific description of the type, or null
  /// if this is an artificial subclass.  We currently provide no
  /// supported mechanism for making a non-artificial subclass
  /// dynamically.
  //类的描述信息
  TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;
  ....
 }

2、点击进入类的描述信息“TargetClassDescriptor”内

class TargetClassDescriptor final
: public TargetTypeContextDescriptor<Runtime>,
      public TrailingGenericContextObjects<TargetClassDescriptor<Runtime>,                             TargetTypeGenericContextDescriptorHeader,
                        .....
}

在这个类内部发现没有vtable,我们尝试内部搜索下“TargetClassDescriptor”,发现有个

using ClassDescriptor = TargetClassDescriptor<InProcess>;

3、全局搜索“ClassDescriptor”,定位“GenMeta.cpp”文件

class ClassContextDescriptorBuilder: public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder,
ClassDecl>, public SILVTableVisitor<ClassContextDescriptorBuilder>
  {
    using super = TypeContextDescriptorBuilderBase;
    ClassDecl *getType() {
      return cast<ClassDecl>(Type);
    }
    // Non-null unless the type is foreign.
    ClassMetadataLayout *MetadataLayout = nullptr;
    Optional<TypeEntityReference> ResilientSuperClassRef;
    SILVTable *VTable;
    bool Resilient;
    bool HasNonoverriddenMethods = false;
    ....
    
    void layout() {
      super::layout();
      addVTable();
      addOverrideTable();
      addObjCResilientClassStubInfo();
      maybeAddCanonicalMetadataPrespecializations();
    }
}

layout先调用父类“TypeContextDescriptorBuilderBase”的layout创建Descriptor

class TypeContextDescriptorBuilderBase: public ContextDescriptorBuilderBase<Impl> {
    ......
    void layout() {
      asImpl().computeIdentity();
      super::layout();
      asImpl().addName();
      asImpl().addAccessFunction();
      asImpl().addReflectionFieldDescriptor();
      asImpl().addLayoutInfo();
      asImpl().addGenericSignature();
      asImpl().maybeAddResilientSuperclass();
      asImpl().maybeAddMetadataInitialization();
    }
}

接着调用自己的vatable方法。 B代表当前的Descriptor,先设置vtable的size,接着遍历vatble,添加函数指针。

void addVTable() {
      ......
      
      if (VTableEntries.empty())
        return;

      auto offset = MetadataLayout->hasResilientSuperclass()
                      ? MetadataLayout->getRelativeVTableOffset()
                      : MetadataLayout->getStaticVTableOffset();

      B.addInt32(offset / IGM.getPointerSize());
      B.addInt32(VTableEntries.size());

      for (auto fn : VTableEntries)
        emitMethodDescriptor(fn);
}

经过以上分析,Descriptor加上偏移量就是方法的起始地址。下面我们可以通过分析macho来验证以上过程。 截屏2021-12-29下午5.36.02.png 类,结构体,enum地址信息都存放在Section64(__TEXT__swift5_types)里,可以计算下0xFFFFFBF4 + 0xBC68 = 0x10000B85C 0x10000在Swift里是虚拟内存的地址 截屏2021-12-29下午5.39.57.png 0xB85C 就是 Descriptor在data里的内存地址 截屏2021-12-29下午5.42.51.png 该内存地址偏移13个4字节(TargetClassDescriptor类中的13个UInt32)得到vtable的地址 0xB890(macho中的地址),再加上ASLR得到实际地址 截屏2021-12-29下午6.00.29.png 0x0000000100044000 + 0xB890 = 0x10004F890(teach函数的TargetMethodDescriptor地址)

struct TargetMethodDescriptor {
    MethodDescriptorFlags Flags; // 4
    TargetRelativeDirectPointer<Runtime, void> Impl; // offset
};

0x10004F890 + 0x4(Flags) = 0x10004F894 0x10004F894 + 0xFFFFC220 = 0x20004BAB4(0x10004BAB4 teach函数地址)

打开Debug-Debug Workflow-Always ShowDisassembly,运行程序。 截屏2021-12-29下午6.17.44.png 汇编指令:

  • mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址),如:
    mov x1, x0 􏰀􏰁􏰂􏰃 将寄存器x0的值 􏰄􏰅􏰆􏰇赋值到􏰈􏰁􏰂􏰃x1􏰉中
  • b: 跳转到某地址(无返回)
  • bl: 跳转到某地址(有返回)
  • ldr: 将内存中的值读取到寄存器中,如:
    ldr x0, [x1, x2] 􏰀􏰁􏰂􏰃 将寄存器x1和寄存器􏰊􏰁x2的值􏰄􏰅􏰋􏰌􏰙􏰚􏰛􏰜􏰝􏰞􏰟􏰗􏰂􏰛􏰜􏰄􏰅􏰠􏰡相加作为地址,取该内存地址的值放入寄存器 􏰁x0中

teach函数的地址与我们通过分析macho文件得出的结论一致。我们以上是分析的类的方法存储及调用过程,那结构体的方法是否跟类的方法是一致的呢?

struct Teacher{
    func teach(){
        print("teach")
    }
    func teach1(){
        print("teach1")
    }
    func teach2(){
        print("teach2")
    }
}

我们同样编译成sil文件发现无vtable信息,用汇编调试发现就是函数直接调用,跟类不同。struct无继承关系,编译完成时信息已确定。

4、影响函数派发方式

  • final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。
  • dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
class Teacher{ 
     dynamic func teach(){
        print("teach")
    }
}
extension Teacher
{
  //替换teach,在调用teach或者teach3都会打印"teach3"
    @_dynamicReplacement(for: teach)
    func teach3(){
        print("teach3")
    }
}
  • @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
  • @objc + dynamic: 消息派发的方式
  • extension: 类加extension后里面定义的方法不会存到vatble里,跟结构体内方法一样,直接调用。