iOS 底層探索篇 —— 方法的轉發流程

前言程序員

方法查找失敗

iOS 底層探索篇 —— 方法的查找流程這篇文章中,咱們已經知道了方法的查找的流程了,若是方法沒有查找到,在lookUpImpOrForward()函數裏面還有一部分是留給查找失敗的處理。緩存

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ...省略部分代碼...

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver did not help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

複製代碼
  • // No implementation found. Try method resolver once.這一段註釋告訴咱們,imp沒有,就仍是方法解析。
  • // No implementation found, and method resolver did not help. // Use forwarding.這一段又是告訴咱們,方法解析沒有幫到咱們,就利用forwarding轉發。

接下來咱們開始去分析下面兩個流程。bash

動態方法解析

1. 函數入口

if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }
複製代碼

_class_resolveMethod就是咱們須要研究的函數。函數

2. 解析方法

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst); // 已經處理
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // 對象方法 決議
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

區分是元類仍是類post

  • 對象方法解析_class_resolveInstanceMethod
  • 類方法解析_class_resolveClassMethod。而且類方法解析之後,依舊沒有找到imp,仍是會走對象方法解析。

3. 對象與類方法解析

3.1 對象方法解析

/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver does not fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    ...省略部分代碼...
}
複製代碼

3.2 類方法解析

/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver does not fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    ...省略部分代碼...
}
複製代碼

兩個函數實際上是相似的。ui

  • if (! lookUpImpOrNil()這一個條件判斷,主要是防止程序員寫的類 不是繼承自NSObject,就不須要開始動態方法解析流程了。
  • (typeof(msg))objc_msgSend,一個objc_msgSend消息發送SEL_resolveInstanceMethod或者SEL_resolveClassMethod
  • 在代碼的註釋段咱們能夠看到resolveInstanceMethod或者resolveClassMethod這麼兩個方法,意思就是告訴咱們能夠本身去實現父類NSObject的方法。

3.2 lookUpImpOrNil

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
複製代碼

動態方法解析以後,在開始查找流程,回到了iOS 底層探索篇 —— 方法的查找流程這篇文章的方法的慢速查找階段。spa

4. 方法解析例子

4.1 對象方法解析例子

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(saySomething)) {
        NSLog(@"說話了");
        
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
    
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        
        const char *sayHType = method_getTypeEncoding(sayHMethod);
        
        return class_addMethod(self, sel, sayHIMP, sayHType);
    }
    
    return [super resolveInstanceMethod:sel];
}
複製代碼

將沒有實現的方法saySomething,用另一個已經實現了方法sayHello來替換。3d

4.2 類方法解析例子

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    if (sel == @selector(sayLove)) {
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
        
         Method method = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
        
        const char *types = method_getTypeEncoding(method);
        
        return class_addMethod(objc_getMetaClass("LGStudent"), sel, imp, types);
    }
    return [super resolveClassMethod:sel];
}
複製代碼

這裏須要注意的位置就是,因爲類方法在元類裏面存着,咱們添加方法的時候要在類的元類裏面添加才能作到動態方法解析的成功。 日誌

4.3 留下問題

  1. 動態解析方法裏面沒有去處理,發現了這個問題,會進來了兩次?

這個問題留到最後面解釋。code

  1. 在解析方法分析中,在類方法的動態方法解析中,還走下面的對象方法的解析的目的?
_class_resolveClassMethod(cls, sel, inst); // 已經處理
 if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
         // 對象方法 決議
         _class_resolveInstanceMethod(cls, sel, inst);
    }
複製代碼
  • 咱們經過iOS 底層探索篇 —— isa的初始化&指向分析這篇文章的分析,無論是類(針對對象方法),仍是元類(針對類方法),他們最終的父類都會指向NSObject
  • 在方法的查找流程中,咱們探索出來,其實是經過selname來匹配的,在底層不分-、+方法的。
  • 目的就是可讓咱們在NSObject中作相同的處理。

方法轉發

若是動態方法解析沒有處理,接下來會來到消息轉發。

1. 轉發來源分析

1.1 日誌打印條件

iOS 底層探索篇 —— 方法的查找流程這篇文章中,咱們知道方法查找到了以後就會進入緩存流程。

static void
log_and_fill_cache(Class cls, Class implementer, Method meth, SEL sel)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    _cache_fill (cls, meth, sel);
}
複製代碼
  • if (objcMsgLogEnabled)若是這個條件成立,那麼就會進行日誌打印。

