2、Swift类与结构体(下)

455 阅读8分钟

一、异变方法

1.1 异变方法

值类型属性不能被自身的实例方法修改

iShot2021-12-30 15.58.34.png 注意:moveBy是实例方法,不是构建器。

值类型如果想要实现能被自身的实例方法修改,那要在前面加一个mutating修饰 iShot2021-12-30 16.01.57.png

那么加不加mutating底层什么样?来加一个常规方法,然后通过SIL文件来看看:


// Point.move(_:)
sil hidden @$s4main5PointV4moveyySdF : $@convention(method) (Double, Point) -> () {
// %0 "deltaX"                                    // users: %4, %2
// %1 "self"                                      // user: %3
bb0(%0 : $Double, %1 : $Point):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %2
  debug_value %1 : $Point, let, name "self", argno 2 // id: %3
  debug_value %0 : $Double, let, name "u"         // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} // end sil function '$s4main5PointV4moveyySdF'


// Point.moveBy(_:_:)
sil hidden @$s4main5PointV6moveByyySd_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
  //省略部分代码
  return %26 : $()                                // id: %27
} // end sil function '$s4main5PointV6moveByyySd_SdtF'

我们都知道,默认方法还传递self参数,观察以上代码

区别一:mutating修饰的方法给Point前面加上了 inout关键字

区别二:move(_:) 方法的 %1 参数:$Point, let, name "self", argno 2 意思是selflet常量值

moveBy(::) 方法的 %2 参数:$*Point, var, name "self", argno 3 意思是selfvar变量值

也就是说,mutating修饰的方法的self传递的是指针的,所以能修改地址对应的值。(Ponit参数代指的就是self,所以我们在方法中可以使用self)

【注意,以上SIL代码中的s4main5PointV4moveyySdFs4main5PointV6moveByyySd_SdtF,分别是 move(:)函数 和 moveBy(:_:)函数,只是SIL混写后的名字,可以使用命令:xcrun swift-demangle <混写后的名称>对其还原查看】

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

1.2 inout

如果我们想让函数能够修改一个形式参数的值,而且希望这些改变在函数结束后依旧生效,那么就需要将形式参数定义成inout

struct Point{
    
    var x = 0.0, y = 0.0
    
    func move(_ deltaX:Double, _ deltaY: inout Double){
        
        let u = deltaX
        deltaY = 50
    }
}

看看SIL:

// Point.move(_:_:)
sil hidden @$s4main5PointV4moveyySd_SdztF : $@convention(method) (Double, @inout Double, Point) -> () {
// %0 "deltaX"                                    // users: %6, %3
// %1 "deltaY"                                    // users: %9, %4
// %2 "self"                                      // user: %5
bb0(%0 : $Double, %1 : $*Double, %2 : $Point):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
  debug_value_addr %1 : $*Double, var, name "deltaY", argno 2 // id: %4
  debug_value %2 : $Point, let, name "self", argno 3 // id: %5
  debug_value %0 : $Double, let, name "u"         // id: %6
  //省略代码
  return %12 : $()                                // id: %13
} // end sil function '$s4main5PointV4moveyySd_SdztF'

二、方法调度

先来点儿预备知识:寄存器指令

2.1 class方法调度

来一段测试代码:

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

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

下面通过两个角度来探索class的方法调度方式

1、lldb断点,观察寄存器指令

2、观察sil代码

2.1.1 LLDB断点,观察寄存器指令

来一波断点,运行真机 iShot2022-01-17 11.47.04.png

观察断点:刚好三个blr方法跳转,也就是方法调用

iShot2022-01-17 11.48.58.png

函数的实例对象是放在x0寄存器里的

mov    x20, x0

这句意思是把x0的值,复制到x20里,也就是把实例对象复制到x20

ldr    x8, [x20]

这句意思是取x20里的值,放到x8里面,那么现在x8存就是实例对象,取的是这个实例对象的前8个字节,因为寄存器是64位,存放8个字节,而x20的第一个8字节是什么?没错,metadata

iShot2022-01-17 15.27.42.png

