Objective-C方法本質(zhì)

說(shuō)到Objective-C,我們都知道它是一個(gè)擴(kuò)充C的面向?qū)ο缶幊痰膭?dòng)態(tài)語(yǔ)言,而其中的動(dòng)態(tài)核心就是Runtime。

Runtime

Runtime簡(jiǎn)單來(lái)說(shuō)就是一套利用匯編語(yǔ)言C語(yǔ)言編寫(xiě)成的代碼庫(kù)。

Objective-C runtime 有兩個(gè)版本modernlegacy。
modern版本是在Objective-C 2.0中引入的,其中包括許多新功能。Objective-C Runtime Reference中描述了modern版本的運(yùn)行時(shí)的編程接口。
legacy版本的編程接口在Objective-C 1 Runtime Reference中進(jìn)行了描述。

Runtime Api

Objective-C Runtime Api

Runtime 作用

  • 消息發(fā)送
objc_msgSend(objc, @selector(methodName));
  • 方法交換
Method oldMethod = class_getClassMethod(self, @selector(methodName:));
Method newMethod = class_getClassMethod(self, @selector(newMethodName:));
method_exchangeImplementations(oldMethod, newMethod);
  • 動(dòng)態(tài)添加屬性
@implementation Person (Property)

- (void)setHobby:(NSString *)hobby {
    // @param object: 保存于哪個(gè)對(duì)象中
    // @param key:屬性名稱
    // @param value:數(shù)據(jù)值
    // @param policy:策略(strong,weak)
    // void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
    objc_setAssociatedObject(self, "hobby", hobby, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)hobby {
    // @param object: 保存于哪個(gè)對(duì)象中
    // @param key:屬性名稱
    return objc_getAssociatedObject(self, "hobby");
}
  • 動(dòng)態(tài)添加方法
// Class cls:給哪個(gè)類添加方法
// SEL name:添加方法的方法編號(hào)
// IMP imp:添加方法的函數(shù)實(shí)現(xiàn)
// const char *types:函數(shù)的類型
// BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
class_addMethod(self, @selector(methodName), (IMP)methodName, "v@:");
  • 數(shù)據(jù)模型轉(zhuǎn)換 - MJExtension
    ...

之所以能夠?qū)崿F(xiàn)這些功能,都是根據(jù)Runtime的機(jī)制和其提供的Api

方法的本質(zhì)

前面說(shuō)了這么多,其實(shí)就是為了了解方法的本質(zhì),在Objctive-C中,方法是怎么去實(shí)現(xiàn)的?

#import <Foundation/Foundation.h>

@interface Person : NSObject

- (void)sayHello;

@end

@implementation Person

- (void)sayHello{
    NSLog(@"%@", __func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [Person alloc];
        [person sayHello];
    }
    return 0;
}
clang

通過(guò)clang -rewrite-objc main.m -o main.cpp,我們可查看編譯后,運(yùn)行前源碼轉(zhuǎn)換成了C語(yǔ)言代碼。

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

在main函數(shù)中,我們簡(jiǎn)化類型轉(zhuǎn)換,可以發(fā)現(xiàn)都是通過(guò)objc_msgSend進(jìn)行消息發(fā)送。

Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));

因此可以理解為Objctive-C方法的本質(zhì)其實(shí)就是objc_msgSend消息發(fā)送,并且默認(rèn)帶有id(消息接收者)sel(方法編號(hào))。

發(fā)送消息
  • objc_msgSend 將帶有簡(jiǎn)單返回值的消息發(fā)送到類的實(shí)例。
  • objc_msgSendSuper 將具有簡(jiǎn)單返回值的消息發(fā)送到類實(shí)例的超類。
  • objc_msgSend_stret 將具有數(shù)據(jù)結(jié)構(gòu)返回值的消息發(fā)送到類的實(shí)例。
  • objc_msgSendSuper_stret 將具有數(shù)據(jù)結(jié)構(gòu)返回值的消息發(fā)送到類實(shí)例的超類。
/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [Person alloc];
        [p sayHello];
        // 方法調(diào)用底層編譯
        // 方法的本質(zhì): 消息 : 消息接受者 消息編號(hào) ....參數(shù) (消息體)
        objc_msgSend(p, sel_registerName("sayHello"));
        
        // 類方法調(diào)用底層編譯
        objc_msgSend(objc_getClass("Person"), sel_registerName("sayHi"));

        // 向父類發(fā)消息(對(duì)象方法)
        struct objc_super pSuper;
        pSuper.receiver = p;
        pSuper.super_class = [Person class];
        objc_msgSendSuper(&pSuper, @selector(sayHello));

        //向父類發(fā)消息(類方法)
        struct objc_super myClassSuper;
        myClassSuper.receiver = [p class];
        myClassSuper.super_class = class_getSuperclass(object_getClass([p class]));
        objc_msgSendSuper(&myClassSuper, @selector(sayHi));
    }
    return 0;
}

Tips:
使用objc_msgSend函數(shù)要把Enable Strict Checking of objc_msgSend Calls校驗(yàn)設(shè)置為NO , 否則編譯會(huì)報(bào)錯(cuò)了。

objc_msgSend匯編

