OC 方法的本质

探索

探索案例

/********对象声明*********/
@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayNB;
@end

@interface OStudent : LGPerson
- (void)sayCode;
+ (void)sayGood;
@end

/********测试代码*********/
OStudent *s = [OStudent new];
[s sayCode];           //对象方法
[OStudent sayGood];    //类方法

使用clang命令把oc代码编译成c代码分析

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

//经过整理,留下方法调用相关代码
OStudent *s = ((OStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OStudent"), sel_registerName("new"));
/*
objc_msgSend(s,sel_registerName("sayCode"))
*/
objc_msgSend((id)s, sel_registerName("sayCode"));
/*
objc_msgSend(objc_getClass("OStudent"),sel_registerName("sayGood"))
*/
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OStudent"), sel_registerName("sayGood"));

  • 都将objc_msgSend强转为(void (*)(id, SEL))(void *),用于适应发送消息的格式
  • 这里的objc_msgSend第一个参数是接收消息的对象,第二个参数是SEL
  • sel_registerName函数的作用是向runtime注册一个方法名;如果方法名已经注册,则放回已经注册的SEL。
  • 经过观察对象方法类方法的区别在于接收者不同
  • 这里也说明了对象方法类方法本质上是没有区别的,区别在于存储的位置不同

objc_msgSend初探

objc_msgSend就是消息发送的实现API,通过源码搜索发现objc_msgSend的具体实现是通过汇编完成,OC中所有的消息发送都会调用该方法,这样也对方法执行速度有了很高的要求,这应该是objc_msgSend选择使用汇编实现的原因。

使用小提示
objc_msgSend使用过程中,如果直接使用objc_msgSend就会报错
解决方法

  1. objc_msgSend 强转成 (void (*)(id, SEL))(void *)((void (*)(id, SEL))(void *)objc_msgSend)(s, @selector(sayCode));
  2. 关闭内存检查


    关闭msgSend类型检查

objc_msgSend梳理

源码中搜索objc_msgSend,

在源码中objc_msgSend有几种架构的实现,这里拿最常见的arm64(objc-msg-arm64.s)举例。

objc_msgSend 流程分析
    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
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

以上是 _objc_msgSend的主干代码,接下来进行分析到底干了什么。

    cmp p0, #0          // 判断p0是否为空,p0是第一个参数(消息接收对象)
#if SUPPORT_TAGGED_POINTERS // 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
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached
  • SUPPORT_TAGGED_POINTERSobjc2的64位环境下是1,所以执行b.le LNilOrTagged
  • b.le 汇编命令表示,前一个cmp命令对比值小于等于,那么执行标号,否则往下执行
  • 如果消息接受者Nil或者是tagged pointer,就会执行LNilOrTagged标记
  • 继续往下执行说明消息接受者isa指针
  • GetClassFromIsa_p16isa&ISA_MASK操作拿到isa指向对象的指针
  • LGetIsaDone isa处理完成执行的标记,之后进行方法的查找CacheLookup NORMAL
LNilOrTagged
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
  • b.eq LReturnZero 如果消息接受者为空return 0;这也是给nil发送消息不会有任何反应的原因所在
  • 完成之后执行LGetIsaDone标记,进行方法的查找CacheLookup NORMAL
CacheLookup
.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    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
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // 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

3:  // double wrap
    JumpMiss $0
    
.endmacro
  • CacheLookup是一个宏macro
  • CacheLookup完成了在cache中查找IMP缓存,这种查找方式称之为 快速查找
  • CacheLookup 有三个参数 CacheLookup NORMAL|GETIMP|LOOKUP,这次传入的是NORMAL
  • cache中查找有三种结果:CacheHitCheckMissadd
CacheHit 缓存命中IMP
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1  // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1  // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1 // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

传入的是NORMAL就返回IMP

CheckMiss缓存未命中IMP
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
  • 出入的是NORMAL,未命中缓存执行的是__objc_msgSend_uncached
__objc_msgSend_uncached
    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
  • __objc_msgSend_uncached是对没有命中缓存的处理
  • 未命中缓存接下来就是去查找对象中的方法列表MethodTableLookup

MethodTableLookup

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro
  • MethodTableLookup查找对象的方法列表
  • 一堆代码的是做准备工作和收尾工作,为__class_lookupMethodAndLoadCache3函数做铺垫
  • __class_lookupMethodAndLoadCache3在汇编中并没有实现具体实现,实现是用c写的_class_lookupMethodAndLoadCache3
_class_lookupMethodAndLoadCache3

至此方法查找中的第一步快速查找已经结束,接下来的是慢速查找

Super在方法调用中的作用

案例

@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayNB;
@end

@interface OStudent : LGPerson
- (void)sayCode;
+ (void)sayGood;
@end

@implementation LGPerson

- (void)sayHello {
    NSLog(@"%s",sel_getName(_cmd));
}

+ (void)sayNB {
    NSLog(@"%s",sel_getName(_cmd));
}

@end

@implementation OStudent

- (void)sayCode {
    [super sayHello];
}

+ (void)sayGood {
    [super sayNB];
}

@end

//调用代码
OStudent *s = [OStudent new];
[s sayCode];
[OStudent sayGood];

编译后的c代码后 sayGoodsayCode的实现

//整理后的代码
struct __rw_objc_super { 
    struct objc_object *object; 
    struct objc_object *superClass; 
};
static void _I_OStudent_sayCode(OStudent * self, SEL _cmd) {
    __rw_objc_super

    // 向父类发消息(对象方法)
    struct __rw_objc_super t_super;
    t_super.receiver = self;
    t_super.super_class = class_getSuperclass(objc_getClass("OStudent"));
    objc_msgSendSuper(&t_super, sel_registerName("sayHello"));
}


static void _C_OStudent_sayGood(Class self, SEL _cmd) {
   
    //向父类发消息(类方法)
    struct __rw_objc_super t_ClassSuper;
    t_ClassSuper.receiver = [s class];
    t_ClassSuper.super_class = class_getSuperclass(objc_getMetaClass("OStudent"));// 元类的父类
    objc_msgSendSuper(&t_ClassSuper, sel_registerName("sayNB"));
}
  • 使用super调用父类方法会使用objc_msgSendSuper发送消息
  • objc_msgSendSuper第一个参数是一个结构体指针,第二个参数是SEL
  • __rw_objc_super中成员object表示消息接收者,super_class表示父类对象
objc_msgSendSuper
    ENTRY _objc_msgSendSuper
    UNWIND _objc_msgSendSuper, NoFrame

    ldp p0, p16, [x0]       // p0 = real receiver, p16 = class
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

    END_ENTRY _objc_msgSendSuper
  • objc_msgSendSuper查找父类对象方法缓存
  • 不需要处理isa,只需要直接查找缓存CacheLookup NORMAL

总结

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