美文网首页
OC 方法的本质

OC 方法的本质

作者: Onego | 来源:发表于2020-01-02 17:12 被阅读0次

    探索

    探索案例

    /********对象声明*********/
    @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. 方法查找分为快速查找慢速查找

    相关文章

      网友评论

          本文标题:OC 方法的本质

          本文链接:https://www.haomeiwen.com/subject/gpnmoctx.html