iOS类与方法底层探析(三)-方法调用的消息机制

消息机制

RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。
对于C语言,函数的调用在编译的时候会决定调用哪个函数( C语言的函数调用请看这里 )。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。

[PlatoJobs PJ_ClassMethod];

首先,编译器将代码[PlatoJobs PJ_ClassMethod];转化为objc_msgSend(PlatoJobs, @selector (PJ_ClassMethod));,在 objc_msgSend函数中。首先通过PlatoJobsisa指针找到PlatoJobs对应的class。在Class中先去cache中 通过SEL查找对应函数method(猜测cachemethod列表是以SELkey通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
本文将详细解释:


#import <UIKit/UIKit.h>

@interface PlatoJobs : NSObject

+ (void) classMethod;

@end

@implementation PlatoJobs

+ (void) classMethod{
    NSLog(@"class method");
}

@end

int main(int argc, char * argv[]){
    @autoreleasepool {
        [PlatoJobs classMethod];
        return 0;
    }
}



我们将上述代码转换成C++代码:
xcrun -sdk iphoneos clang -rewrite-objc -F UIKit -fobjc-arc -arch arm64 /Users/platojobs/Desktop/PL/PL/main.m

在生成的cpp文件中,可以看到main函数对应的c++代码
通过如下代码可以知道我们在文初所开始的方法被转换成了objc_msgSend

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

          ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("PlatoJobs"), sel_registerName("classMethod"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_wm_jgydmfmx3vnbhg_9dglv581c0000gn_T_main_5349ff_mi_1);
    }
    return 0;
}

那么问题来了,objc_msgSend它是怎么确定应该调用哪个方法的呢?它有一个动态查找过程,具体如下:
汇编代码中长成这牙子:


/*****************************************************************
 *
 * id objc_msgSend(id self, SEL    _cmd,...);
 *
 *****************************************************************/
    ENTRY objc_msgSend
    MESSENGER_START
    cbz    r0, LNilReceiver_f  // 判断消息接收者是否为nil
    ldr    r9, [r0]   // r9 = self->isa
    CacheLookup NORMAL  // 到缓存中查找方法
LCacheMiss:         // 方法未缓存
    MESSENGER_END_SLOW
    ldr    r9, [r0, #ISA]
    b    __objc_msgSend_uncached
LNilReceiver:      // 消息接收者为nil处理
    mov    r1, #0
    mov    r2, #0
    mov    r3, #0
    FP_RETURN_ZERO
    MESSENGER_END_NIL
    bx    lr
LMsgSendExit:
    END_ENTRY objc_msgSend

其实主要就是:

  • 判断接收者是否为nil,如果为nil,清空寄存器,消息发送返回nil

  • 到类缓存中查找方法,如果存在直接返回方法

  • 没有找到缓存,到类的方法列表中依次寻找

查找方法实现是通过_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)
{
    Class curClass;
    IMP methodPC = nil;
    Method meth;
    bool triedResolver = NO;
    methodListLock.assertUnlocked();
    // 如果传入的cache为YES,到类缓存中查找方法缓存
    if (cache) {
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) return methodPC;
    }
    // 判断类是否已经被释放
    if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;
    // 如果类未初始化,对其进行初始化。如果这个消息是initialize,那么直接进行类的初始化
    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
 retry:
    methodListLock.lock();
    // 忽略在GC环境下的部分消息,比如retain、release等
    if (ignoreSelector(sel)) {
        methodPC = _cache_addIgnoredEntry(cls, sel);
        goto done;
    }
    // 遍历缓存方法,如果找到,直接返回
    methodPC = _cache_getImp(cls, sel);
    if (methodPC) goto done;
    // 遍历类自身的方法列表查找方法实现
    meth = _class_getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, cls, meth, sel);
        methodPC = method_getImplementation(meth);
        goto done;
    }
    // 尝试向上遍历父类的方法列表查找实现
    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
        if (meth) {
            if (meth != (Method)1) {
                log_and_fill_cache(cls, curClass, meth, sel);
                methodPC = method_getImplementation(meth);
                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;
            }
        }
        // 查找父类的方法列表
        meth = _class_getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, curClass, meth, sel);
            methodPC = method_getImplementation(meth);
            goto done;
        }
    }
    // 没有找到任何的方法实现,进入消息转发第一阶段“动态方法解析”
    // 调用+ (BOOL)resolveInstanceMethod: (SEL)selector
    // 征询接收者所属的类是否能够动态的添加这个未实现的方法来解决问题
    if (resolver  &&  !triedResolver) {
        methodListLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        goto retry;
    }
    // 仍然没有找到方法实现进入消息转发第二阶段“备援接收者”
    // 先后会调用 -(id)forwardingTargetForSelector: (SEL)selector
    // 以及 - (void)forwardInvocation: (NSInvocation*)invocation 进行最后的补救
    // 如果补救未成功抛出消息发送错误异常
    _cache_addForwardEntry(cls, sel);
    methodPC = _objc_msgForward_impcache;
 done:
    methodListLock.unlock();
    assert(!(ignoreSelector(sel)  &&  methodPC != (IMP)&_objc_ignored_method));
    return methodPC;
}

上面就是一个方法调用的全部过程。主要分为三个部分

  • 查找是否存在对应的方法缓存,如果存在直接返回调用
    为了优化性能,方法的缓存使用了散列表的方式,在下一部分会进行比较详细的讲述

  • 未找到缓存,到类本身或顺着类结构向上查找方法实现,返回的method_t *类型也被命名为Method


