Swift进阶04:方法调度

静态派发

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

  • 结构体函数符号调试如下:
静态派发
  • 打开Mach-O可执行文件,其中的 __text段,就是所谓的代码段,需要执行的汇编指令都在这里
Mach-O文件

对于上面的分析,有个疑问:直接地址调用后面是符号,这个符号是怎么来的?

符号

  • 是从Mach-O文件的 符号表 Symbol Table,但是符号表中并不存储字符串,字符串存储在 字符串表 String Table(存放所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值字符串表中查找对应的字符,然后进行命名重整
image
  • Symbol Table: 存储符号位于字符串表的位置
  • Dynamic Symbol Table: 动态库函数位于符号表的偏移位置

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

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

  • 通过命令还原符号名称:xcrun swift-demangle 符号

  • Edit Scheme 中的 Debug改成 Release,编译后查看,在可执行文件目录下,多了一个后缀为 dSYM的文件,此时,再去 Mach-o文件中查找 teach符号,发现是找不到的。 其主要原因是因为静态链接的函数,实际上是不需要符号的,一旦编译完成,其地址确定后,当前的符号表就会删除当前函数对应的符号,在release环境下,符号表中存储的只是不能确定地址的符号

  • 对于不能确定地址的符号,是在运行时确定的,即函数第一次调用时(相当于懒加载),例如 print,是通过 dyld_stub_bind确定地址的

函数符号命名规则

  • 对于C函数来说,命名的重整规则就是在函数名之前加 _(注意:C中不允许函数重载,因为没有办法区分)
#include <stdio.h>
void test(){}
image
  • 对于OC来说,也不支持函数重载,其符号命名规则是-[类名 函数名]
  • 对于Swift来说,是允许函数重载,主要是因为swift中的重整命名规则比较复杂,可以确保函数符号的唯一性

ASLR(随机地址偏移)

新建一个iOS项目,在 ViewController中定义一下代码

struct HTStack {
    func teacher() {
        print("teacher")
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let t = HTStack()
        t.teacher()
        print("end")
    }
}
  • 运行上述代码,查看mach-o文件,发现mach-o文件中的地址 与 函数调用的地址不一致,主要原因是实际调用时地址多了一个 ASLR(地址空间布局随机化 address space layout randomizes)
ASLR
  • mach-o文件中查看,程序运行静态基地址(VM address) 是 0x0000000100000000
image
  • 可以通过image list查看,其中 0x100000000程序运行的首地址,后八位是随机地址偏移 0x20ef000(即 ASLR)
image
  • 函数地址等于 0x100000000(程序运行首地址)+ 0x20ef000(ASLR) + 0x3A30(符号表地址偏移)= 0x1020F2A30
image

动态派发

汇编指令补充

ARM64汇编指令

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

探索class的调度方式

首先介绍下V_Table在SIL文件中的格式

//声明sil vtable关键字
decl ::= sil-vtable
//sil vtable中包含 关键字、标识(即类名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了声明以及函数名称
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name

例如,以HTTacher为例,其SIL中的v-table如下所示

class HTTeacher {
    func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
image
  • sil_vtable:关键字
  • HTTeacher:表示是 HTTeacher类的函数表
  • 其次就是当前方法的声明对应着方法的名称
  • 函数表 可以理解为 数组,声明在 class内部的方法在不加任何关键字修饰的过程中,是连续存放在我们当前的地址空间中的。这一点,可以通过断点来印证

函数表源码探索

下面来进行函数表底层的源码探索

  • 源码中搜索 initClassVTable,并加上断点,然后写上源码进行调试
    image
  • 其内部是通过 for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,从这里可以验证函数是连续存放的
  • 对于class中函数来说,类的方法调度是通过V-Taable,其本质就是一个连续的内存空间(数组结构)。

问题:如果更改方法声明的位置呢?例如 extension中的函数,此时的函数调度方式还是函数表调度吗?

  • 定义一个 HTTeacher的 extension
extension HTTeacher {
    func teacher4() { print("teacher4") }
}
  • 再定义一个子类 HTStudent继承自 HTTeacher,查看SIL中的 V-Table
class HTStudent: HTTeacher {}
  • 查看 SIL文件,发现子类只继承了class中定义的函数,即函数表中的函数
    image

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

开发注意点:

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

final、@objc、dynamic修饰函数

final 修饰

  • final 修饰的方法是 直接调度的,可以通过SIL验证 + 断点验证
class HTTeacher {
    final func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
image

@objc 修饰

  • 使用 @objc关键字是将 swift中的方法暴露给OC
class HTTeacher {
    @objc func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
  • 通过SIL+断点调试,发现 @objc修饰的方法是 函数表调度
    image

【小技巧】混编头文件查看方式:查看项目名-Swift.h头文件

image

  • 如果只是通过 @objc修饰函数,OC还是无法调用swift方法的,因此如果想要 OC访问swift,class需要继承NSObject
<!--swift类-->
class HTTeacher: NSObject {
    @objc func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}

<!--桥接文件中的声明-->
SWIFT_CLASS("_TtC11HTSwiftDemo9HTTeacher")
@interface HTTeacher : NSObject
- (void)teacher;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

查看 SIL文件发现被 @objc修饰的函数声明有两个:swift + OC(内部调用的swift中的teach函数)

image

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

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

dynamic 修饰

以下面代码为例,查看 dynamic修饰的函数的调度方式

class HTTeacher: NSObject {
    dynamic func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
  • 其中 teach函数的调度还是 函数表调度,可以通过断点调试验证,使用 dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用 method-swizzling

@objc + dynamic

class HTTeacher: NSObject {
    @objc dynamic func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
  • 通过断点调试,走的是 objc_msgSend流程,即 动态消息转发
    image

swift中实现方法交换

在swift中的需要交换的函数前,使用dynamic修饰,然后通过: @_dynamicReplacement(for: 函数符号)进行交换,如下所示

class HTTeacher {
    dynamic func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}

extension HTTeacher {
    @_dynamicReplacement(for: teacher)
    func teacher5() {
        print("teacher5")
    }
}

var t = HTTeacher()
t.teacher()

teacher()方法替换成了 teacher5

image

  • 如果 teacher()方法没有实现 或者 没有dynamic修饰符,会报错
    image

方法调度总结

  • struct值类型,其中函数的调度属于直接调用地址,即静态调度
  • class引用类型,其中函数的调度是通过V-Table函数表来进行调度的,即动态调度
  • extension中的函数调度方式是直接调度
  • final修饰的函数调度方式是直接调度
  • @objc修饰的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject
  • dynamic修饰的函数的调度方式是函数表调度,使函数具有动态性
  • @objc + dynamic 组合修饰的函数调度,是执行的是 objc_msgSend流程,即 动态消息转发

内存插件 libfooplugin.dylib的使用

安装和使用

方式一

  • 在根目录下创建 .lldbinit 􏰈􏰉􏰊文件: vim /.lldbinit
  • 然后输入 plugin load libfooplugin.dylib路径

方式二

  • 在通过lldb调试的时候,直接输入 plugin load libfooplugin.dylib路径

使用

  • lldb环境下,通过 cat address 地址使用

内存分区调试实践

堆区

对于堆区的内存来说,就是通过 new & malloc 关键字来申请的内存空间,不连续,类似链表的结构,最直观的就是类的实例对象。
定义代码如下,通过cat查看类实例的内存分区

class HTTeahcer {
    func teacher() {
        print("teacher")
    }
}

var t = HTTeahcer()

image

从上图可以看出,类的实例对象存储在堆区,即 heap pointer

栈区

查看以下代码的 age内存地址位于哪个区?

func test() {
    // 我们在函数内部声明的age变量就是一个局部变量
    var age: Int = 18
    print(age)
}

test()

image

从结果来看,age位于栈区,即 stack address,此处的age如果用 let修饰,取不到地址

全局区

对于C的分析

下面是C语言的部分代码,查看其变量的内存地址

//全局已初始化变量
int a = 10;
//全局未初始化变量
int age;

//全局静态变量
static int age2 = 30;

int main(int argc, const char * argv[]) {
    
    char *p = "CJLTeacher";
    printf("%d", a);
    printf("%d", age2);
    return  0;
}
  • 查看 a(全局已初始化变量)的内存地址

    image

    其中 __DATA.__data表示 segment.section,这里的位置和全局区并不冲突,因为一个是人为的内存分配(内存布局分区),一个是 Mach-O的 segment.section段中,是文件的格式划分
    image

  • 查看 age(全局未初始化变量)的内存地址

    image

    age在Mach-O文件中,放在了__DATA.__common段,主要放的就是未初始化的符号声明(mach-o相比内存划分更细,主要是为了更好的定位符号),当然此时的 age在内存中依然在全局区

  • 查看 age2(全局已初始化静态变量)的内存地址(其中需要注意:age2必须使用才能找到,否则会报错)

    image

  • 观察3个变量的地址,其地址都是相邻的,因为在内存中都放在了全局区,观察其内存地址,可以发现,在全局区中,未初始化变量地址已初始化变量地址

    image

  • 如果定义了一个char *p = "CJLTeacher",查看 *pMach-O存储在__TEXT.cstring段,内存中存储在常量区

    image

  • 如果是 const修饰的变量呢?存放在Mach-O文件中的__TEXT.__const

    image

  • 如果使用static + const修饰变量,此时变量在哪?

    image

    查看 age4的内存地址,地址特别大,而且使用 cat查看不了,因为mach-o没有记录,age4 就是50,即使用static+const修饰的变量就相当于直接替换

对于Swift的分析
let age = 10
  • 对于let修饰的变量,由于是不可变的,所以不能通过 po+cat查看内存,通过汇编 首地址+偏移 来获取 age的内存,发现是在Mach-O的 __DATA.__common

    image

    从这里可以发现,这与C中是有所区别的。swift的不同之处:已经初始化的全局变量放在__DATA.__common段,猜测是因为 age开始是被标记为未初始化的,当我们执行代码之后才将 10存储到对应的内存地址中

  • 如果是 var修饰的变量呢?可以发现与 let是一致的,还是 __DATA.__common

var age2 = 20
image
总结
  • 对于C语言中全局变量,根据是否已经初始化,存储在Mach-O中存储位置是不同的
    • 初始化的全局变量:__DATA.__data
    • 初始化的全局变量:__DATA.__common
    • 初始化的全局静态变量,即static修饰:__DATA.__data
    • 对于char *p类型的字符:__TEXT.cstring
    • const修饰的全局变量:__TEXT.__const
    • static+const修饰的全局变量:Mach-O中没有记录
  • 对于 Swift中的全局变量
    • let修饰的全局变量:__DATA.__common
    • var修饰的全局变量:__DATA.__common
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容