iOS底层学习 - Runtime之方法消息的前世今生(一)

843 阅读9分钟

经过前几章的探索,已经了解了对象和类的底层实现,对属性、成员变量和方法的存储也有了一定的了解,明白了方法的缓存机制,那么方法到底是如何进行调用的,它的整个流程是什么样的,怎么进行转发的,我们本章来探究一下

传送门☞iOS底层学习 - 类的前世今生(一)

传送门☞iOS底层学习 - 类的前世今生(二)

RunTime简介

我们都知道OC是一门动态语言,分为编译时和运行时,而Runtime是OC进行运行时支持API

  • Objective-C是一门动态性比较强的编程语言,根C、C++等语言有很大不同
  • Objective-C的动态性是由Runtime API来支撑的
  • Runtime API提供的接口基本都是C语言的,源码由C/C++/汇编语言编写

主要代码

1、Objective-C code:例如@selector()

2、NSObject的方法:例如NSSelectorFromString()

3、Runtime Api:例如sel_registerName

运行时

将代码装载在内存在需要的是进行调用就叫做运行时

编译时

Xcode中command+B就是编译时操作,将语法翻译成机器能识别的语言,编译成的可执行文件,运行的时候就是将这个可执行文件加载到内存中

方法的本质

通过运行clang命令,查看方法sayNB在编译后是如何运行的

LGPerson *person = [LGPerson alloc];
[person sayNB];
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));

通过上面的例子我们知道,方法再底层会变成objc_msgSend方法,即通过objc_msgSend来发送消息,其实obj表示消息接受者,sel_registerName是Runtime的API,表示根据一个方法名,返回一个SEL

objc_msgSend(obj, sel_registerName("sayNB"));
SEL sel_registerName(const char *name) {
    return __sel_registerName(name, 1, 1);     // YES lock, YES copy
}

消息的发送

既然知道了方法的本质即为objc_msgSend的消息发送,那么他是怎么实现的呢,通过objc源码,我们可以知道objc_msgSend方法再底层是由汇编来实现的,我们主要研究iOS设备的arm64结构的汇编实现

使用汇编的主要原因:

  • 对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能,容易被机器来识别。
  • 一些未知参数的识别,用C或者C++来实现比较困难

我们可以先到objc_msgSend相关汇编实现如下

GetClassFromIsa_p16方法即代表通过对象的isa & mask即可得到类,这个和之前章节讲过的方式是一样的,只不过这里是汇编实现

快速查找流程

快速流程即通过底层汇编代码快速找到方法的调用IMP,通过上面的代码分析,我们可以查看CacheLookup NORMAL相关代码

通过注释我们可知,该方法有3中参数NORMAL,GETIMP,LOOKUP,我们目前使用的是NORMAL参数

CacheLookup NORMAL主要查找流程如下,基本就是通过汇编来实现上一章节中,对类的cache_find的操作,从而找到对应缓存的IMP

CacheHit即为把响应的IMP返回给接受者
CheckMiss说明类的缓存中没有响应的IMP,会调用__objc_msgSend_uncached进行下一步查找
可以发现__objc_msgSend_uncached的实现中,主要进行了MethodTableLookup操作
MethodTableLookup中主要对未知的参数进行了一系列的处理,然后,调用__class_lookupMethodAndLoadCache3进行慢速查找,这是一个C和C++函数,所以调用起来比汇编要慢

流程小结

1.对接受者进行判空处理

2.进行taggedPoint等异常处理

3.获取到接受者isa,对isa & mask 获取到class

4.通过对class的isa进行指针偏移,获取到cache_t

5.通过对cache_t中key & mask 获取到下标,查找到对应的bucket,获取到其中的IMP

6.如果上述没有找到IMP,走到__objc_msgSend_uncached中的MethodTableLookup开始慢速查找

慢速查找流程

通过上述的汇编快速查找,如果没有方法的缓存,则会进入这个慢速查找的流程,那么慢速查找流程的起点是什么,我们可以通过打断点,看汇编代码查看

首先断点在需要跟踪的方法,打开Xcode中的Always Show Disassembly即可跟踪到相对应的汇编实现

