Swift 进阶 02: 类与结构体(下)

911 阅读9分钟

一、异变方法

1 mutating关键字

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

截屏2022-01-02 21.08.08.png

可得出,self不可修改,因为当前xy属于self

解决方式 在func关键字前加mutating,就可允许修改行为

截屏2022-01-02 21.20.14.png

  • 思考🤔:不添加mutating访问与添加mutating访问两者有什么本质区别❓

1.1 通过生成SIL文件分析

  • main.swift文件添加测试代码
struct Point {
    var x = 0.0,
        y = 0.0
    func test(){
        let tmp = self.x
        print(tmp)
    }
    mutating func moveBy(x deltaX: Double, y deltaY: Double){
        x += deltaX
        y += deltaY
    }
}
  • 生成SIL文件命令
swiftc -emit-sil main.swift >> main.sil

得到main.sil文件,通过sil文件对比分析如下

  • test 函数
// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Point):
  debug_value %0 : $Point, let, name "self", argno 1 // id: %1
  ......
}
  • moveBy函数
// Point.moveBy(x:y:)
sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@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
  debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
  ......
}

1.2 对比分析

  • test函数默认参数是Point, 接收的是结构体的实例(self,也就是值)
    • 赋值过程:let self = Poit
  • moveBy的默认参数是@inout Point, 接收的是地址
    • 赋值过程:var self = &Poit

注意:
let声明是 不可变的
var声明是 可变的

1.3 实例代码分析

var p = Point()
// let self = Poit
var x1 = p
// var sefl = &Poit
var x2 = withUnsafePointer(to: &p) { return $0}

var x3 = p
p.x = 30.0
print(x2.pointee.x)
print(x3.x)

输出结果:

30.0
0.0
  • 根据结果可得出修改Point的值:x2可修改,x3不可修改

mutating(异变方法)的本质: 对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切。

2. inout输入输出参数

2.1 inout SIL文档解释

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

实例代码分析

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

输出结果:

11
  • inout 地址的传递

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

二、方法调度

回顾OC语言,调用方法的本质是消息传递,底层是通过objc_mgsend消息机制的方法去调度

补充:常见汇编指令

  1. mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址),
mov x1, x0 将寄存器 x0 的值复制到寄存器x1中
  1. add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中
add x0, x1, x2      将寄存器x1 和 x2 的值相加后保存到寄存器 x0 中
  1. sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中
sub x0, x1, x2       将寄存器 x1 和 x2 的值相减后保存到寄存器 x0 中
  1. and: 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中
and x0, x0, #0x1      将寄存器 x0 的值和常量 1 按位与后保存到寄存器 x0 中
  1. orr: 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中
orr x0, x0, #0x1       将寄存器 x0 的值和常量 1 按位或 并将结果保存到寄存器 x0 中
  1. str : 将寄存器中的值写入到内存中
str x0, [x0, x8] ;     将寄存器 x0 中的值保存到栈内存 [x0 + x8] 处
  1. ldr: 将内存中的值读取到寄存器中,
ldr x0, [x1, x2]        将寄存器 x1 和寄存器 x2 的值相加作为地址,去改地址的值放入寄存器 x0 中
  1. cbz: 和 0 比较,如果结果为零就转移(只能跳到后面的指令) 
  2. cbnz: 和非 0 比较,如果结果非零就转移(只能跳到后面的指令) 
  3. cmp: 比较指令
  4. blr: (branch)跳转到某地址(无返回)
  5. bl: 跳转到某地址(有返回)
  6. ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中

2.1 汇编分析

新建Swift项目。定义LGTeacher类,定义teach函数。打上符号断点如下:

image.png

