我们知道Objective C 是一门动态语言,它是在编译器和Runtime共同协作下完成的,编译时期决定了向哪个对象发送哪个消息,但是这个对象在收到具体消息的时候如何处理是由Runtime决定的,Runtime对消息的处理包括了消息的发送和消息的转发。我们接下来就来看下这部分处理:

在Objective C代码编译期间当遇到一个方法调用的时候,编译器会产生一个对objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret的函数调用

  • 如果调用的是[super xxxx] 那么将会调用objc_msgSendSuper objc_msgSendSuper_stret 也就是有带super的函数
  • 如果调用的是[obj xxxxx] 那么将会调用objc_msgSend objc_msgSend_stret
  • 如果返回的值是带有结构的将会调用 objc_msgSendSuper_stret objc_msgSend_stret 也就是带有stret的。

为了简单起见我们这里只研究objc_msgSend的情况:

OBJC_EXPORT id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这里的第一个参数为消息的接收对象,第二个参数为SEL,可以看成是一个消息。

objc_msgSend底层代码是通过汇编实现的:

    ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//判空 如果检测方法的接受者是nil,那么系统会自动clean并且return。
NilTest NORMAL
//可以快速地获取到对象的 isa 指针地址放到 r11 寄存器
GetIsaFast NORMAL // r10 = self->isa
//在缓存中查找IMP 如果查找成功直接调用 IMP
CacheLookup NORMAL, CALL // calls IMP on success
//检查是否返回值为空
NilTestReturnZero NORMAL

GetIsaSupport NORMAL

//查找缓存失败,在方法列表中查找
LCacheMiss:
// isa still in r10
jmp __objc_msgSend_uncached
END_ENTRY _objc_msgSend

上面的汇编代码会通过isa找到消息接收者的objc_class,然后通过CacheLookup汇编代码在objc_class 方法缓存中查找,是否有和当前SEL匹配的方法缓存,如果有直接调用对应的IMP,如果没有则走LCacheMiss分支,它会跳转到__objc_msgSend_uncached中:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r10 is the searched class

// 在方法表中查找对应的IMP
// r10 is already the class to search
MethodTableLookup NORMAL // r11 = IMP
// 跳到对应的IMP去执行
jmp *%r11 // goto *imp

END_ENTRY __objc_msgSend_uncached

在__objc_msgSend_uncached会调用MethodTableLookup从方法列表中查找:

.macro MethodTableLookup
//......

.if $0 == NORMAL
// receiver already in a1
// selector already in a2
.else
movq %a2, %a1
movq %a3, %a2
.endif
movq %r10, %a3
// 调用__class_lookupMethodAndLoadCache3
call __class_lookupMethodAndLoadCache3

// IMP is now in %rax
movq %rax, %r11

//......

.endmacro

这里会调用****__class_lookupMethodAndLoadCache3**** 进行查找。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

_class_lookupMethodAndLoadCache3 是专门提供给汇编调用的不需要查找方法缓存,直接查找objc_class方法列表的方法。所以这里调用lookUpImpOrForward时候cache传的是NO.

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;

//确保进入的时候是没锁住的
runtimeLock.assertUnlocked(); //这个是加一个读写锁,保证线程安全。

//优化缓存查找传入的参数cache 用于表示是否找到cache的布尔量,
//从_class_lookupMethodAndLoadCache3进来的是在缓存中已经找过并且没找到的情景,这时候cache为NO
if (cache) {
//如果传入的是YES,那么就会调用cache_getImp方法去找到缓存里面的IMP,注意cache_getImp是通过汇编实现的,cache_getImp会把找到的IMP放在r11中
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

//锁住读写锁
runtimeLock.lock();
//.......
//如果已经初始化会在objc_class对应的标识位设置为true
if (!cls->isRealized()) {
//实例化类结构
realizeClass(cls);
}

// +initialize就是在这个阶段调用的
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
//_class_initialize是类初始化的过程。它会发送一个initialize消息给当前类
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
//如果我们发送的sel就是initialize那么这里的_class_initialize会发送一次+initialize,后续还会发送一次+initialize,但是这种情况很少见
// 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;
}
}
//尝试在父类的缓存和方法列表中查找
{
//如果以上尝试都失败了,接下来就会循环尝试父类的缓存和方法列表。一直找到NSObject为止。因为NSObject的superclass为nil,才跳出循环。
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.");
}
// Superclass cache.
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;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
//没有找到实现方法,尝试寻找方法的解决者
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
//如果父类找到NSObject还没有找到,那么就会开始尝试_class_resolveMethod方法。
//注意,这些需要打开读锁,因为开发者可能会在这里动态增加方法实现,所以不需要缓存结果。
//此处虽然锁被打开,可能会出现线程问题,所以在执行完_class_resolveMethod方法之后,会goto retry,重新执行一遍之前查找的过程。
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_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;
}

// No implementation found, and method resolver didn't help.
// Use forwarding.
// 消息转发
imp = (IMP)_objc_msgForward_impcache;
//---> __objc_msgForward_impcache --> __objc_msgForward --> __objc_forward_handler --> objc_defaultForwardHandler
//在cache_fill中还会去调用cache_fill_nolock函数,如果缓存中的内容大于容量的 3/4就会扩充缓存,使缓存的大小翻倍。找到第一个空的 bucket_t,以 (SEL, IMP)的形式填充进去。
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();

return imp;
}

