ios objc_msgSend流程

1.runtime简介

  • 编译时
    顾名思义,编译时就是正在编译的时候,而编译,指的是将代码翻译成机器可以识别的代码。在编译时,检查到的错误叫做编译时错误,做的类型检查叫做编译时类型检查,也叫做静态类型检查,这种情况下,是计算机还没将代码运行到内存中,只是把代码以文本的形式进行扫描。
  • 运行时
    运行时,就是代码在内存中运行起来了。运行时与编译时类型检查不一样,不只是简单的扫描代码,而是在内存中做了操作,或者做了什么判断。

runtime就是以C/C++以及汇编编写封装的供OC提供运行时的API。

2.objc_msgSend方法快速查找流程

2.1 通过clang命令简单探索

在main.m文件中demo代码如下:

@interface ZCPerson : NSObject
- (void)sayNB;
@end

@implementation ZCPerson
- (void)sayNB{
    NSLog(@"666");
}

@end

@interface ZCTeacher : ZCPerson
- (void)sayHello;
- (void)sayNB;

@end

@implementation ZCTeacher
- (void)sayHello{
    NSLog(@"666");
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        
        ZCPerson *p = [ZCPerson alloc];
        [p sayNB];
        
        ZCTeacher *t = [ZCTeacher alloc];
        [t sayHello];
        [t sayNB];
    }
    return 0;
}

找到当前main.m文件所在路径,cd到当前文件目录下,使用clang命令:

clang -rewrite-objc main.m -o main.cpp

这样,就在当前文件目录下得到了main.cpp文件,打开main.cpp文件,找到main.m中main函数实现,发现mian函数在底层被编译成如下代码:

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

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3l_y1z24zqx5qv48b07nkgx49wr0000gn_T_main_93b105_mi_2);

        ZCPerson *p = ((ZCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZCPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayNB"));

        ZCTeacher *t = ((ZCTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZCTeacher"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)t, sel_registerName("sayHello"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)t, sel_registerName("sayNB"));
    }
    return 0;
}

通过底层编译代码说明,对象方法调用在底层会被编译成objc_msgSend的方式发送消息,也就是说,我们可以在main.m中将sayNB或者sayHello方法改成objc_msgSend。导入#import <objc/message.h>框架,在build setting中将enable strict checking objc_msgSend Calls中的Yes改为NO,如下图:

objc_msgsend.png

接着,将main.m中的函数改为:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        
        ZCPerson *p = [ZCPerson alloc];
//        [p sayNB];
        objc_msgSend(p, sel_registerName("sayNB"));
        
        ZCTeacher *t = [ZCTeacher alloc];
//        [t sayHello];
//        [t sayNB];
        objc_msgSend(t, sel_registerName("sayHello"));
    }
    return 0;
}

通过输出可以证实, [p sayNB];等价于objc_msgSend(p, sel_registerName("sayNB"));。OC在进行方法调用的时候,会找到一个方法编号sel,通过sel绑定到函数指针地址imp,再通过imp找到函数实现的内容。我们知道,在imp查找函数内容属于指针级别的查找,所以sel绑定到imp才是需要进行探索的内容。

2.2 sel查找imp流程

我们知道方法存在于类/元类中,而查找元类需要isaisa存在于对象当中(不论是实例对象还是类对象),通过isa找到类后,开始找cache_t,看看cache_t中是否有缓存的方法,如果没有,则是从bit里查找methodlist,看看方法列表里是否有查找的方法。

  • 源码分析
    打开源码搜索objc_msgSend,因为是要找汇编代码,所以点击搜索栏下objc-msg-arm64.s文件里的ENTRY _objc_msgSend进入代码:
ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check(判断当前消息发送对象是否为空)
/*
    com :compare,比较
    p0:objc_msgSend第一个参数,也就是当前发送消息的对象
    #0:空
*/
#if SUPPORT_TAGGED_POINTERS //(判断是否支持TAGGEDPOINTER类型)
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa  //拿出isa
    GetClassFromIsa_p16 p13     // p16 = class  //通过isa找到当前的类
LGetIsaDone: //isa流程查找完毕
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend  //从缓存里获取imp的流程
  • CacheLookup
#define CACHE            (2 * __SIZEOF_POINTER__)   //2*8 = 16
.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]      //isa向右平移16位,拿到当前的cache_t       // p11 = mask|buckets ,mask占高16位,buckets占48位

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets 拿到cache里的bucket
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask ,
/*LSR#48:逻辑右移48位拿到cache里的mask,也就是p11
   p1:_cmd
*/
#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)),获得当前的bucket
   /**
    PTRSHIFT: #define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE;
    ((_cmd & mask) << (1+PTRSHIFT)):当前的值向左平移4,即值*2^4;
    
*/
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd),判断缓存里的sel与当前方法的_cmd是否相等
    b.ne    2f          //     scan more 不相等,走下面2: 流程
    CacheHit $0         // call or return imp 相等则返回imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets 判断要查找的bucket是否是首地址
    b.eq    3f  //相同,则到3:流程处理 ,直接拿到最后一个bucket
    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

2.3 流程图分析

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

推荐阅读更多精彩内容