ldr    x8, [x8, #0x50]

这句意思是把 x8#0x50 的值相加(偏移量),然后放到x8里面去

blr    x8

最后,调用x8

总结:class函数的调用过程:找到Metadata -> 确定函数地址(metadata+偏移量)-> 执行函数

2.1.2 函数表v_table

仔细看一下调用前相加的偏移量:

iShot2022-01-17 15.37.18.png

#0x50 #0x58 #0x60互相之间相差8个字节!那这8个字节就是函数指针的大小,且他们在内存中是连续的内存空间,到这里,就可以引出swift第一种函数调度方式:基于 函数表 v_table 的调度

编译个SIL来看看:

sil_vtable CCTeacher {
  #CCTeacher.teach: (CCTeacher) -> () -> () : @$s14ViewController9CCTeacherC5teachyyF	// CCTeacher.teach()
  #CCTeacher.teach1: (CCTeacher) -> () -> () : @$s14ViewController9CCTeacherC6teach1yyF	// CCTeacher.teach1()
  #CCTeacher.teach2: (CCTeacher) -> () -> () : @$s14ViewController9CCTeacherC6teach2yyF	// CCTeacher.teach2()
  #CCTeacher.init!allocator: (CCTeacher.Type) -> () -> CCTeacher : @$s14ViewController9CCTeacherCACycfC	// CCTeacher.__allocating_init()
  #CCTeacher.deinit!deallocator: @$s14ViewController9CCTeacherCfD	// CCTeacher.__deallocating_deinit
}

sil文件中,这个sil_vtable就是函数表,它就罗列出CCTeacher这个类有哪些函数

2.1.3从源码查询vtable

在上一篇中,我们把Swift类的本质整理出来了,里面有一个metadata,其中一个成员是:

var typeDescriptor: UnsafeMutableRawPointer

类、结构体、枚举 都有这个成员变量,是存放对自己的描述

在源码中找到typeDescriptor的定义,查找流程为:

  • 在HeapObject.h文件中找到HeapMetadata
using HeapMetadata = TargetHeapMetadata<InProcess>;

HeapMetadataTargetHeapMetadata的别名

  • 进入TargetHeapMetadata结构体 找到:
ConstTargetMetadataPointer<Runtime, TargetClassDescriptor> Description;

iShot2022-01-17 16.41.55.png

此时,发现 Description 是一个为 TargetClassDescriptor 的类,并且继承了一堆东西

iShot2022-01-17 16.43.59.png

其中成员整理后,可以得到这样的内容:

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
}

接着往下翻会发现TargetClassDescriptor有个别名:ClassDescriptor

using ClassDescriptor = TargetClassDescriptor<InProcess>;

全局搜索:ClassDescriptor,可以找到GenMeta.cpp文件,这里就是生成元数据的地方

iShot2022-01-17 16.53.06.png

进入GenMeta.cpp文件,定位到ClassContextDescriptorBuilder,就是它,创建的metadataDescriptor

往下翻,看到layout()方法

void layout() {
    super::layout();
    addVTable();
    addOverrideTable();
    addObjCResilientClassStubInfo();
}

它先调用了super::layout(),那先看看它父类吧:

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

它也调用了void layout()

void layout() {
    asImpl().addFlags();
    asImpl().addParent();
}

那行,串起来看,是不是似曾相识?

iShot2022-01-17 17.24.14.png

基本都对上了,也就是说,这个layout,就是在构建TargetClassDescriptor

关注到addVTable()函数来:

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

    if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal))
        IGM.emitMethodLookupFunction(getType());

    auto offset = MetadataLayout->hasResilientSuperclass()
                 ? MetadataLayout->getRelativeVTableOffset()
                 : MetadataLayout->getStaticVTableOffset();
    B.addInt32(offset / IGM.getPointerSize());
    B.addInt32(VTableEntries.size());
      
    for (auto fn : VTableEntries)
        emitMethodDescriptor(fn);
}

前面的代码不用管,直接看到B.addInt32,计算 offset 偏移量之后,添加到BB还添加了vtable的size,然后遍历了VTableEntries数组,调用emitMethodDescriptor(SILDeclRef fn)函数 添加了函数指针fn,这个B就是当前TargetClassDescriptor,也就是说,往B里面添加内容,就是往TargetClassDescriptor添加内容

2.1.4 在Mach-O中分析类方法

打开MachOView查看mach-o,查看一下虚拟内存的基地址:0x100000000

iShot2022-01-18 17.13.11.png

看到:Section64(__Text,__swift5_types)这个section,这里存放的是结构体、枚举、类的 Descriptor,前四个字节存的是类的Descriptor,也就是说 90 FB FF FF 就是 CCTeacher 的 Descriptor 的地址信息, 那如何验证它存的就是地址信息?

拿它跟前面的四个字节,也就是在mach-o中的偏移量相加,就能得出这个Descriptor在mach-o文件中的内存地址

