swift中 class与struct的方法调用

swift为了实现快这么一个终极目标。在许多地方做了大量的优化。简直可以说是集现代编程语言之长。而这一点在swift中的方法调用尤为突出。我们来探究一下swift中的方法调用。

swift中函数调用方试

swift语言中总共有三种方法调用方式:
1.通过内存地址直接调用
2.通过v-table这么一个结构类似数组的函数表调用
3.就是我们非常熟悉的send_msg()消息派发。
他们的调用效率是1>2>3。动态性则是3>2>1。然后swift在任何时候都会优先的尽量使用内存直接调用,不能够使用内存地址直接调用的时候使用函数表调用。那么什么时候不能够使用内存直接调用了,很简单函数可能会被重写时就不能够使用内存直接调用了,而第三种消息表派发就是需要使用到runtime的时候会使用

stuct的方法调用

image.png

stuct一把都是采用的函数地址调用。在汇编代码中bl表示跳转到地址。这就效率最高的函数调用方式不用查找,撸起地址直接干。但是上面我们看的func2方法添加了一个mutating的关键字,要知道它的不同我们进入到sil中,能更详细的看的swift中做了什么

struct Person {
  func func1()
  mutating func func2()
  init()
}
.....
// Person.func1()
sil hidden [ossa] @$s14ViewController6PersonV5func1yyF : $@convention(method) (Person) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : $Person):
  debug_value %0 : $Person, let, name "self", argno 1 // id: %1

.....
// Person.func2()
sil hidden [ossa] @$s14ViewController6PersonV5func2yyF : $@convention(method) (@inout Person) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : $*Person):
  debug_value_addr %0 : $*Person, var, name "self", argno 1 // id: %1

我截取了部分关键的sil代码。在这段代码里面 func1()和func2()第一个不同点在@convention(method) (@inout Person) 中,func2()方法多了一个@inout,第二个不同点在Person, var, name "self", argno 1。里面的Person取得是地址。
inout 的作用很简单可以让let 定义的变量可以修改

func func3(age:inout Int){
        age = 20
        print(age)
    }

我们都知道在swift是不可以直接修改参数值的如果你要强行修改就会报这个错


image.png

但是如果我添加了inout关键字就可以修改这个let定义的变量了。调用inout修饰的参数的方法。参数需要传地址。像这样调用func3(age: &leoAge)。
那么在sil中func2()方法这里swift隐式的添加@inout的目的也就很明显了。意思就是可以修改传递进来的Person的参数。也就是struct本身。这就是mutaiting修饰的方法可以修改struct变量的原因。
struct中只有内存直接调用与函数表调用两种调用方式。因为要是使用消息派发必需继承自NSObject对象。

函数表调用

函数表的方式调用函数,在下面的截图中我们可以看到func2()的blr后面跟的不是函数地址而是一个变量。那这个变量swift是怎样存储的了?我们知道它是怎样存储的就知道了它是怎样调用的了。我们尝试着来找到函数表中函数地址的存储方式。

image.png

下面我们需要查看macho文件格式,我们先简单介绍一下macho文件的格式
Macho文件格式是这样的


image.png

它主要分为三个部分:1.Header 2.Load commands 3Data区
header中表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构
Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。

我们在看一下swift的类的构成,有助于我们在machoc中查找我们需要的信息。swift中类都有一个元数据结构,这个数据结构是一个Metadata 的struct。通过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、Enumd都有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
...
}

我们都知道struct是值类型。如果我们现在能找到typeDescriptor这个指针地址,通过指针偏移就能访问到struct中值类型的变量的值或者引用类型的指针。那么v-table这个函数表我们推测它与位于size的后面。因为它如果不在这个地方我们考虑不到它能存放在什么地方了。下面我们来验证一下。如果能找到就说明确实是这样的。

1.我们用machoview打开我们编译后的可执行文件。

image.png

我们直接定位到数据段中的__TEXT,__swift5_types中。__TEXT,__swift5_types就是存放TargetClassDescriptor的地方。pFile是虚拟内存地址,我们来计算一下前面8位的值。0x0000BBDC+0x9CFBFFFF,注意这个地方的0x9CFBFFFF是小段地址所以应该是0xFFFFFB9C。我们得到的值是0x10000B778。然后这个值我们需要减去程序本身虚拟内存得地址,这个值在Load Commands中的LC_SEGMENT_64(__TEXT)中。


image.png

可以看到这个值是0x100000000。我们现在得到的值是0x0000B778,我们到data段中,在Section64(__TEXT,__const)中我们能看到0x0000B778位于的内存区间。


image.png

那么B778应该就是在0xB770偏移8位,那么0x80000050就应该是我们typeDescriptor的地址。

swift 方法的结构

image.png

struct TargetMethodDescriptor就是swift中函数的数据结构。其中第一个flags描述了函数的类型。


image.png

然后我们根据TargetClassDescriptor的结构在来偏移13个字节我们在地址0xB7B0找到了我们的TargetRelativeDirectPointer的偏移值。0x00000010ffffc5cc,根据TargetMethodDescriptor的数据结构,前面4个字节存放的是方法类型所以我们的函数本身的impl这个地方的偏移值应该是0xFFFFC5CC。我们当前程序的ASLR的地址为0x1025d0000

0xFFFFC5CC+0x0000B7b0 = 0x100007d7c
0x100007d7c+0x1025d0000 = 0x2025D7D7C
0x2025D7D7C - Vm地址(0x10000000) = 0x1025D7D7C
下面看代码

image.png

我们我们断点到leo.fun2()这里查看汇编代码,blr x8 其x8寄存器存放的就是func2()的地址。我们使用register read x8读取x8的地址 0x00000001025d7d7c。与我们计算得到的地址是一样的说明我们上面的推论是正确的。

消息派发

class Person:NSObject{
    
    func func2(){
        
    }
    
    @objc dynamic func func3(){
        
    }
    
    @objc func func4(){
        
    }
    
    dynamic func func5(){
        
    }
}


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let leo = Person()
        leo.func2()
        leo.func3()
        leo.func4()
        leo.func5()
    }
}

我在Person中又添加了func3()、func4()、func5()方法。同时对他们分别使用@objc 、dynamic修饰
然后我们看sil代码


image.png

在Person的sil_vtable中func3()方法没有添加到vtable这个函数表中。因为func3()使用了@objc + danamic 修饰。


image.png

在sil中 这个地方就很明显的标识出来了他们调用方法的不同。func3()使用的是objc_method,而虚函数表使用的是class_method.
swift中的消息派发主要是为了兼容runtime的机制。所以使用消息派发的swift方法也就可以使用我们runtime的黑魔法了。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容