//非加锁状态下查找方法实现
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // fixme nil cls?
    // fixme nil sel?
    for (auto mlists = cls->data()->methods.beginLists(),
            end = cls->data()->methods.endLists();
             mlists != end;
               ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
// 搜索方法列表
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 {
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif
    return nil;
}

如果在这个步骤中找到了方法的实现,那么将它加入到方法缓存中以便下次调用能快速找到:


// 记录并且缓存方法
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(),
                                cls->nameForLogging(),
                                implementer->nameForLogging(),
                                sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}
//在无加锁状态下缓存方法
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    if (!cls->isInitialized()) return;
    if (cache_getImp(cls, sel)) return;
    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);
    // 如果缓存占用不到3/4,进行缓存。
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied expand();
    }
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

如果在类自身中没有找到方法实现,那么循环获取父类,重复上面的查找动作,找到后再将方法缓存到本类而非父类的缓存中

未找到任何方法实现,触发消息转发机制进行最后补救

其中消息转发分为两个阶段,第一个阶段我们可以通过动态添加方法之后让编译器再次执行查找方法实现的过程;第二个阶段称作备援的接收者,就是找到一个接盘侠来处理这个事件,

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    // 非beta类的情况下直接调用 resolveInstanceMethod 方法
    if (! cls->isMetaClass()) {
        _class_resolveInstanceMethod(cls, sel, inst);
    }
    else {
        // 先调用 resolveClassMethod 请求动态添加方法
        // 然后进行一次查找判断是否处理完成
        // 如果没有添加,再调用 resolveInstanceMethod 方法
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst,
                      NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
在最新的runtime源码的objc-runtime-new.h中,objc_class的结构如下(笔者已经略去了大部分的函数):

struct objc_class : objc_object {
    Class superclass;          // Class ISA;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
    bits.setData(newData);
    }
    // .........
}
很明显cache存储着我们在方法调用中需要查找的方法缓存。作为缓存方法的cache采用了散列表,以此来大幅度提高检索的速度:

#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    // functions
}
// cache method
buckets = (cache_entry **)cache->buckets;
for (index = CACHE_HASH(sel, cache->mask);
     buckets[index] != NULL;
     index = (index+1) & cache->mask)
{ }
buckets[index] = entry;

在每次调用完未被缓存的方法时,下面的那段缓存方法的代码就会调用。苹果利用了sel的指针地址和mask做了一个简单的位运算,然后找到一个空槽存储起来。 以此我们可以推出从缓存中查找sel实现的代码CacheLookup,但是为了高度优化性能,苹果同样丧心病狂的使用汇编完成了查找的步骤,官方给出的注释足够我们大致看明白这段代码:


.macro CacheLookup
    ldrh    r12, [r9, #CACHE_MASK]    // r12 = mask
    ldr    r9, [r9, #CACHE]    // r9 = buckets
.if $0 == STRET  ||  $0 == SUPER_STRET
    and    r12, r12, r2        // r12 = index = SEL & mask
.else
    and    r12, r12, r1        // r12 = index = SEL & mask
.endif
    add    r9, r9, r12, LSL #3    // r9 = bucket = buckets+index*8
    ldr    r12, [r9]        // r12 = bucket->sel
2:
.if $0 == STRET  ||  $0 == SUPER_STRET
    teq    r12, r2
.else
    teq    r12, r1
.endif
    bne    1f
    CacheHit $0
1:
    cmp    r12, #1
    blo    LCacheMiss_f        // if (bucket->sel == 0) cache miss
    it    eq            // if (bucket->sel == 1) cache wrap
    ldreq    r9, [r9, #4]        // bucket->imp is before first bucket
    ldr    r12, [r9, #8]!        // r12 = (++bucket)->sel
    b    2b
.endmacro

在OC中方法被抽象成的数据类型是Method,如果了解并且使用过runtime的读者们可能了解这个类型,其结构如下:

struct old_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
};
typedef struct method_t *Method;

  • method_imp方法的实现代码,你可以把它看做一个block。事实上,后者确实可以转换成一个IMP类型来实现某些黑魔法。

  • method_types方法的参数编码,什么意思?在属性与变量中我说过每一种数据类型有着自己对应的字符编码,这个表示方法返回值、参数的字符编码,比如-(void)playWith:(id)的字符编码为v@:@

  • method_name顾名思义,方法的名字。通常我们使用@selector()的方式获取一个方法的sel地址,这个被用来进行散列计算存储方法的imp实现。由于SEL类型采用了散列的算法,因此如果同一个类中存在同样名字的方法,那么就会导致方法的imp地址无法唯一化。这也是苹果不允许同名不同参数类型的方法存在的原因.

以上的过程就是:


runtime分析.png
具体调用过程如下:
阶段流程.jpg
  1. 在相应对象的缓存方法列表中(objc-classcache)查找调用的方法。
  2. 如果没找到,则在相应对象的方法列表中找调用方法。
  3. 如果还没找到,就在父类指针指向的对象中执行1和2两步。
  4. 如果直到根类都没有找到,就进行消息转发,给自己保留处理找不到方法这一状况的机会。
  5. 调用resolveInstanceMethodresolveClassMethod,有机会让类添加这个函数的实现。
  6. 调用forwardingTargetForSelector,让其他对象执行这个函数。
  7. 调用forwardInvocation更加灵活的处理函数调用。
  8. 如果以上都没有找到,也没有进行特殊处理,就抛出doesNotRecognizeSelector异常.
    流程2.jpg
根据类的层级查找方法.gif

RunTime的具体应用,我想下篇再写.

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

推荐阅读更多精彩内容