1.2 日誌打印入口

進行所有搜索objcMsgLogEnabled,能夠發現

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
複製代碼
  • 條件賦值的函數。
  • 查看函數定義 OBJC_EXPORT void instrumentObjcMessageSends(BOOL flag)是一個可供外部使用的。

1.3 打印日誌位置

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
複製代碼
instrumentObjcMessageSends(true);
 [student saySomething];
 instrumentObjcMessageSends(false);
複製代碼
  • /tmp/msgSends這裏是日誌存放的位置。
  • instrumentObjcMessageSends方法須要定義一下extern表示外部使用。
  • commond + shift + G查看/tmp/msgSends

1.4 查看打印日誌

+ LGStudent NSObject resolveInstanceMethod:
+ LGStudent NSObject resolveInstanceMethod:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent NSObject methodSignatureForSelector:
- LGStudent NSObject methodSignatureForSelector:
- LGStudent NSObject class
- LGStudent NSObject doesNotRecognizeSelector:
- LGStudent NSObject doesNotRecognizeSelector:
- LGStudent NSObject class

+ __NSCFString NSObject resolveInstanceMethod:
+ __NSCFString NSObject resolveInstanceMethod:
複製代碼

每一個方法會打印兩次的緣由是,本身自己有一次,還有一次是super給幹出來的。

  • 經過日誌分析咱們能夠看到流程是resolveInstanceMethodforwardingTargetForSelectormethodSignatureForSelectordoesNotRecognizeSelector,每一步都沒有實現就會報錯了。

這裏注意了,能夠解決`4.3`遺留下來的問題,爲何會進來兩次。看日誌文件還有一次是__NSCFString系統級別處理了一次。

2. 快速轉發

經過上面的日誌咱們能夠看到快速轉發流程forwardingTargetForSelector

2.1 NSObject.mm文件查看定義

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
複製代碼

可讓子類本身實現一下。

2.2 蘋果官方定義

意思就是一個沒法識別的消息可讓其餘的對象來處理這個消息。

2.3 具體實現

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGStudent *student = [LGStudent alloc] ;
        [student saySomething];
    }
    return 0;
}

@interface LGTeacher : NSObject

@end

@implementation LGTeacher

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

@end

@implementation LGStudent

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}


@end
複製代碼

LGStudent這個類沒有實現該saySomething的方法,可是LGTeacher實現了,就直接交給LGTeacher的對象處理。

3. 慢速轉發

若是快速轉發階段也沒有實現,就會進入到慢速轉發階段

3.1 方法簽名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(saySomething)) { 
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
複製代碼

這個位置就是對這個方法簽名以後 丟出去,誰想要處理就去處理。

3.2 轉發回調

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    SEL aSelector = [anInvocation selector];
    
    if ([[LGTeacher alloc] respondsToSelector:aSelector])
        [anInvocation invokeWithTarget:[LGTeacher alloc]];
    else
        [super forwardInvocation:anInvocation];
}
複製代碼

若是可讓其餘人處理就丟給另外的處理,不然系統就不處理這個方法了。

總結

1. 流程總結

消息轉發的分析到這裏就結束了,下面作一下總結。

  • 通過objc_msgSend方法快速查找和慢速查找不到結果以後就進入消息動態解析。
  • 動態解析過程_class_resolveMethod(),有處理就按照處理的來,沒有處理,就進入消息快速轉發階段。
  • 消息快速轉發階段forwardingTargedForSelector(),有處理,就按照交給處理的對象來實現,有沒有交給其餘對象處理,進入慢速轉發階段。
  • 消息慢速轉發階段methodSignatureForSelector(),進行方法簽名,把方法丟出去,forwardInvocation()來對消息處理。
  • 若以上中間步驟沒有進行,則就進入到了doesNotRecognizeSelector報錯。

附上流程圖以下:

2. 觀察轉發流程控制檯打印

控制檯的打印順序是按照resolveInstanceMethod -> forwardingTargetForSelector -> methodSignatureForSelector -> resolveInstanceMethod -> forwardInvocation

  • 倒數第二步把動態方法解析又調用了一次,這裏其實是上一步返回了一個方法簽名以後,下一步就會查找這個簽名,查找流程就會在走一次,這就是簽名匹配的過程,再次調用_class_getInstanceMethod是系統級別作的。
相關文章
相關標籤/搜索