iOS底层探索 -- objc_msgSend()流程分析

引子:我们在很早时候就听过OC是一个运行时语言,那么什么是运行时?

引入两个概念,编译时运行时

  1. 编译时 :顾名思义就是正在编译的时候 . 那啥叫编译呢?就是编译器帮你把
    源代码翻译成机器能识别的代码 .
    (当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言.)

2.运行时 :就是代码跑起来被装载到内存中去的阶段.

运行时语言主要就是讲OC将数据类型的确定编译时推迟到了运行时runtime.

运行时机制使我们知道运行时采取决定一个对象的类别,以及调用该类对象指定方法
而不同对象以自己的方式响应相同消息的能力叫做多态。

那么这个runtime对我们开发来说有什么作用?

runtime

   runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 runtime库里
面包含了跟类、成员变量、方法相关的API,比如获取类里面的所有成员变量,为类动态添 加成员变量,动态改变
类的方法实现,为类动态添加新的方法等 需要导入<objc/message.h><objc/runtime.h>
   在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了runtime的C语言代码,比如类转成了
runtime库里面的结构体等数据类型,方法转成了runtime库里面的C语言函数,平时调方法都是转成了objc_msgSend
函数(所以说OC有个消息发送机制)因此,可以说runtime是OC的底层实现,是OC的幕后执行者有了runtime库,
能做什么事情呢?runtime库里面包含了跟类、成员变量、方法相关的API,runtime是属于OC的底层, 可以进
行一些非常底层的操作(用OC是无法现实的, 不好实现)

 1.在程序运行过程中, 动态创建一个类(比如KVO的底层实现)
 2.在程序运行过程中, 动态地为某个类添加属性\方法, 修改属性值\方法
 3.遍历一个类的所有成员变量(属性)\所有方法 

 有了runtime,想怎么改就怎么改, runtime算是OC的幕后工作者.

在网上,我找到了这样一段关于runtime的描述。那么接下来,我们尝试一下其中提到的runtime库,以及里面最基础的objc_msgSend方法。

我们依旧使用之前的工程,在main中,我们写这样一段代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        FQPerson *person = [FQPerson alloc];
        Class personClass = [FQPerson class];
        [person eat1];
        [person eat2];
        [person eat3];
        [person eat4];
        [person sayHelloWorld];
        NSLog(@"%@",personClass);  
    }
    return 0;
}

这里可以说是我们最基础的写法。

现在,我们通过终端使用clang指令 生成main.m对应的main.cpp文件。

我们在其中找到对应上面 main 函数的方法

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 



        FQPerson *person = ((FQPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FQPerson"), sel_registerName("alloc"));

        Class personClass =((Class (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("class"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat1"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat2"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat3"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("eat4"));



        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHelloWorld"));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_bs_g7z1ww_14gq20hnx_3hcvm2w0000gn_T_main_3fd2f3_mi_5,personClass);


    }
    return 0;
}

我们惊奇发现经过clang编译后,本身我们写的方法就已经被转为了我们之前提到的objc_msgSend方法。

那么,对我们来时就轻松了 可以直接的来“抄”一下这段系统调用方法的代码

首先引入头文件

#import <objc/message.h>

然后,在main函数中,我们将之前的方法改成objc_msgSend()的调用。

注意,我们需要先将工程中target -> BuildSetting -> enableStrictChecking of objc_msgSend calls -> NO 否则会报错


环境设置.jpg
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        FQPerson *person = [FQPerson alloc];
        objc_msgSend(person, sel_registerName("sayHelloWorld"));
        [person sayHelloWorld];
        
    }
    return 0;
}

运行后打印


打印1.jpg

可见两种代码的实现方式一模一样。

接下来,进入今天的正题,让我们来研究一下objc_msgSend()

objc_msgSend()的方法查找流程

下面我们来看看objc_msgSend()

在我们一般的oc代码中

[person sayHelloWorld]
通过对象person,去调用他的类FQPersonsayHelloWorld方法

这个很好理解,因为直接在.h中声明,.m中实现,很清楚。

但在objc_msgSend中,我们是通过发送消息 通过 对象,和方法名SEL,让他们直接去查找对应的IMP进而找到方法的实现。

那么这个由sel-> imp的过程 objc_msgSend 是如何实现的

首先回忆一下之前的内容,关于方法的存储。

对象->类->cache_t或者 bits中的methodlist

回到我们的781版本的源码

全局搜索objc_msgSend

objc_msgSend全局查找.jpg

茫茫多的结果,我们不可能每个去看。

简单分析一下。

objc_msgSendruntime中的,我们之前提到OC运行时装载是在编译时之后,编译后代码是汇编的,那我们现在要研究的也应该是在汇编中的,所以结尾应该是.s文件,我们开发面向的又是真机,那么我们第一要研究的必然是真机对应的arm64

打开对应的obj-msg-arm64.s。好了,研究开始。

继续研究源码

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class

  1. 其中p0第一个参数,即为当前objc_msgSend中的消息接收者。

  2. p0nil比较,即为判断第一个obj是否为空

  3. 若为tagged pointer或为都会跳转到其他流程,则剩下的为不为空的流程

  4. 此时取首地址即isa 然后通过GetClassFromIsa_p16获取class

GetClassFromIsa_p16 流程

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

例如
LP64 中,直接通过isa与上ISA_MASK 得到类,与我们之前探索一致

继续之前流程

LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend
  1. 得到isa后 开始CacheLookup流程。
.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 label we may have loaded
    //   an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd$1,
    //   then our PC will be reset to LLookupRecover$1 which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //
LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif


    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

汇编源码比较难懂,所幸苹果在旁边写了清楚的注释。

提醒我们自己写代码多写点注释,不然时间久了自己都看不懂

闲话扯完继续分析

之前我们得到了class的地址

ldr p11, [x16, #CACHE]  // p11 = mask|buckets
#define CACHE            (2 * __SIZEOF_POINTER__)
  1. 所以即为平移16个字节,得到maskAndBuckets

  2. 然后通过位运算求得 buckets 以及 _cmd & mask

add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

这里回到了我们之前研究的地址平移的内容

  1. buckets的地址,直接加上_cmd&mask<<(1+PTRSHIFT)个单位的长度,那么就能知道找到_cmd&mask为下标的bucket.
  • _cmd&mask的哈希运算与我们之前存储时计算下标的方式一致

  • PTRSHIFT = 3 cmd&mask<<(1+PTRSHIFT)即向左移4位,等于值cmd&mask * 16

  1. 分别取出SEL 存入P17 IMP 存入P9

  2. 开始递归循环

  • 判断sel是否与_cmd一致,如果一致,则跳转 CacheHit 表示找到方法,返回imp
  • 如果不相等 则分情况
  • 若一直找不到,直接跳checkMiss 进入慢速查找流程
  • 找到buckets的最后一个地址(根据首地址和_mask 平移计算)向前循环递归

流程简图为


objc_msgSend流程.png

这样,我们就研究了objc_msgSend缓存查找,或者说快速查找流程
后续的慢速查找下次继续研究更新

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