lookUpImpOrForward 会先根据实际情况看要不要从缓存中查找,然后再从objc_class的方法列表中查找,如果还没有找到再从父类的缓存和方法列表中找,一直找到NSObject如果发现还没有,那么就调用_class_resolveMethod,这时候_class_resolveMethod可能会通过代码为该类增加方法实现,所以在_class_resolveMethod结束之后还需要通过goto retry走一遍上面流程。上面的resolve过程有些地方称为动态方法决议,下面给出一个简单例子:

void customMethodIMP(id self, SEL _cmd) {
NSLog("customMethodIMP running");
}

@implementation IDLResolveTestClass

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if (aSEL == @selector(unimplementSelector)) {
class_addMethod([self class], aSEL, (IMP) customMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面例子中会判断如果向当前对象发送unimplementSelector消息的时候,resolveInstanceMethod会通过class_addMethod动态完类中添加unimplementSelector的IMP customMethodIMP,并返回YES,由于前面介绍了这个流程走完还会走一遍goto retry,所以就会在goto retry中找到customMethodIMP并执行。

如果方法决议还没有找到就准备走消息转发机制了,也就是:

imp = (IMP)_objc_msgForward_impcache;

先结合下面这张图有个大致的认知下,后面会整理个比较全面的流程图:

这里同时需要注意一点:初始化方法+initialize就是在这个阶段调用的。也就是在第一次往某个对象发送消息的时候如果没有初始化就会调用+initialize。

前面我们了解到缓存查找cache_getImp是汇编实现的,我们接着来看下类方法列表的查找是怎样的:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();

assert(cls->isRealized());
//在getMethodNoSuper_nolock方法中,会遍历一次methodList链表,从begin一直遍历到end。遍历过程中会调用search_method_list函数。
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);
//在search_method_list函数中,会去判断当前methodList是否有序,如果有序,会调用findMethodInSortedMethodList方法,这个方法里面的实现是一个二分搜索
//如果非有序,就调用线性的傻瓜式遍历搜索。
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;
}
}
//.....
return nil;
}

这里需要注意的是search_method_list方法,它会去判断当前methodList是否有序,如果有序,会调用findMethodInSortedMethodList方法,这个方法里面的实现是一个二分搜索,如果是无序的,就调用线性的傻瓜式遍历搜索。

消息查找已经大致介绍完了,如果这时候没有找到方法,就会进入下个阶段消息转发阶段了,消息转发顾名思义就是在当前对象不能处理当前消息的情况下将消息转给能够处理的对象进行处理。

我们紧接着上面_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

__objc_msgForward_impcache 内部很简单就是调用了__objc_msgForward,而在__objc_msgForward中也只是简单得转调了__objc_forward_handler。但是期间还有一系列的调用,我们没有办法看到源码,这部分大家可以看下杨萧玉的 Objective-C 消息发送与转发机制原理,这里为避免复杂化我们直接介绍结论:

在我们通过动态方法决议给类添加缺失的方法无效的情况下,Runtime就会继续调用****-(id)forwardingTargetForSelector:(SEL)aSelector,这个方法会根据传入的selector,返回一个备用的target,这个消息会被转发给它进行处理,如果-(id)forwardingTargetForSelector:(SEL)aSelector**** 返回nil 或者self,就会进入下一步。运行时系统首先会调用****-(NSMethodSignature )methodSignatureForSelector:(SEL)aSelector方法来获得记录了方法的参数和返回值的信息的方法签名。如果methodSignatureForSelector*** 返回的是nil, 运行时系统会抛出unrecognized selector exception
程序到这里就结束了否则会继续调用 -(void)forwardInvocation:(NSInvocation )anInvocation。我们这里要看下NSInvocation*有哪些功能,才能知道forwardInvocation 可以做哪些工作,NSInvocation定义如下:

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end

这里最关键的属性包括methodSignaturetargetselector,方法部分主要可以分成三类,参数设置获取部分,返回值设置获取部分,以及触发部分,触发可以通过invokeWithTarget交给某个具体目标对象执行。

接着我们再来看下NSMethodSignature:

@interface NSMethodSignature : NSObject

+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

@property (readonly) NSUInteger numberOfArguments;
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx NS_RETURNS_INNER_POINTER;

@property (readonly) NSUInteger frameLength;

- (BOOL)isOneway;

@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
@property (readonly) NSUInteger methodReturnLength;

@end

它主要包括了方法参数和返回值的信息获取。最后以一个流程图来做个总结:

整个过程可以分成如下三个阶段:

  1. 消息查找

这个阶段开始之前会进行一系列的检查,比如检查当前selector是否是忽略的selector,检查target是否为空。然后查找当前对象的缓存是否有当前的要查找的对象,如果没有则进入方法列表的查找阶段,这个阶段会先在当前对象的方法列表中查找是否有待查找的方法,如果没有则顺着继承链往上找,先找父类的方法缓存,再找方法列表。一直找到NSObject都没找到的时候,就进入消息动态决议阶段。

  1. 消息动态决议

消息动态决议实际上是通过resolveInstanceMethod针对当前的消息,添加一个补充的IMP,然后再重新走消息查找的流程。如果还没找到则进入消息转发流程。

  1. 消息转发

消息转发可以分成两种,第一种通过forwardingTargetForSelector它不对消息参数和返回值类型做处理单纯转发给某个对象,这种比较简单,但是灵活性不如后者。

第二种通过forwardInvocation它可以通过methodSignatureForSelector改写方法签名,并在forwardInvocation中对参数数值进行修改,然后再调用invokeWithTarget将消息转发给某个特定的对象,在拿到返回值的时候还可以对返回值进行处理,可以说这是最灵活的一种方式了,JSPatch以及Aspect都是基于这种方式实现的。

Contents