objc_msgSend的實(shí)現(xiàn)為何采用匯編代碼?

  • 性能:匯編語(yǔ)言更能容易被機(jī)器識(shí)別,無(wú)需在進(jìn)行機(jī)器語(yǔ)言轉(zhuǎn)換。
  • 動(dòng)態(tài)性:C語(yǔ)言無(wú)法通過(guò)寫(xiě)一個(gè)函數(shù)來(lái)保留未知的參數(shù)并跳轉(zhuǎn)到一個(gè)任意函數(shù)指針。

objc4源碼中,全局查詢了objc_msgSend,此處以arm64為主。

    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:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

#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

整體的流程:
1、對(duì)消息接收者(id self, sel _cmd)進(jìn)行判斷處理。
2、taggedPointer判斷處理。
3、 GetClassFromIsa_p16 p13,獲取相應(yīng)的Class
4、CacheLookup NORMAL, _objc_msgSend進(jìn)行imp查找

.macro CacheLookup
    
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

結(jié)合匯編源碼的注釋,CacheLookup是一個(gè)方法查找的流程:
1、獲取到類的cache_t,并將拆分出對(duì)應(yīng)的bucketsmask
2、通過(guò)循環(huán)遍歷buckets,然后if (bucket->sel != _cmd)判斷在緩存中是否存儲(chǔ)有對(duì)應(yīng)的imp。
3、存在相應(yīng)的imp,則來(lái)到CacheHit,緩存命中,返回對(duì)應(yīng)的imp。
4、不存在相應(yīng)的imp,則繼續(xù)遍歷。
5、遍歷結(jié)束,找不到相應(yīng)的imp,會(huì)再一次重試(考慮到并發(fā))。
6、最后還是找不到對(duì)應(yīng)到方法時(shí),來(lái)到JumpMiss。

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

.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

CheckMiss中,因?yàn)槭?code>NORMAL的流程,所以會(huì)執(zhí)行__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
.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)]

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward

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

簡(jiǎn)單的方法跳轉(zhuǎn)到MethodTableLookup中。
通過(guò)注釋,前面進(jìn)行參數(shù)的準(zhǔn)備,然后調(diào)用了_lookUpImpOrForward

lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
在objc4源碼的匯編中,我們已經(jīng)找不到相應(yīng)的實(shí)現(xiàn),而在objc-runtime-new.mm文件中發(fā)現(xiàn)了C函數(shù)的實(shí)現(xiàn)。

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* Without LOOKUP_CACHE: skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use LOOKUP_INITIALIZE and LOOKUP_CACHE
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //
    // TODO: this check is quite costly during process startup.
    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

這段代碼的流程:

  • if (fastpath(behavior & LOOKUP_CACHE))還是回到匯編查詢cache。
  • 循環(huán)遍歷,查找是否有對(duì)應(yīng)的方法
    • 實(shí)例方法:循環(huán)遍歷當(dāng)前類,父類,根類,從這些類中的Method List中查詢是否存在對(duì)應(yīng)imp。
    • 類方法:循環(huán)遍歷當(dāng)前元類,父元類,根元類,最后到NSObject,從中的Method List中查詢是否存在對(duì)應(yīng)imp。
  • 查詢到相應(yīng)的imp,對(duì)方法進(jìn)行緩存log_and_fill_cache,然后返回相應(yīng)的imp。
  • 未查詢到,則imp = _objc_msgForward_impcache
  • 進(jìn)行一次方法解析嘗試resolveMethod_locked,再調(diào)用lookUpImpOrForward重試。
  • 最后如果還是沒(méi)找到,就返回imp = _objc_msgForward_impcache。

此處log_and_fill_cache只是簡(jiǎn)單的判斷是否支持消息日志記錄和調(diào)用cache_fill,
cache_fill的分析,在類的內(nèi)容中有做簡(jiǎn)單解釋。

/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled. 
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill(cls, sel, imp, receiver);
}

方法Crash

通常情況下,我們?nèi)绻{(diào)用一個(gè)未實(shí)現(xiàn)的方法,系統(tǒng)會(huì)有一的Carsh表現(xiàn)。
-[Person saySomething]: unrecognized selector sent to instance 0x100683700

在查找方法的最后,有個(gè)賦值imp = _objc_msgForward_impcache。

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    
    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel) {

    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

在方法的實(shí)現(xiàn)中,我們找到了Carsh輸出。

最后

objc方法的調(diào)用,其實(shí)就是通過(guò)objc_msgSend消息的發(fā)送。

  • 先通過(guò)objc_msgSend匯編代碼,在類的cache_t中快速查找。
  • cache_t找到,直接返回對(duì)應(yīng)的imp。
  • 如果在類的cache_t中查詢不到,則跳轉(zhuǎn)至C函數(shù)lookUpImpOrForward進(jìn)行慢速查找。
    • 實(shí)例方法:通過(guò)層層遞歸,-父類-NSObject-nil
    • 類方法:通過(guò)層層遞歸,元類-父元類-根元類-NSObject-nil
  • 找到,直接返回對(duì)應(yīng)的imp。
  • 未找到,進(jìn)行一次動(dòng)態(tài)解析resolveMethod_locked,在重復(fù)一次慢速查找。
  • 最后如果如果沒(méi)有處理動(dòng)態(tài)解析,則Crash。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容