通过跟踪汇编调用的实现,总到了快速查找的最后一步__objc_msgSend_uncached

继续跟踪,我们可以发现调用了方法_class_lookupMethodAndLoadCache3,自此开始了慢速查找的流程

lookUpImpOrForward方法分析

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    💡//️接收传入的参数, initialize = YES , cache = NO , resolver = YES
    💡//初始化相关参数
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    💡// 缓存查找, 因为cache传入的为NO,这里不会进行缓存查找,因为在汇编语言中CacheLookup已经查找过
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
    ❗️// 当类没有初始化时,初始化类和父类、元类等,保证后面方法的查找流程
    runtimeLock.read();
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        realizeClass(cls);
        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

 retry:    
    runtimeLock.assertReading();

    💡// 防止动态添加方法,缓存会变化,再次查找缓存。imp = cache_getImp(cls, sel);
    💡// 如果找到imp方法地址, 直接调用done, 返回方法地址
    if (imp) goto done;

    ❗️// 查找方法列表, 传入类对象和方法名
    {
        💡// 根据sel去类对象里面查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            💡// 如果方法存在,则缓存方法,
            💡// 内部调用的就是 cache_fill。
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            💡// 方法缓存之后, 取出函数地址imp并返回
            imp = meth->imp;
            goto done;
        }
    }

    ❗️// 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
    {
        unsigned attempts = unreasonableClassCount();
        ❗️// 如果父类缓存列表及方法列表均找不到方法,则去父类的父类去查找。一层层进行递归查找,直到找到NSObject类
        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.");
            }
            
             ❗️// 查找父类的cache_t缓存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    💡// 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    // 执行done, 返回imp
                    goto done;
                }
                else {
                    // 跳出循环, 停止搜索
                    break;
                }
            }
            
            ❗️// 查找父类的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                💡// 同样拿到方法, 在本类进行缓存
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                // 执行done, 返回imp
                goto done;
            }
        }
    }
    
     ❗️// ---------------- 消息发送阶段完成,没有找到方法实现,进入动态解析阶段 ---------------------
     ❗️//首先检查是否已经被标记为动态方法解析,如果没有才会进入动态方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
       💡 //将triedResolver标记为YES,下次就不会再进入动态方法解析
        triedResolver = YES;
        goto retry;
    }

     ❗️// ---------------- 动态解析阶段完成,进入消息转发阶段 ---------------------
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();
    💡// 返回方法地址
    return imp;
}

getMethodNoSuper_nolock方法

getMethodNoSuper_nolock方法就是一个简单的一个遍历方法列表

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;
}

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 {
        // 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;
}

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` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            💡// 继续向前二分查询
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            // 取出 probe
            return (method_t *)probe;
        }
        💡// 如果keyValue > probeValue 则折半向后查询
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

_objc_msgForward_impcache简述

在通过消息查找和动态解析失败后,最后会走到_objc_msgForward_impcache方法,调用__objc_msgForward,最终调用__objc_forward_handler

	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

搜索__objc_forward_handler发现这是一个C++方法,最终实现如下,这就是最终找不到方法时,LLDB打印输出的内容

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);
}

流程小结

1.首先对需要的变量进行初始化操作和加锁操作

2.其次如果没有进行初始化,则初始化类,父类、元类等,保证方法的查找

3.在receive的方法列表中进行二分查找,如果找到,则返回,并写入缓存

4.如果没找到,则一层层递归receive的父类的缓存和方法列表,直到NSObject,找到即返回,并写入receive的缓存

5.如果没找到,则进入动态方法解析流程,进行动态方法的解析,有则执行

6.如果没有动态方法解析,则进入消息转发流程

7.如果上述都没有实现和处理,则最终无法找到方法,会崩溃

总结

OC的消息机制可以分为一下三个阶段:

  • 消息发送阶段:从类及父类的方法缓存列表及方法列表查找方法;
  • 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;
  • 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理;

通过本章,我们基本了解了方法的本质和方法的查找流程,但是对于消息动态解析和消息转发的流程并没有深入了解,下一章节会着重讲解这两个部分