iShot2022-01-18 15.24.16.png

由于这个地址是小端模式,所以从右往左读:FF FF FB 90

FFFFFB90 + 0000BBCC = 0x10000B75C

然后拿这个计算的结果,减去虚拟内存基地址,就可以得到Descriptor在mach-o文件中的内存地址

0x10000B75C - 0x100000000 = B75C

ok,到const段去找:

iShot2022-01-18 15.35.26.png

也就是说,从50开始,找个地方里面存的就是Descriptor的内容,也就是说50是的TargetClassDescriptor这个结构体的首地址,后面儿的内容应该要跟结构体的内容一一对应,既然如此,我对着TargetClassDescriptor数12个4字节,直接找到vtable(因为vtable排第13):

iShot2022-01-18 17.18.10.png

2.1.5 通过程序来验证Mach-O的分析

上面的分析得出teach()在mach-o对应的偏移地址是000B790

命令image list

iShot2022-01-18 16.12.30.png 这个 0x0000000100a8c000 就是程序运行的基地址也就是ASLR,就是这个项目加载进内存后,它的起始地址。

那我们拿这个程序运行的基地址加上teach()方法在mach-o中的偏移量000B790,就能得出teach()在内存中的地址

0x0000000100a8c000 + 000B790 = 0x100A97790

也就是说0x100A97790指向这个结构: iShot2022-01-18 16.20.14.png

再来看一下源码,看 TargetMethodDescriptor这个结构体,它表示的是swift方法的结构 iShot2022-01-18 16.23.05.png Flags标识这个方法是什么类型

class MethodDescriptorFlags {
public:
  typedef uint32_t int_type;
  enum class Kind {
    Method,
    Init,
    Getter,
    Setter,
    ModifyCoroutine,
    ReadCoroutine,
  };

Impl是一个相对指针,一个Offset

那也就是说我们刚才算出来的0x100A97790指向这个TargetMethodDescriptor这个结构,由于这个结构的首个成员是Flags,所以要做一个偏移,Flags是一个uint32的枚举,所以是4字节,要偏移4字节,又由于Impl数一个相对指针,存的内容是一个offset,所以要找到方法的imp就还得偏移上它存的内容,总结:

方法地址 = 0x100A97790 + flags(4字节) + impl

impl内容是什么?

iShot2022-01-18 16.40.30.png

前面的4个字节是flags,后面4个字节的内容就是impl,那么impl就是:FF FF C2 5C

0x100A97790 + 4 + FFFFC25C = 0x200A939F0

最后再减去mach-o虚内存的基地址0x100000000

0x200A939F0 - 0x100000000 = 0x100A939F0

0x100A939F0就是teach()函数地址

汇编打断点验证: iShot2022-01-18 16.57.11.png

2.2 struct方法调度

直接把上面的类换成struct,跑个断点看看

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

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

iShot2022-01-18 17.32.05.png 好家伙,直接bl函数地址,也就是直接调用函数了。。

也就意味着,编译之后,函数地址就确定了。因为结构体没有继承关系,它没有必要另外开辟内存空间来另外记录每个函数所在地址,所以编译链接完函数地址就确定了,直接优化成静态调用

三、函数派发方式

3.1 final

两个点要注意:

  • final修饰的类,不能够被继承
  • final修饰的方法、下标、属性,禁止被重写,对于被被final修饰的方法直接静态派发,不会添加到vtable中,且对objc运行时不可见。

3.2 dynamic

  • 函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表vtable派发。
  • 可跟 @_dynamicReplacement(for:) 配合使用
class CCTeacher {
    
    dynamic func teach(){
        print("teach")
    }
}

extension CCTeacher {
    //用teach3()替代teach()
    @_dynamicReplacement(for:teach())
    func teach3(){
        print("teach3")
    }
}

let t = CCTeacher()
t.teach()
//打印结果:teach3

3.3 @objc / @objc + dynamic

  • @objc 关键字可以将Swift函数暴露给objc运行时,从而使用runtime大法,如方法交换等,但依旧是函数表vtable派发,也就是说oc类依旧不能使用该swift函数。
class CCTeacher {
    
    @objc dynamic func teach(){
        print("teach")
    }
}
  • 要使oc类可以使用该swift函数,需要让swift类继承自NSObject

iShot2022-02-07 10.13.05.png

iShot2022-02-07 10.13.34.png

iShot2022-02-07 10.14.05.png @objc + dynamic 消息派发的方式,也就是oc中的消息传递。