iOS之方法调用

源码
message.h
message汇编源码
runtime源码

参考

在iOS中,方法调用过程分三步

  • 消息发送:从类及父类的缓存列表以及方法列表查找方法。

  • 动态解析:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现。

  • 消息转发:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理。

如果消息转发也没有实现,则会抛出常见的异常unrecognzied selector sent to instance

消息发送流程.png

我们先来去掉方法调用的伪装,在main.m文件中创建一个Animal类,添加一个run实例方法
在main函数中

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

    Animal *animal = [[Animal alloc] init];
    [animal run];
    
    return 0;
}

通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m命令转成C++代码main.cpp文件,进入文件后定位到main函数可以看到

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

    Animal *animal = ((Animal *(*)(id, SEL))(void *)objc_msgSend)((id)((Animal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animal"), sel_registerName("alloc")), sel_registerName("init"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("run"));

    return 0;
}

通过上面源码可以很清楚的看到C++底层将方法调用最终转换为调用objc_msgSend,并且传入参数(id)animal(消息接受者self),sel_registerName("run")(消息名_cmd),因此OC的方法调用也称为消息机制,表示给方法调用者发送消息。

接下来我们慢慢剖析一个消息发送的完整过程。

Step 1.1、 消息发送-缓存列表查找

Objective-C中方法调用从objc_msgSend开始就正式开始,objc_msgSend的申明定义在源码message.h中,实现是在Messengers.subproj/这个汇编文件夹中,以arm64的汇编源码为例(这里就只贴关键的代码)

_objc_msgSend,对消息接受者判空,是则进入LReturnZero返回nil,否则进入CacheLookup检查缓存

ENTRY _objc_msgSend
...
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged  // 判空
#else
    b.eq    LReturnZero // 不为空则进入LGetIsaDone
#endif
......
LGetIsaDone:
    CacheLookup NORMAL // 开始查找
......

CacheLookup开始查找缓存,查到缓存进入CacheHit并返回imp,否则进入CheckMiss

.macro CacheLookup
......
CacheHit $0
......
CheckMiss
.endmacro

CheckMiss根据传入参数跳转到__objc_msgSend_uncached or __objc_msgLookup_uncached

.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

__objc_msgSend_uncached调用MethodTableLookup进入方法列表查找

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup  // 这里是MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached

MethodTableLookup内部逻辑进入方法列表查找的核心函数__class_lookupMethodAndLoadCache3,该方法在runtime.m文件中实现

.macro MethodTableLookup
......
    bl  __class_lookupMethodAndLoadCache3
......
.endmacro

进入到__class_lookupMethodAndLoadCache3后我们接下来看1.2的方法列表查找过程。

Step 1.2、 消息发送-方法列表查找

从核心函数_class_lookupMethodAndLoadCache3开始

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj,
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 已经在1.1的缓存中已经查找过,所以传入的NO不再进行缓存查找
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }


    runtimeLock.lock();
    checkIsKnownClass(cls);
    if (!cls->isRealized()) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    if (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
    }


 retry:    
    runtimeLock.assertLocked();

    // 尝试查找一次缓存,如果找到直接返回
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 传入对象和方法名,在方法列表中获取
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 查找到后进行缓存,并直接返回
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 依次遍历父类的方法列表中或缓存进行查找
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 父类缓存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // 父类的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 未找到imp,如果传入resolver为YES且triedResolver为NO(未解析过)则开始动态解析过程
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        // 开始动态解析
        resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // 未找到imp且动态解析也未解析到,则开始消息转发
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

realizeClass是初始化了很多数据,包括cls->ro赋值给cls->rw,添加元类version为7,cls->chooseClassArrayIndex()设置cls的索引,supercls = realizeClass(remapClass(cls->superclass)); metacls = realizeClass(remapClass(cls->ISA()))初始化superclasscls->isa,后边针对没有优化的结构进行赋值这里不多讲,然后协调实例变量偏移布局,设置cls->setInstanceSize,拷贝flagsrorw中,然后添加subclassrootclass,最后添加类别的方法,协议,和属性。

getMethodNoSuper_nolock在类对象的方法列表中查找

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // cls->data() 得到的是 class_rw_t
    // class_rw_t->methods 得到的是methods二维数组
    for (auto mlists = cls->data()->methods.beginLists(),
              end = cls->data()->methods.endLists();
         mlists != end;
         ++mlists)
    {
         // mlists 为 method_list_t
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

search_method_list

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
   int methodListIsFixedUp = mlist->isFixedUp();
   int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
   // 如果方法列表是有序的,则使用效率较高的二分法查找方法
   if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
       return findMethodInSortedMethodList(sel, mlist);
   } else {
       // 否则遍历列表查找
       for (auto& meth : *mlist) {
           if (meth.name == sel) return &meth;
       }
   }
   return nil;
}

findMethodInSortedMethodList函数内二分查找实现原理

static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // >>1 表示将变量n的各个二进制位顺序右移1位,最高位补二进制0。
    // count >>= 1 如果count为偶数则值变为(count / 2)。如果count为奇数则值变为(count-1) / 2 
    for (count = list->count; count != 0; count >>= 1) {
        // probe 指向数组中间的值
        probe = base + (count >> 1);
        // 取出中间method_t的name,也就是SEL
        uintptr_t probeValue = (uintptr_t)probe->name;
        if (keyValue == probeValue) {
            // 取出 probe
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
           // 返回方法
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 则折半向后查询
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }

    return nil;
}
_class_lookupMethodAndLoadCache3内部流程.png