image.png

  • 运行查看汇编断点如下 image.png

  • 汇编分析,从初始化开始

    • 调用函数的初始化的指令bl, 有返回值。返回的是LGTeacher实例对象,函数的返回值放在x0寄存器中
    • x0的第一个8字节是: medata image.png
    • x8 + (0x50)偏移量 函数的调用地址 image.png
  • 可得出,函数调用前都有偏移量的操作:ldr    x8, [x8, #0x50]ldr    x8, [x8, #0x58]ldr    x8, [x8, #0x60]

teach函数的调用过程: 找到 Metadata 基于函数表的调度,确定函数地址(metadata + 偏移量), 执行函数

2.2 SIL验证

命令生成ViewController.sil文件

swiftc -emit-silgen -Onone -target x86_64-apple-ios13.3-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ./ViewController.sil

滑到ViewController.sil文件的最底部

sil_vtable LGTeacher {
  #LGTeacher.teach: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC5teachyyF // LGTeacher.teach()
  #LGTeacher.teach1: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC6teach1yyF // LGTeacher.teach1()
  #LGTeacher.teach2: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC6teach2yyF // LGTeacher.teach2()
  #LGTeacher.init!allocator: (LGTeacher.Type) -> () -> LGTeacher : @$s14ViewController9LGTeacherCACycfC // LGTeacher.__allocating_init()
  #LGTeacher.deinit!deallocator: @$s14ViewController9LGTeacherCfD // LGTeacher.__deallocating_deinit
}
sil_vtable ViewController {
  #ViewController.deinit!deallocator: @$s14ViewControllerAACfD // ViewController.__deallocating_deinit
}

vtable 叫函数表,sil_vtable含有类中的所以函数。

2.3 swift 源码分析 V-table

之前在上一篇章讲到了 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 ,就是对类的一个详细描述

  1. 打开Swift 源码分析

  2. 打开Metadata.h文件

  3. 找到TargetClassMetadata 结构体 image.png

  4. 在内部找到 Description 变量

image.png

点击 TargetClassDescriptor 进入结构体,搜索 TargetClassDescriptor,找到别名(ClassDescriptor)如下:

using ClassDescriptor = TargetClassDescriptor<InProcess>;

全局搜索ClassDescriptor别名,进入 GenMeta.cpp文件,定位到 ClassContextDescriptorBuilder类,这个类是描述建立在,创建metadataDescriptor类的地方.

image.png

进入layout方法

 void layout() {
      assert(!getType()->isForeignReferenceType());
      super::layout();
      addVTable();
      addOverrideTable();
      addObjCResilientClassStubInfo();
      maybeAddCanonicalMetadataPrespecializations();
}

在函数中实现了父类,点击super::layout()进入父类实现:

void layout() {
      asImpl().computeIdentity();
      super::layout();
      asImpl().addName();
      asImpl().addAccessFunction();
      asImpl().addReflectionFieldDescriptor();
      asImpl().addLayoutInfo();
      asImpl().addGenericSignature();
      asImpl().maybeAddResilientSuperclass();
      asImpl().maybeAddMetadataInitialization();
}

到此已推出部分的 TargetClassDescriptor类成员变量。返回子类进入addVTable函数,如下:

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.addInt32(offset / IGM.getPointerSize());
      B.addInt32(VTableEntries.size());
      
      for (auto fn : VTableEntries)
        emitMethodDescriptor(fn);
    }

addVTable函数分析:offset计算偏移量,调用addInt32添加偏移量到B(Descriptor);最后for循环添加函数的指针。

最终得出 TargetClassDescriptor结构如下:

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

2.4 什么是Mahco

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

Mahoc文件格式:

image.png

2.4.1 Header(头文件)

首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排

2.4.2 Load commands

Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。

image.png

2.4.3 Data数据

Data区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load CommandSegmentsection 就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。

2.2.4 MachOView工具展示项目Mach-O文件格式

image.png

  • 显示包内容 image.png
  • 拖入MachOView工具打开

image.png

  • Mach-O 文件格式如下 截屏2022-01-04 23.07.01.png

2.5 class 函数 Mach-O 文件分析

执行 class 函数源码

