Swift进阶1.类与结构体(下)

312 阅读15分钟

类与结构体(下)

一、异变方法

上一篇文章我们了解到,Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况 下,值类型属性不能被自身的实例方法修改。

不添加mutating访问

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

报错: Left side of mutating operator isn't mutable: 'self' is immutable

原因是值类型本身是不允许修改属性的,因为x和y是属于self, self是let类型。

代码改成如下:

struct Point {
    var x = 10.0

    func moveBy(x deltaX: Double) {
        print(x)
    }
}

通过SIL来分析

// Point.moveBy(x:)
sil hidden @main.Point.moveBy(x: Swift.Double) -> () : $@convention(method) (Double, Point) -> () {
// %0 "deltaX"                                    // user: %2
// %1 "self"                                      // users: %10, %3
bb0(%0 : $Double, %1 : $Point):
  // 此时的self是let类型,即是不允许修改的
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %2
  debug_value %1 : $Point, let, name "self", argno 2 // id: %3
  %4 = integer_literal $Builtin.Word, 1           // user: %6
  ......

解决方式:方法用 mutating 关键字进行修饰就不报错了

添加mutating

struct Point {
    var x = 0.0, y = 0.0
    
    func test(){
        let tmp = self.x
    }
    
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

SIL文件

struct Point {
  @_hasStorage @_hasInitialValue var x: Double { get set }
  @_hasStorage @_hasInitialValue var y: Double { get set }
  func test()
  mutating func moveBy(x deltaX: Double, y deltaY: Double)
  init()
  init(x: Double = 0.0, y: Double = 0.0)
}


// Point.test()
// let self = Point 当前self是不可修改的
sil hidden @main.Point.test() -> () : $@convention(method) (Point) -> () {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Point):
  debug_value %0 : $Point, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $Point, #Point.x       // user: %3
  debug_value %2 : $Double, let, name "tmp"       // id: %3
  %4 = tuple ()                                   // user: %5
  return %4 : $()                                 // id: %5
} // end sil function 'main.Point.test() -> ()'


// Point.moveBy(x:y:)
// mutaing的本质:添加了inout输入输出
sil hidden @main.Point.moveBy(x: Swift.Double, y: Swift.Double) -> () : $@convention(method) (Double, Double, @inout Point) -> () {
// %0 "deltaX"                                    // users: %10, %3
// %1 "deltaY"                                    // users: %20, %4
// %2 "self"                                      // users: %16, %6, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*Point):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
  debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
  // let self = &Point 当前self是可修改的
  // self是var类型,可以修改,而且这里访问的地址,并不是原始的值
  debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
  %6 = begin_access [modify] [static] %2 : $*Point // users: %15, %7
  %7 = struct_element_addr %6 : $*Point, #Point.x // users: %13, %8
  %8 = struct_element_addr %7 : $*Double, #Double._value // user: %9
  %9 = load %8 : $*Builtin.FPIEEE64               // user: %11
  %10 = struct_extract %0 : $Double, #Double._value // user: %11
  %11 = builtin "fadd_FPIEEE64"(%9 : $Builtin.FPIEEE64, %10 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %12
  %12 = struct $Double (%11 : $Builtin.FPIEEE64)  // user: %13
  store %12 to %7 : $*Double                      // id: %13
  %14 = tuple ()
  end_access %6 : $*Point                         // id: %15
  %16 = begin_access [modify] [static] %2 : $*Point // users: %25, %17
  %17 = struct_element_addr %16 : $*Point, #Point.y // users: %23, %18
  %18 = struct_element_addr %17 : $*Double, #Double._value // user: %19
  %19 = load %18 : $*Builtin.FPIEEE64             // user: %21
  %20 = struct_extract %1 : $Double, #Double._value // user: %21
  %21 = builtin "fadd_FPIEEE64"(%19 : $Builtin.FPIEEE64, %20 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %22
  %22 = struct $Double (%21 : $Builtin.FPIEEE64)  // user: %23
  store %22 to %17 : $*Double                     // id: %23
  %24 = tuple ()
  end_access %16 : $*Point                        // id: %25
  %26 = tuple ()                                  // user: %27
  return %26 : $()                                // id: %27
} // end sil function 'main.Point.moveBy(x: Swift.Double, y: Swift.Double) -> ()'

通过sil文件我们发现

// Point.test()
sil hidden @main.Point.test() -> () : $@convention(method) (Point) -> () {
  // self是let类型, 不可修改的
  debug_value %0 : $Point, let, name "self", argno 1 // id: %1
  
简单理解就是: let self = Point


// Point.moveBy(x:y:)
sil hidden @main.Point.moveBy(x: Swift.Double, y: Swift.Double) -> () : $@convention(method) (Double, Double, @inout Point) -> () {
  // self是var类型,可以修改,而且这里访问的地址,并不是原始的值
  debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
  
简单理解就是: let self = &Point

总结:用 @inout 修饰接受的是一个地址,是可以修改的

举例:

struct Point {
    var x = 0.0, y = 0.0

    func test(){
        let tmp = self.x
    }

    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

var p = Point()
let x1 = p
var x2 = withUnsafePointer(to: &p ) { return $0 }
var x3 = p
p.x = 30.0
print(x2.pointee.x) // 30.0
print(x3.x)	// 0.0

发现修改 point 的值,x3不能被修改,x2 可以被修改

@inout

官方文档解释

An @inout parameter is indirect. The address must be of an initialized object.
当前参数类型是间接的,传递的是已经初始化过的地址

一般情况下,在函数的声明中,默认的参数都是不可变的,如果想要直接修改,需要给参数加上inout关键字

  • 未加inout关键字,给参数赋值,编译报错
var age = 10

//函数的形式参数都是let类型
func modifyage1(_ age: Int) {
    // age += 1  // 报错 Left side of mutating operator isn't mutable: 'age' is a 'let' constant
    var tmp = age
    tmp += 1
}
  • 添加inout关键字,可以给参数赋值
var age = 10

func modifyage(_ age: inout Int) {
    age += 1
}
modifyage(&age)
print(age) // 11

总结

  1. 结构体中的函数如果想修改其中的属性,需要在函数前加上mutating,而类则不用
  2. mutating本质也是加一个 inout修饰的self
  3. inout相当于取地址,可以理解为地址传递,即引用
  4. mutating修饰方法,而inout 修饰参数
  5. 对于变异方法, 传入的self被标记为inout参数。无论在mutating方法内部发生什么,都会影响外部依赖类型的一切。
  6. 如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为 输入输出形式参数。在形式参数定义开始的时候在前边添加一个inout关键字可以定义一个输入输出形式参数

二、方法调度

2.1 静态派发

值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用

我们先来看一下Swift中的方法调度

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

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        var t = Teacher()
        t.teach()   //断点
        t.teach1()  //断点
        t.teach2()  //断点
    }
}

2.1.1 汇编探索

1023

  • 上图中 x8x8x8分别代表teach()teach1()teach2()

  • 读取x8,进行验证:

    (lldb) register read x8
          x8 = 0x00000001041825d8  SwiftDemo`SwiftDemo.Teacher.teach() -> () at ViewController.swift:12
    
  • 同时我们看到函数调用前都有偏移的操作[x8, #0x50][x8, #0x58][x8, #0x60]

总结

所以函数的调用过程是: 找到Metadata,然后确定函数地址(metadata + 偏移量),最后在执行函数。编译后,我们看到函数地址已经确定的。

2.1.2 SIL验证

下面进行分析

添加Run Script

swiftc -emit-silgen -Onone -target arm64-apple-ios15.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/LGSwift/ViewController.swift > ./ViewController.sil && open ViewController.sil

# swiftc -emit-silgen -Onone -target x86_64-apple-ios15.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/LGSwift/ViewController.swift > ./ViewController.sil && open ViewController.sil

SIL文件如下所示

sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @$s14ViewController7TeacherC5teachyyF	// Teacher.teach()
  #Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF	// Teacher.teach1()
  #Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF	// Teacher.teach2()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @$s14ViewController7TeacherCACycfC	// Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @$s14ViewController7TeacherCfD	// Teacher.__deallocating_deinit
}

可以看到有 vtable函数表,罗列了类中所有的函数方法

2.1.3 源码分析

之前我们在上一篇文章讲到了Metdata的数据结构,那么V-Table是存放在什么地方那? 我们先来回顾一下当前的数据结构

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,就是对类的一个详细描述

打开源码,在 metadata.h 中找到 Description

template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
  using StoredPointer = typename Runtime::StoredPointer;
  using StoredSize = typename Runtime::StoredSize;

  TargetClassMetadata() = default;
  constexpr TargetClassMetadata(const TargetAnyClassMetadata<Runtime> &base,
             ClassFlags flags,
             ClassIVarDestroyer *ivarDestroyer,
             StoredPointer size, StoredPointer addressPoint,
             StoredPointer alignMask,
             StoredPointer classSize, StoredPointer classAddressPoint)
    : TargetAnyClassMetadata<Runtime>(base),
      Flags(flags), InstanceAddressPoint(addressPoint),
      InstanceSize(size), InstanceAlignMask(alignMask),
      Reserved(0), ClassSize(classSize), ClassAddressPoint(classAddressPoint),
      Description(nullptr), IVarDestroyer(ivarDestroyer) {}

  // The remaining fields are valid only when isTypeMetadata().
  // The Objective-C runtime knows the offsets to some of these fields.
  // Be careful when accessing them.

  /// Swift-specific class flags.
  ClassFlags Flags;

  /// The address point of instances of this type.
  uint32_t InstanceAddressPoint;

  /// The required size of instances of this type.
  /// 'InstanceAddressPoint' bytes go before the address point;
  /// 'InstanceSize - InstanceAddressPoint' bytes go after it.
  uint32_t InstanceSize;

  /// The alignment mask of the address point of instances of this type.
  uint16_t InstanceAlignMask;

  /// Reserved for runtime use.
  uint16_t Reserved;

  /// The total size of the class object, including prefix and suffix
  /// extents.
  uint32_t ClassSize;

  /// The offset of the address point within the class object.
  uint32_t ClassAddressPoint;
  
  // Description is by far the most likely field for a client to try
  // to access directly, so we force access to go through accessors.
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;
  ......


简化:
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
  ClassFlags Flags;
  uint32_t InstanceAddressPoint;
  uint32_t InstanceSize;
  uint16_t InstanceAlignMask;
  uint16_t Reserved;
  uint32_t ClassSize;
  uint32_t ClassAddressPoint;
private:
  TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description; //这里Description描述的类型是TargetClassDescriptor

这里我们看到Description,描述的类型是TargetClassDescriptor

搜索TargetClassDescriptor

using ClassDescriptor = TargetClassDescriptor<InProcess>;

ClassDescriptor 是它的一个别名,全局搜索,在 GenMeta.cpp 中找到下面的内容:

// swift/lib/IRGen/GenMeta.cpp

  class ClassContextDescriptorBuilder
    : public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder, ClassDecl>,
      public SILVTableVisitor<ClassContextDescriptorBuilder> {
  
  	.......

  	void layout() {
      super::layout();
      addVTable();
      addOverrideTable();
      addObjCResilientClassStubInfo();
      maybeAddCanonicalMetadataPrespecializations();
   	}
  	
  	......
  	
  	void addVTable() {
      LLVM_DEBUG(
        llvm::dbgs() << "VTable entries for " << getType()->getName() << ":\n";
        for (auto entry : VTableEntries) {
          llvm::dbgs() << "  ";
          entry.print(llvm::dbgs());
          llvm::dbgs() << '\n';
        }
      );

      // Only emit a method lookup function if the class is resilient
      // and has a non-empty vtable, as well as no elided methods.
      if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal)
          && (HasNonoverriddenMethods || !VTableEntries.empty()))
        IGM.emitMethodLookupFunction(getType());

      if (VTableEntries.empty())
        return;
      
      // 计算偏移量
      auto offset = MetadataLayout->hasResilientSuperclass()
                      ? MetadataLayout->getRelativeVTableOffset()
                      : MetadataLayout->getStaticVTableOffset();
      // B就是descriptor, 添加函数指针和函数的数量
      B.addInt32(offset / IGM.getPointerSize());
      B.addInt32(VTableEntries.size());
      
      for (auto fn : VTableEntries)
        emitMethodDescriptor(fn);
    }
   }
  
 
  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();
    }
  }

上面的代码这就是在创建descriptor,做了一些赋值的操作,我们也看到了addVTable()

还原出 TargetClassDescriptor 结构如下

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
    // 上面共12个4字节
    var size: UInt32
    // V-Table
}

2.1.4 Mach-O:

Mach-O其实是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式,类似于windows上的PE格式(Portable Executable), linux上的elf格式(Executable and Linking Format)。常⻅的 .o,.a .dylib Framework,dyld .dsym。

MachO文件格式:

1022

24

  • 首先是文件头,表明该文件是Mach-O格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排
  • Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
参数说明
LC_SEGMENT_64将文件中(32位或64位)的段映射到进程地址空间中
LC_DYLD_INFO_ONLY动态链接相关信息
LC_SYMTAB符号地址
LC_DYSYMTAB动态符号表地址
LC_LOAD_DYLINKERdyld加载
LC_UUID文件的UUID
LC_VERSION_MIN_MACOSX支持最低的操作系统版本
LC_SOURCE_VERSION源代码版本
LC_MAIN设置程序主线程的入口地址和栈大小
LC_LOAD_DYLIB依赖库的路径,包含三方库
LC_FUNCTION_STARTS函数起始地址表
LC_CODE_SIGNATURE代码签名
  • Data区主要就是负责代码和数据记录的。Mach-O是以Segment这种结构来组织数据的,一个Segment可以包含0个或多个Section。根据Segment是映射的哪一个Load Command,Segment中section就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据Segment做内存映射的。

Swift除了兼容了OC的存储结构外,还具备自己的存储结构,通过MachOView能看到Mach-O文件中存储了很多以swift5命名的section

1029

这些section中,__swift5_types中存储的是Class、Struct、Enum的地址。具体每个section存储Swift的哪些数据,在Swift metadata一文中有较为详细的描述。

如果此时你打开MachOView,查看__swift5_types的二进制数据后你会发现它与OC的存储有很大的不同。在OC中,存储地址通常都是8字节的直接存储对应的地址。但是types不是8字节地址,而是4字节,并且所存储的数据明显不是直接地址,而是相对地址。那么如何得出Teach类的地址呢?当前文件偏移 + 随后4字节中存储的value即可得到地址。

1048

为什么Swift要采用这种方式来存储数据呢?猜测是为了节省包大小,按照OC的存储习惯存储一个地址需要8字节,而在这里4字节就够了。 经过计算后可发现,Teach类的偏移位于__TEXT,__ __const中。

2.1.5 分析V-Table

  • Section64(_TEXT,__swift5_types) 中存放的就是 Descriptor

    计算 Descriptor 在 Mach-O 的内存地址:

    0xBB8C + 0xFFFFFBA4 =  0x10000B730
    

0x10000 是虚拟地址的开端,B730 就是 Descriptor 在 Mach-O 中的偏移量,定位位置如下:

1036

如图最上面红圈就是 Descriptor 的首地址,后面就是 Descriptor 结构体里面的内容,Descriptor 中有 13 个 UInt32,也就是13 个 4 字节。定位到下面的位置

B764 就是 teach() 在 Mach-O 文件中的偏移量, B764 + ASLR(随机偏移地址) 就是 teach() 的地址

通过 image list 命令得到 ASLR 程序运行的基地址 0x0000000100b0c000

1035

所以 teach() 函数的首地址是:

0x0000000100b0c000 + 0xB764 = 0x100B17764

在源码中找到下面的结构TargetMethodDescriptor

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

using MethodDescriptor = TargetMethodDescriptor<InProcess>;
  • 计算 Impl 的地址
0x100B17764 + 0x4 + 0xFFFFAE70 = 0x200B125D8
  • teach() 函数地址
0x200B125D8 - 0x100000000 = 0x100B125D8
  • 读取的 teach() 的地址:

1034

2.1.6 V-Table偏移

源码中搜索initClassVTable,并加上断点,然后写上源码进行调试

static void initClassVTable(ClassMetadata *self) {
  const auto *description = self->getDescription();
  auto *classWords = reinterpret_cast<void **>(self);

  if (description->hasVTable()) {
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    auto descriptors = description->getMethodDescriptors();
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
      auto &methodDescription = descriptors[i];
      swift_ptrauth_init_code_or_data(
          &classWords[vtableOffset + i], methodDescription.Impl.get(),
          methodDescription.Flags.getExtraDiscriminator(),
          !methodDescription.Flags.isAsync());
    }
  }

  if (description->hasOverrideTable()) {
    auto *overrideTable = description->getOverrideTable();
    auto overrideDescriptors = description->getMethodOverrideDescriptors();

    for (unsigned i = 0, e = overrideTable->NumEntries; i < e; ++i) {
      auto &descriptor = overrideDescriptors[i];

      auto *baseClass = cast_or_null<ClassDescriptor>(descriptor.Class.get());
      auto *baseMethod = descriptor.Method.get();

      if (baseClass == nullptr || baseMethod == nullptr)
        continue;

      auto baseClassMethods = baseClass->getMethodDescriptors();

      if (baseMethod < baseClassMethods.begin() ||
          baseMethod >= baseClassMethods.end()) {
        fatalError(0, "resilient vtable at %p contains out-of-bounds "
                   "method descriptor %p\n",
                   overrideTable, baseMethod);
      }

      auto baseVTable = baseClass->getVTableDescriptor();
      auto offset = (baseVTable->getVTableOffset(baseClass) +
                     (baseMethod - baseClassMethods.data()));

      swift_ptrauth_init_code_or_data(&classWords[offset],
                                      descriptor.Impl.get(),
                                      baseMethod->Flags.getExtraDiscriminator(),
                                      !baseMethod->Flags.isAsync());
    }
  }
}

其内部是通过for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,从这里可以印证函数是连续存放的

对于class中函数来说,类的方法调度是通过V-Table,其本质就是一个连续的内存空间(数组结构)

2.2 类类型

代码分析

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

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        var t = Teacher()
        t.teach()   //断点
        t.teach1()  
        t.teach2()  
    }
}

1037

teach、teach1、teach2都是函数表派发

新增extension函数调度

extension Teacher {
    func teach3() {
        print("teach3")
    }
}

1038

类类型中,teach、teach1、teach2函数表派发, teach3函数被优化成直接调用

2.3 值类型

把class改成struct

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

1039

我们可以看到值类型中,teach、teach1、teach2直接的地址调用

新增extension函数调度

extension Teacher {
    func teach3() {
        print("teach3")
    }
}

1040

我们可以看到值类型中,teach、teach1、teach2直接调用, teach3函数被优化成直接调用

2.4 NSObject子类

class Teacher: NSObject {
    func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
    func teach2() {
        print("teach2")
    }
}
extension Teacher {
    func teach3() {
        print("teach3")
    }
}

1041

class MarkTeacher: Teacher {
    func teach4() {
        print("teach4")
    }
}

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        var t = MarkTeacher()
        t.teach()   //断点
        t.teach1()
        t.teach2() 
        t.teach3()  
        t.teach4() 
    }
}

1042

teach、teach1、teach2、teach4函数表派发, teach3函数被优化成直接调用。teach4用空间换时间的方式

查看SIL文件

sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @$s14ViewController7TeacherC5teachyyF	// Teacher.teach()
  #Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF	// Teacher.teach1()
  #Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF	// Teacher.teach2()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @$s14ViewController7TeacherCACycfC	// Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @$s14ViewController7TeacherCfD	// Teacher.__deallocating_deinit
}

sil_vtable MarkTeacher {
  #Teacher.teach: (Teacher) -> () -> () : @$s14ViewController7TeacherC5teachyyF [inherited]	// Teacher.teach()
  #Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF [inherited]	// Teacher.teach1()
  #Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF [inherited]	// Teacher.teach2()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @$s14ViewController11MarkTeacherCACycfC [override]	// MarkTeacher.__allocating_init()
  #MarkTeacher.teach4: (MarkTeacher) -> () -> () : @$s14ViewController11MarkTeacherC6teach4yyF	// MarkTeacher.teach4()
  #MarkTeacher.deinit!deallocator: @$s14ViewController11MarkTeacherCfD	// MarkTeacher.__deallocating_deinit
}

我们发现子类只继承了class中定义的函数,即函数表中的函数

其原因是因为子类将父类的函数表全部继承了,如果此时子类增加函数,会继续在连续的地址中插入,假设extension函数也是在函数表中,则意味着子类也有,但是子类无法并没有相关的指针记录函数是父类方法还是子类方法,所以不知道方法该从哪里插入,导致extension中的函数无法安全的放入子类中。所以在这里可以侧面证明extension中的方法是直接调用的,且只属于类,子类是无法继承的

2.5 总结

方法调度方式总结:

类型调度方式extension
值类型静态派发静态派发
函数表派发静态派发
NSObject子类函数表派发静态派发

extension

  • 继承方法和属性,不能写在extension中。

  • 而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写

三、函数派发方式

3.1 final:

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

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        var t = Teacher()
        t.teach()   //断点
        t.teach1()
        t.teach2()
    }
}

1043

teach被优化成了直接地址调用, 就是静态派发。teach1、teach2函数调度都是首地址+偏移,也就是函数表派发

查看SIL文件

sil_vtable Teacher {
  #Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF	// Teacher.teach1()
  #Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF	// Teacher.teach2()
  #Teacher.deinit!deallocator: @$s14ViewController7TeacherCfD	// Teacher.__deallocating_deinit
}

添加了final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可⻅。

实际开发过程中属性,方法,类不需要被重载,可以使用final修饰

3.2 dynamic

函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。

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

1037

我们看到teach、teach1、teach2函数都是函数表派发

类继承自NSObject

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

1037

为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。

使用dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling

场景:swift中实现方法交换

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

extension Teacher {
    @_dynamicReplacement(for: teach)
    func teach3() {
        print("teach3")
    }
}

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        var t = Teacher()
        t.teach3()
        t.teach()
    }
}

输出:
teach
teach

3.3 @objc:

该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。

class Teacher: NSObject {
    @objc func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
    func teach2() {
        print("teach2")
    }
}
extension Teacher {
    @objc func teach3() {
        print("teach3")
    }
}

1044

1045

我们查看SIL文件

// Teacher.teach() swift中的函数
sil hidden [ossa] @$s14ViewController7TeacherC5teachyyF : $@convention(method) (@guaranteed Teacher) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : @guaranteed $Teacher):
  debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
  %2 = integer_literal $Builtin.Word, 1           // user: %4
  .......
} // end sil function '$s14ViewController7TeacherC5teachyyF'

// @objc Teacher.teach() OC中的函数,实际内部调用swift中的函数
sil hidden [thunk] [ossa] @$s14ViewController7TeacherC5teachyyFTo : $@convention(objc_method) (Teacher) -> () {
// %0                                             // user: %1
bb0(%0 : @unowned $Teacher):
  %1 = copy_value %0 : $Teacher                   // users: %6, %2
  %2 = begin_borrow %1 : $Teacher                 // users: %5, %4
  // function_ref Teacher.teach()
  %3 = function_ref @$s14ViewController7TeacherC5teachyyF : $@convention(method) (@guaranteed Teacher) -> () // user: %4
  %4 = apply %3(%2) : $@convention(method) (@guaranteed Teacher) -> () // user: %7
  end_borrow %2 : $Teacher                        // id: %5
  destroy_value %1 : $Teacher                     // id: %6
  return %4 : $()                                 // id: %7
} // end sil function '$s14ViewController7TeacherC5teachyyFTo'

即在SIL文件中生成了两个方法

  1. swift原有的函数
  2. @objc标记暴露给OC来使用的函数: 内部调用swift的

3.4 @objc + dynamic: 消息派发的方式

用 @objc + dynamic 修饰方法,我们就可以使用runtime的api

class Teacher {
    @objc dynamic func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
    func teach2() {
        print("teach2")
    }
}
extension Teacher {
    @objc dynamic func teach3() {
        print("teach3")
    }
}

1046

通过断点调试,teach使用消息派发的机制, 走的是objc_msgSend流程,即动态消息转发

原生的Swift类添加了@objc + dynamic,可以使用Runtime的方法进行交换

增加NSObject

class Teacher: NSObject {
    @objc dynamic func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
    func teach2() {
        print("teach2")
    }
}
class MarkTeacher: Teacher {
    @objc dynamic func teach4() {
        print("teach4")
    }
}

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        var t = MarkTeacher()
        t.teach()
        t.teach4()
    }
}

SwiftDemo-Swift.h

1047

原生的Swift类添加了@objc + dynamic,继承NSObject,可以做动态交换,还可以暴露给oc使用

四、函数内联

函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
  • always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
  • never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
  • 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))

如果对象只在声明的文件中可⻅,可以用 privatefileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明中访问)

class Person {
    private var sex: Bool
    
    private func unpdateSex() {
        self.sex = !self.sex
    }
    
    init(sex innerSex: Bool) {
        self.sex = innerSex
    }
    
    func test() {
        self.unpdateSex()
    }
}

let p = Person(sex: true)
p.test()

五、补充

5.1 汇编指令

  • blr:带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址
  • mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与起存起或者 寄存器与常量之间 传值,不能用于内存地址)
    • mov x1, x0 将寄存器x0的值复制到寄存器x1中
  • ldr:将内存中的值读取到寄存器中
    • ldr x0, [x1, x2] 将寄存器x1和寄存器x2 相加作为地址,取该内存地址的值翻入寄存器x0中
  • str:将寄存器中的值写入到内存中
    • str x0, [x0, x8] 将寄存器x0的值保存到内存[x0 + x8]处
  • bl:跳转到某地址

5.2 ASDL:

下面是针对函数地址的一个验证

  • 通过运行发现,Mach-O中的地址与调试时直接获取的地址是由一定偏差的,其主要原因是实际调用时地址多了一个ASLR(地址空间布局随机化 address space layout randomizes)
struct Teacher {
    var age: Int = 18
    
    func teach(){
        print("speak")
    }
}
var t = Teacher()
t.teach()

SwiftTest`main:
    0x100003a18 <+0>:   sub    sp, sp, #0x40             ; =0x40 
    0x100003a1c <+4>:   stp    x29, x30, [sp, #0x30]
    0x100003a20 <+8>:   add    x29, sp, #0x30            ; =0x30 
    0x100003a24 <+12>:  bl     0x100003c28               ; SwiftTest.Teacher.init() -> SwiftTest.Teacher at main.swift:11
    0x100003a28 <+16>:  mov    x8, x0
    0x100003a2c <+20>:  adrp   x9, 5
    0x100003a30 <+24>:  str    x9, [sp]
    0x100003a34 <+28>:  adrp   x0, 5
    0x100003a38 <+32>:  add    x0, x0, #0x150            ; =0x150 
    0x100003a3c <+36>:  str    x8, [x9, #0x150]
    0x100003a40 <+40>:  add    x1, sp, #0x18             ; =0x18 
    0x100003a44 <+44>:  str    x1, [sp, #0x8]
    0x100003a48 <+48>:  mov    w8, #0x20
    0x100003a4c <+52>:  mov    x2, x8
    0x100003a50 <+56>:  mov    x3, #0x0
    0x100003a54 <+60>:  bl     0x100003d98               ; symbol stub for: swift_beginAccess
    0x100003a58 <+64>:  ldr    x8, [sp]
    0x100003a5c <+68>:  ldr    x0, [sp, #0x8]
    0x100003a60 <+72>:  ldr    x8, [x8, #0x150]
    0x100003a64 <+76>:  str    x8, [sp, #0x10]
    0x100003a68 <+80>:  bl     0x100003db0               ; symbol stub for: swift_endAccess
    0x100003a6c <+84>:  ldr    x0, [sp, #0x10]
->  0x100003a70 <+88>:  bl     0x100003ab0               ; SwiftTest.Teacher.teach() -> () at main.swift:14 直接地址调用,即静态派发
    0x100003a74 <+92>:  mov    w0, #0x0
    0x100003a78 <+96>:  ldp    x29, x30, [sp, #0x30]
    0x100003a7c <+100>: add    sp, sp, #0x40             ; =0x40 
    0x100003a80 <+104>: ret 

1028

  • 可以通过image list查看,其中0x0000000100000000是程序运行的首地址,后8位是随机偏移00000000(即ASLR)

    (lldb) image list
    [  0] 54AE1596-8635-3684-9671-254ED47F9017 0x0000000100000000 /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/SwiftTest-ezgreflbigvpqjcibzqxeikfdqsy/Build/Products/Debug/SwiftTest 
    [  1] 38657979-1ABE-3C9A-BF64-EF3B746216AB 0x0000000100014000 /usr/lib/dyld 
    [  2] A23D1D3A-AD28-3AC2-AEAF-53F4B7A5B2F5 0x000000018a3f2000
    

    1027

  • 将Mach-O中的文件地址0x100003ab0 + 0x00000000 = 0x100003ab0,正好对应上面调用的地址

还可以通过终端命令nm,获取项目中的符号表

  • 查看符号表:
nm mach-o文件路径

// 获取所有符号
nm /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/SwiftTest-ezgreflbigvpqjcibzqxeikfdqsy/Build/Products/Debug/SwiftTest
  • 通过命令还原符号名称:
xcrun swift-demangle 符号

// 还原第一个符号
% xcrun swift-demangle s14ViewController7TeacherC5teachyyF
$s14ViewController7TeacherC5teachyyF ---> ViewController.Teacher.teach() -> ()