Step 2、 动态解析

动态解析阶段源码:

static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }
    }
}

resolveInstanceMethod函数

static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // 未实现动态解析方法SEL_resolveInstanceMethod
        return;
    }

    // 调用动态解析方法也就是OC中的类方法"resolveInstanceMethod:"
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // 动态解析后再次进行查找
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

lookUpImpOrNil

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    // 如果是`_objc_msgForward_impcache`则说明,动态解析添加失败,返回nil
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

在进行过一次动态解析后,通过resolvertriedResolver两个参数的值知道,无论动态解析是否有用,都不会在lookUpImpOrForward中再次进行动态解析。

接下来我们看一下OC中动态解析的方法实现以及怎么实现动态解析
动态解析对象方法+(BOOL)resolveInstanceMethod:(SEL)sel,动态解析类方法+(BOOL)resolveClassMethod:(SEL)sel

// 这里以`resolveInstanceMethod :`举例怎么实现动态解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(test)) {
        // 获取实例对象方法的指针
        Method otherMethod = class_getInstanceMethod(self, @selector(run));
        // 添加到类实例对象方法列表
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        // 是否动态添加
        return YES;
    }

    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}
int main(int argc, char * argv[]) {

    Animal *animal = [[Animal alloc] init];
    [animal run];
    [animal performSelector:@selector(test)];
    
    return 0;
}

animal对象调用方法test,可以看到最后调的run函数,通过上面对消息发送的分析我们知道,当本类和父类cacheclass_rw_t中都找不到方法时,就会进行动态解析的方法,也就是说会自动调用类的resolveInstanceMethod:方法进行动态查找。

需要注意Methodobjc_method结构体,其内部结构同method_t(内部包含SELtypeIMP)结构体相同,可以等同转换

动态解析流程.png

Step 3、 消息转发

runtime源码中可以看出,在未进行动态解析或者动态解析失败后,就会执行imp = (IMP)_objc_msgForward_impcache;进行消息转发,但是此部分代码并未开源,我们可以从Objectiv-C提供的模板方法来进行分析。

在Objectiv-C中消息转发涉及三个函数:
1.- (id)forwardingTargetForSelector:(SEL)aSelector
2.- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

  1. - (void)forwardInvocation:(NSInvocation *)anInvocation

如果forwardingTargetForSelector函数返回为nil或者没有实现的话,就会调用methodSignatureForSelector方法,用来返回一个方法签名,这也是我们正确跳转方法的最后机会。

如果methodSignatureForSelector方法返回正确的方法签名就会调用forwardInvocation方法,forwardInvocation方法内提供一个NSInvocation类型的参数,NSInvocation封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数。在forwardInvocation函数内修改方法调用对象即可。

如果methodSignatureForSelector返回的为nil,就会来到doseNotRecognizeSelector:方法内部,程序crash提示无法识别选择器unrecognized selector sent to instance。

// 3.消息转发阶段(未进行动态解析或者动态解析失败进入本阶段)
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(roar)) {
        
        return [[Lion alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(hunt)) {

        // 创建方法签名
//        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
        return [[[Lion alloc] init] methodSignatureForSelector:aSelector];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

/// 如果上一步‘methodSignatureForSelector:’返回正确的方法签名,则执行此步。这里需要注意修改NSInvocation的target对象,由于修改前的对象是原始调用对象未实现该方法,所以一直进行消息转发,造成调用栈溢出
/// @param anInvocation anInvocation,封装了上一步分方法签名
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    NSLog(@"NSInvocation original target is %@", anInvocation.target);
    Lion *target = [[Lion alloc] init];
    anInvocation.target = target;
    [anInvocation invoke];
//    [anInvocation invokeWithTarget:[[Lion alloc] init]];
    NSLog(@"NSInvocation destination target is %@", anInvocation.target);
}

同样上述代码,我们为动态转发方法添加返回值和参数,并在forwardInvocation方法中修改方法的返回值及参数。

// 3.消息转发阶段(未进行动态解析或者动态解析失败进入本阶段)
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(roar)) {
        
        return [[Lion alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 创建方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(hunt)) {

        return [[[Lion alloc] init] methodSignatureForSelector:aSelector];
    }
    
    if (aSelector == @selector(offspringCountWithAge:)) {
        
        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

/// 如果上一步‘methodSignatureForSelector:’返回正确的方法签名,则执行此步。这里需要注意修改NSInvocation的target对象,由于修改前的对象是原始调用对象未实现该方法,所以一直进行消息转发,造成调用栈溢出
/// @param anInvocation anInvocation,封装了上一步分方法签名
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    if (anInvocation.selector == @selector(hunt)) {
     
            NSLog(@"NSInvocation original target is %@", anInvocation.target);
            Lion *target = [[Lion alloc] init];
            anInvocation.target = target;
            [anInvocation invoke];
        //    [anInvocation invokeWithTarget:[[Lion alloc] init]];
            NSLog(@"NSInvocation destination target is %@", anInvocation.target);
    } else if (anInvocation.selector == @selector(offspringCountWithAge:)) {
     
        int age;
        // 获取方法的参数,方法默认还有self和cmd两个参数,因此新添加的参数下标为2
        [anInvocation getArgument:&age atIndex:2];
        NSLog(@"offspringCountWithAge:修改前参数的值 = %d",age);
        age = 3;
        NSLog(@"offspringCountWithAge:修改后参数的值 = %d",age);
        [anInvocation setArgument:&age atIndex:2];
        [anInvocation invokeWithTarget:[[Lion alloc] init]];

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