class LGTeacher{ // 继承关系吗?没有
    func teach() {
        print("teach")
    }
    func teach1(){
        print("teach1")
    }
    func teach2(){
        print("teach2")
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

2.5.1 查找函数在 Mach-O文件中的地址

image.png

__swift5_types 存放的是ClassStructEnumDescriptor


前4个字节F4 FB FF FF就是LGTeacherDescriptor信息, F4 FB FF FF加上pFile字段下的 0000BC58得到的就是DescriptorMach-O文件中的地址信息

由于当前ios是小端模式,需从右往左读;Descriptor地址信息相加等式:

FFFFFBF4+0000BC58 = 0x10000B84C

得出Descriptor的地址信息为 0x10000B84C0x100000000是 Mach-O 文件中虚拟内存的基地址

image.png

0x10000B84C - 0x100000000 = 0xB84C0xB84CLGTeacher的首地址,后面是 TargetClassDescriptor 结构体里面的内容

image.png

其中TargetClassDescriptor结构size前有12个字段,加上size13个,往后就是V-Tableteachteach1teach2方法结构体地址, 定位如下图所示:

截屏2022-01-05 00.03.09.png

最终teach函数在Mach-O文件偏移量为B880

2.5.2 验证当前函数Mach-O中文件地址在程序运行中的地址

teach()在程序中的运行地址:B880 + ASLR(随机偏移地址)

断点执行函数,执行lldb 函数 image list得到 ASLR(程序运行的基地址) 0x0000000104da8000

image.png

得出teach()函数的结构地址为0x0000000104da8000 + B880 = 0x104DB3880,下图所示:

image.png

在源码中找到TargetMethodDescriptor结构体,是 Swift 的方法在内存中的结构

template <typename Runtime>
struct TargetMethodDescriptor {
  /// Flags describing the method. 
  // 4 字节
  MethodDescriptorFlags Flags;

  /// The method implementation.
  // offset
  TargetRelativeDirectPointer<Runtime, void> Impl;

  // TODO: add method types or anything else needed for reflection.
};
  • Impl地址: 0x104DB3880 + 0x4 + 0xFFFFB9D4 = 0x204DAF258
    • 0x4: Flags
    • 0xFFFFB9D4: Offset (D4 B9 FF FF) 小端模式从右往左
  • teach函数的地址为:0x204DAF258 - 0x100000000 = 0x104DAF258
    • 0x100000000:  Mach-O 文件中虚拟内存的基地址

打开汇编调试模式,读取汇编中的teach函数地址,如下图所示:

image.png

最终得出-> 0x104DAF258teach函数的地址,V-Table 是在 Descriptor 结构的后面,并且 Swift 类的方法是存放在 V-Table函数表中。

؏؏☝ᖗ乛◡乛ᖘ☝؏؏

2.5.3 为啥 MethodData + Offset (偏移操作)?

image.png

函数存储到vtable后,进行了vtableOffset偏移,所以取的时候也要进行偏移


2.6 struct 函数调度

struct LGTeacher{ // 继承关系吗?没有
    func teach() {
        print("teach")
    }
    func teach1(){
        print("teach1")
    }
    func teach2(){
        print("teach2")
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

汇编调试结果: image.png struct函数调用,编译完成之后,当前函数内的地址已经确定了,就是静态派发

2.7 structextension 函数调度

struct LGTeacher 添加 extension

struct LGTeacher{ 
    ......
}
extension LGTeacher {
    func teach3(){
        print("teach2")
    }
}

汇编调试结果:

image.png

teach3 直接是地址调用,extension 在 struct 也是 静态派发

2.8 classextension 函数调度

class LGTeacher 添加 extension实现类

class LGTeacher{ 
    ......
}
extension LGTeacher {
    func teach3(){
        print("teach2")
    }
}

汇编调试结果:

image.png teach3 没有在 V-Table函数表中,而是直接地址调用,extension 在 class 也属于静态派发

方法调度方式总结

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

三、影响函数派发方式

3.1 final关键字

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

image.png 汇编调试结果:

image.png SIL分析(无teach函数):

image.png 添加了final关键字, 从sil_vtable中移除掉了,优化成了直接调用

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

3.2 dynamic关键字

  • dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
class LGTeacher{ // 继承关系吗?没有
    dynamic func teach() {
        print("teach")
    }
}

extension LGTeacher {
    @_dynamicReplacement(for: teach)
    func teach3(){
        print("teach3")
    }
}
class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
    }
}

输出结果:

teach3

3.3 @objc关键字

  • 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
class LGTeacher : NSObject {
    @objc func teach() {
        print("teach")
    }
}
class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
    }
}

查看暴露给Objc的方式 image.png

查看暴露给Objc的API image.png

3.4 @objc + dynamic关键字

  • 消息派发的方式
class LGTeacher{ // 继承关系吗?没有
    @objc dynamic func teach() {
        print("teach")
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
    }
}

汇编调试:

image.png


四、函数内联

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

4.1 OC项目编译器优化

int sum(int a, int b) {
    return a + b;
}
int main(int argc, char * argv[]) {
    int a = sum(3, 5);
    NSLog(@"%d", a);
}

未优化

Debug 默认None 模式 进行测试

image.png

汇编调试:

image.png

先把35分别存入w0w1,在进行sum计算

优化
Debug 选择 Fastest, Samallest 模式 进行测试

image.png 汇编调试:

image.png

得出,直接将计算结果存入到w8,调用NSLog函数输出。


4.2 Swift 函数内联

优化设置

image.png

  1. 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。


  2. always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为

@inline(__always) func test() {
    print("test")
}
  1. never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现
@inline(never) func test() {
    print("test")
}
  1. 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))

4.3 Private 修饰函数

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

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