Runtime之消息转发

我们都知道,在iOS开发中方法的调用实现实际上是通过消息发送的机制实现的,我们可以简单的理解为:

[A a];调用A类的a方法,实际上就是给类A的a方法发消息。

在这个过程中又会经历:方法查找、方法调用、方法转发这几个阶段,下面我们来通过这几个阶段来看下Runtime在这几个环境是如何实现的。

在研究方法调用之前,我们先看几个方法调用先关的结构,了解这些结构之后会方便我们后续了解消息的调用流程。

Method

在我们上文介绍方法交换时,我们利用class_getInstanceMethod方法获取某个方法时,返回的结构体都是Method,我们先来看下这个结构体包含的内容:

typedef struct method_t *Method;
// OC实例方法或类方法结构体
struct method_t {
// 方法名
SEL name;
// 记录方法返回值和方法参数
const char *types;
// 方法实现
MethodListIMP imp;
};

从上面的结构中我们看到一个方法的结构体中包含三个属性:方法名 name SEL 类型;types 方法编码方式包含该方法的返回值类型和参数类型;方法实现地址;

下面我们先从一个简单的例子来看下这三个值分别是什么样的,然后我们在进行深入的研究:

首先我们有一个方法,如下:

- (BOOL)canSayHi:(NSString *)name {
NSLog(@"can say hi to %@",name);
return YES;
}

然后我们尝试获取这个方法的方法名,编码方式,以及方法实现:

- (void)getMethodStructure {
Method method = class_getInstanceMethod([self class], @selector(canSayHi:));
NSLog(@"method sel %@",NSStringFromSelector(method_getName(method)));
NSLog(@"method types %@",[NSString stringWithFormat:@"%s",method_getTypeEncoding(method)]);
NSLog(@"method imp %p",method_getImplementation(method));
}

然后我们看下打印结果:

2020-10-31 17:32:26.711510+0800 Runtime_MsgSend[24686:737212] method sel canSayHi:
2020-10-31 17:32:26.711697+0800 Runtime_MsgSend[24686:737212] method types B24@0:8@16
2020-10-31 17:32:26.711830+0800 Runtime_MsgSend[24686:737212] method imp 0x100725c20

打印结果非常清晰这个方法的方法名为 canSayHi: ,方法编码方式为 B24@0:8@16 ,方法实现地址为 0x100725c20

下面我们来仔细看下这三个属性的意义:

SEL

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

从定义我们可以看出SEL 实际上是一个 objc_selector 类型的结构体,但是这个结构体在Runtime中我们并没有找到定义。

我们先来看下官方文档给出的解释:

A selector is the name used to select a method to execute for an object, or the
unique identifier that replaces the name when the source code is compiled. A
selector by itself doesn’t do anything. It simply identifies a method. The only
thing that makes the selector method name different from a plain string is that
the compiler makes sure that selectors are unique. What makes a selector useful
is that (in conjunction with the runtime) it acts like a dynamic function pointer
that, for a given name, automatically points to the implementation of a method
appropriate for whichever class it’s used with. Suppose you had a selector for
the method run, and classes Dog, Athlete, and ComputerSimulation (each of which
implemented a method run). The selector could be used with an instance of each of
the classes to invoke its run method—even though the implementation might be
different for each.

大概的意思是:selector表示某个对象的某个方法的名字,selector跟一个普通字符串的唯一区别是selector用来在编译时保证方法名的唯一。

因此在绝大多数情况下,我们可以直接把SEL当做一个字符串看待。下面我们来验证下:

在一个类中我们有同名(参数和返回值也相同)的类方法和对象方法

- (BOOL)canSayHi:(NSString *)name {
NSLog(@"can say hi to %@",name);
return YES;
}
+ (BOOL)canSayHi:(NSString *)name {
NSLog(@"can say hi to %@",name);
return YES;
}

下面我们来打印下这两个方法的实现:

- (void)getMethodStructure {
Method method = class_getInstanceMethod([self class], @selector(canSayHi:));
NSLog(@"method sel %@",NSStringFromSelector(method_getName(method)));
Method method1 = class_getClassMethod([self class], @selector(canSayHi:));
NSLog(@"method sel %@",NSStringFromSelector(method_getName(method1)));
BOOL isEqual = sel_isEqual(method_getName(method), method_getName(method1));
NSLog(@"isEqual %@",@(isEqual));
}

输出结果:

2020-10-31 17:54:22.838428+0800 Runtime_MsgSend[24985:752267] method sel canSayHi:
2020-10-31 17:54:22.838743+0800 Runtime_MsgSend[24985:752267] method sel canSayHi:
2020-10-31 17:54:22.839079+0800 Runtime_MsgSend[24985:752267] isEqual 1

首先我们通过打印发现这两个方法的方法名是相同的都是 canSayHi: ,但是鉴于我们上面看到在runtime中SEL的定义实际上是一个objc_selector结构体,因此我们还是使用sel_isEqual方法来判断这两个方法是否相等,从打印结果来看,显而易见两者是相等的。鉴于系统并未开放objc_selector结构体,我们大多数情况下将SEL看做方法名标识的字符串也是没问题的。

types

方法的type实际上是个字符串,从上面的打印我们看到 canSayHi: 方法的值为 B24@0:8@16 这又该怎么解析呢?

实际苹果官方文档专门有针对这个字段的解释具体可以看 这里

下面我们对 B24@0:8@16 进行拆解解析:

B 24 @ 0 : 8 @ 16
返回值类型为bool 占用总空间为24字节 id(SEL) 从0开始存储 SEL 从第0位开始占8位空间 id(NSString) 从8位开始占16位空间

这个位置研究的不是太明白,有比较了解的可以回复我,或者贴下文档地址,感激!!!

MethodListIMP

我们来看下MethodListIMP的定义:

using MethodListIMP = IMP;

IMP的定义如下:

/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

实际上MethodListIMP就是IMP。

而从我们上面打印的结果看IMP实际就是一个指针,指向方法实现的函数,IMP和SEL是一一对应的。

那是否意味着,我们拿到方法的地址之后可以直接调用这个方法??? 下面我们来验证下,当然为了方便我们先简单的定义一个没有参数也没有返回值的方法:

- (void)test {
NSLog(@"test");
}

然后我们调用下面这个方法:

- (void)exeIMP {
Method method = class_getInstanceMethod([self class], @selector(test));
IMP imp = method_getImplementation(method);
imp();
}

我们看下结果:

2020-10-31 18:39:18.334411+0800 Runtime_MsgSend[25510:782591] test

方法调用成功!

上述就是Method结构体的介绍,接下来我们来看下消息的调用流程。

方法调用

我们都知道方法调用的实现实际上是通过objc_msgSend方法,下面我们来看下这个方法:

objc_msgSend

/**
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
*   A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
*
* @note When it encounters a method call, the compiler generates a call to one of the
*  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
*  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;
*  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
*  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
*/
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

参数介绍:

self op
表示指向类的实例(类是元类的实例)的指针用来接收消息 SEL 类型 表示调用的方法的方法名 方法的参数

注意:当这个方法被调用时,实际上编译器会调用 objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret 。当给对象的superclass发送消息时(使用super关键词)调用 objc_msgSendSuper 方法;当方法有返回值时调用的是 objc_msgSendSuper_stret 或者 objc_msgSend_stret

objc_msgSendSuper

我们来看下这个方法的实现:

/**
* Sends a message with a simple return value to the superclass of an instance of a class.
*
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
*  context the message was sent to, including the instance of the class that is to receive the
*  message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
*   A variable argument list containing the arguments to the method.
*
* @return The return value of the method identified by \e op.
*
* @see objc_msgSend
*/
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

我们看到这个方法的第一个参数是objc_super类型,我们在进一步看下这个类型:

/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */
};

结合上面对objc_msgSendSuper注释的理解,我们可以看出objc_super结构体中两个属性的含义:

  • receiver: 调用super方法的子类,用于方法发送后消息的接收
  • super_class:子类 receiver的父类 方法查找会先从这个类开始

objc_msgSend

objc_msgSend方法的实现,在runtime中实际是用汇编语言实现的,下面我们来看下实现,顺便猜想实现原理:

ENTRY _objc_msgSend
//cbz 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
//在汇编代码中 r0 表示函数的第一个参数,所以 r0 代表消息的接受者,
// 判断 r0 (消息接受者)是否为空,如果为空跳转到 标签 `LNilReceiver_f`
cbz	r0, LNilReceiver_f
// 将 [r0] 的值加载到r9寄存器
ldr	r9, [r0]		// r9 = self->isa
// 调用GetClassFromIsa方法 根据isa获取到对应的类
GetClassFromIsa			// r9 = class
// 调用CacheLookup方法 查找方法缓存
CacheLookup NORMAL
// cache hit, IMP in r12, eq already set for nonstret forwarding
// 如果缓存命中 IMP 放到r12中 调用imp方法
bx	r12			// call imp
// 没有找到缓存的方法
CacheLookup2 NORMAL
// cache miss r0的内容加载到r9中
ldr	r9, [r0]		// r9 = self->isa
// 调用GetClassFromIsa方法 根据isa获取到对应的类
GetClassFromIsa			// r9 = class
// b 跳转指令,可带条件跳转与cmp配合使用
// 跳转去执行 __objc_msgSend_uncached 既然没有找到方法,就去从类,父类,元类中查找
b	__objc_msgSend_uncached
LNilReceiver:
// r0 is already zero
// 消息接受者为空 表示r0为空
mov	r1, #0
mov	r2, #0
mov	r3, #0
// return 0
FP_RETURN_ZERO
// 直接调用lr方法
bx	lr
END_ENTRY _objc_msgSend

从上面的调用流程中我们看到,首先会判断这条消息的接收者是否为nil,如果为nil直接调用LNilReceiver_f方法,如果不为nil,先根据isa获取到对应的类,然后在方法调用的缓存中查找是否有缓存如果有直接调用,如果没有则去调用__objc_msgSend_uncached方法。

下面我们在来看下这个方法:

STATIC_ENTRY __objc_msgSend_uncached
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r9 is the class to search
MethodTableLookup NORMAL	// returns IMP in r12
bx	r12
END_ENTRY __objc_msgSend_uncached

从上图我们看出__objc_msgSend_uncached方法主要是调用了MethodTableLookup方法,而在这个方法中,主要又是调用了_class_lookupMethodAndLoadCache3方法,那么我们来看下这个方法的实现:

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

实际上就是调用了lookUpImpOrForward,对于这个方法我们就比较熟悉了。

lookUpImpOrForward

我们先来看下这个方法的实现:

// 当方法调用的缓存中找不到对应的方法时就会调用这个方法
// cls 消息的接受者
// sel 要调用的方法
// initialize 是否已经初始化
// cache 是否需要查找缓存 但是只是在当前类使用 在父类统一都会先查找缓存在查找方法列表
// resolver 是否需要动态解析
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
// 如果在缓存中找到对应的方法 则直接返回
if (cache) { //从汇编过来是NO
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
//检查是否是已知的 项目的所有类都是从镜像加载出来的 这里是在镜像加载出的类中查找 如果不是则返回false
// 但是这里并没有使用返回值
checkIsKnownClass(cls);
// 这个类的内容还没有被加载 会通过镜像加载这个类先关的内容 比如 属性 方法列表
if (!cls->isRealized()) {
realizeClass(cls);
}
// 如果类需要被initialize但是目前还没有初始化
if (initialize  &&  !cls->isInitialized()) {
//当cls需要初始化和没有初始化的时候 进行cls初始化,
//初始化会加入到一个线程,同步执行,先初始化父类,再初始化子类
//数据的大小最小是4,扩容规则是:n*2+1;
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
//再次获取imp
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
//在本类中查找method
{//从cls->data()->methods查找method
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {//找到添加到cache中
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
//从cls->superclass->data()->methods查找methd,supercls没有查找出来,再查找父类的父类。
{
unsigned attempts = unreasonableClassCount();
// 递归查找父类 直到父类为nil
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();
_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;
}
//如果没找到resolveInstanceMethod 和resolveClassMethod,
//	进行消息转发 阶段
imp = (IMP)_objc_msgForward_impcache;
//填充 cache
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}

这个方法的实现比较长,下面我们从下图中来看下整个流程:

下面我们来看下lookUpImpOrForward方法执行内部的几个条件和关键方法

checkIsKnownClass

我们来看下这个方法的实现,在这个方法的实现中主要是调用了isKnownClass方法,我们直接看这个方法的实现:

// 判断一个类是否为已知类
// 在共享cache或者加载镜像的数据段或者通过obj_allocateClassPair方法创建的类
static bool isKnownClass(Class cls) {
// 查找的顺序是先查找共享缓存,然后是通过obj_allocateClassPair创建的类,最后才是从MACH-O的数据段进行查找
return (sharedRegionContains(cls) ||
NXHashMember(allocatedClasses, cls) ||
dataSegmentsContain(cls));
}

什么情况下会出现非已知的类呢?

当我们使用NSStringFromClass()方法创建一个类时,如果这个类的名字写错了这里就检测为一个未知类。

realizeClass

递归初始化类(父类)的结构,对类的第一次初始化,包括配置类的读写空间(class_rw_t)并且返回类的正确的结构体,就相当于搭好了这个类的框架这里主要是为了后续可以从类的方法列表中查找方法。

static Class realizeClass(Class cls)
{
runtimeLock.assertLocked();
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
// fixme verify class is not in an un-dlopened part of the shared cache?
//首先将rw赋值给ro,因为数据结构一样可以直接强制转化
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {//是否已经初始化过,初始化过的哈 则 cls->rw 已经初始化过
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// 正常情况下 申请class_rw_t空间
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;//cls->rw->ro 指向现在的ro
rw->flags = RW_REALIZED|RW_REALIZING;//realized = 1 and  realizing = 1
cls->setData(rw);//赋值
}
isMeta = ro->flags & RO_META;//是否是元类
rw->version = isMeta ? 7 : 0;  // 元类版本是7,旧版的6,否就是0
// Choose an index for this class.
//设置cls的索引
cls->chooseClassArrayIndex();
// 如果父类没有初始化则进行初始化
// root_class 做完需要设置RW_REALIZED=1,
// root metaclasses 需要执行完.
//从NXMapTable 获取cls ,然后进行初始化
//从NXMapTable 获取cls->isa ,然后进行初始化
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()));
// Update superclass and metaclass in case of remapping
cls->superclass = supercls;
cls->initClassIsa(metacls);
// 协调实例变量偏移/布局
//可能重新申请空间 class_ro_t,更新我们的class_ro_t
if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// 设置setInstanceSize 从ro->instanceSize
cls->setInstanceSize(ro->instanceSize);
//拷贝flags 从ro到rw中
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}
//添加superclass指针
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
// Attach categories
//类别的方法 在编译的时候没有添加到二进制文件中,在运行的时候添加进去的
methodizeClass(cls);
return cls;
}

这个方法主要操作是:

  • 将class->data指向的数据强制转化为class_ro_t结构体,因为编译期间class->data指向的就是class_ro_t结构体,所以这一步的转化是没有问题的
  • 生成一个class_rw_t结构体
  • 将class_rw_t的ro指针指向上一步转化出的class_ro_t结构体
  • 设置class_rw_t的flags值
  • 设置class->data指向class_rw_t结构体
  • 调用methodizeClass函数

因为这里不是我们本次介绍的主角,所以我们不在进一步的详细介绍。

_class_initialize

上一步在realizeClass中我们完成了类的初始化,_class_initialize这个方法主要是调用我们OC中的+ (void)initialize方法,在_class_initialize中主要是调用了callInitialize方法,我们看下这个方法的实现

// 调用类中的+ (void)initialize方法
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}

而调用规则

void _class_initialize(Class cls) {
supercls = cls->superclass;
if (supercls  &&  !supercls->isInitialized()) {
_class_initialize(supercls);
}
// 调用+initialize方法
callInitialize(cls);
}

从上面的调用顺序上我们可以看出类的+initialize方法的调用顺序是先调用父类在调用子类。且子类不会覆盖父类。如果子类没有实现这个方法那么会调用父类的方法,也就是说父类的方法会调用多次。

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)
{
// 在mlists中查找sel
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}

log_and_fill_cache和cache_getImp

cache_getImp的实现在Runtime中并未找到实现,不过我们可以从log_and_fill_cache的实现反推cache_getImp的实现,所以我们先看下log_and_fill_cache的实现

// 方法缓存
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// 获取到类对应的缓存
cache_t *cache = getCache(cls);
// 从类对应的缓存中找到方法缓存的key
cache_key_t key = getKey(sel);
// 在cache中根据key和receiver查找bucket
bucket_t *bucket = cache->find(key, receiver);
// 如果key==0 k++
if (bucket->key() == 0) cache->incrementOccupied();
// 存储缓存
bucket->set(key, imp);
}

从上面的代码中我们可以看出,我们可以通过类名获取到类对应的方法缓存cache,然后根据方法名获取到缓存的cache的key,然后根据key和receiver获取到对应缓存的bucket,然后将要缓存方法添加到缓存中,这里我们注意bucket是通过key和receiver获取而不是cls。所以这里我们添加的缓存是子类的缓存中。

下面我们来看下bucket_t的结构:

struct bucket_t {
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
// unsigned long 的指针,其实是一个被 hash 化的一串数值,就是方法的 sel
cache_key_t _key;
// 保存着对应的函数地址
MethodCacheIMP _imp;
#endif
}

_class_resolveMethod

// 动态解析方法
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 不是元类 则调用动态解析实例方法
if (! cls->isMetaClass()) {
//首先调用
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
//如果是元类 则应该动态解析类方法
_class_resolveClassMethod(cls, sel, inst);
// 查找这个类的方法列表中是否有这个方法对应的IMP
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 如果方法列表中没有找到IMP 则调用_class_resolveInstanceMethod方法????? 为什么
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}

看完了上面的方法,我们有个疑问:为什么在元类的情况下,在调用了_class_resolveClassMethod方法后,我们仍然要判断lookUpImpOrNil如果没有找到方法则调用_class_resolveInstanceMethod???

下面我们来先看下_class_resolveClassMethod方法

_class_resolveClassMethod

// 动态解析类方法
// 调用_class_resolveClassMethod 查找一个要被添加到cls的方法
// cls 应该是元类
// 如果方法已经存在了就不需要检查了
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
// 先判断resolveClassMethod方法是否实现 如果没实现则直接return
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
// resolveClassMethod在cls中实现了
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 调用_class_getNonMetaClass(cls, inst)的SEL_resolveClassMethod方法参数为SEL
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
// 在cls类中再次查找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 resolveClassMethod:%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));
}
}
}

从上面的代码我们可以看出_class_resolveClassMethod方法首先是查找SEL_resolveClassMethod在cls中是否有实现IMP,如果没有那么我们也不需要进行动态解析 直接返回。如果实现了,那么调用SEL_resolveClassMethod这个方法根据这个方法的返回值判断这个方法是否需要动态解析。然后我们在根据方法名在cls中查找对应的IMP,这里获取到IMP后纯粹是为了打日志。

下面我们看下lookUpImpOrNil这个方法

lookUpImpOrNil

// 与lookUpImpOrForward方法类似 但是如果imp是_objc_msgForward_impcache时返回nil
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
// 获取cls中sel方法的实现IMP 如果没有找到返回_objc_msgForward_impcache 表示需要转发
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
// 如果需要转发那么返回nil
if (imp == _objc_msgForward_impcache) {
return nil;
}else {
return imp;
}
}

实际上lookUpImpOrNil方法是对lookUpImpOrForward进行了一次包装,当找不到对应方法时直接返回nil而不是_objc_msgForward_impcache。

从上面实现我们看出实际上在_class_resolveClassMethod方法中我们并没有做什么时机操作,只是判断了cls中的SEL_resolveClassMethod方法是否实现,如果实现了那么返回的是true还是false。

那么接下来我们继续看下动态解析类方法后面做了什么?

// 查找这个类的方法列表中是否有这个方法对应的IMP
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 如果方法列表中没有找到IMP 则调用_class_resolveInstanceMethod方法????? 为什么
_class_resolveInstanceMethod(cls, sel, inst);
}

我们看到这里判断 如果当前类没有动态解析这个方法那么我们直接调用_class_resolveInstanceMethod方法。

那么我们先看下_class_resolveInstanceMethod这个方法里到底做了什么,然后再去思考上面提出的问题。

####### _class_resolveInstanceMethod

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;
}
//如果找到SEL_resolveInstanceMethod 则使用objc_msgSend函数
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 doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
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));
}
}
}

实际上_class_resolveInstanceMethod方法和_class_resolveClassMethod的实现基本一致,只不过将SEL_resolveClassMethod改成了SEL_resolveInstanceMethod。

但是实际上在_class_resolveInstanceMethod和_class_resolveClassMethod方法中我们并没有做什么事情,只是调用了SEL_resolveClassMethod和SEL_resolveInstanceMethod这两个方法。 然后重新走了一次lookUpImpOrForward方法。因此这相当于给我们一次机会让我们在SEL_resolveClassMethod或者SEL_resolveInstanceMethod方法中给方法添加一个默认实现。

比如下面的代码:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

这样我们再次调用lookUpImpOrForward方法是就可以查找到对应的方法和实现IMP了。

但是如果我们并没有实现resolveInstanceMethod这个方法那么接下来就走到了方法转发的流程。我们看到在lookUpImpOrForward方法的最后 如果没有做动态解析,那么我们就会走到下面这段代码中

//如果没找到resolveInstanceMethod 和resolveClassMethod,
//	进行消息转发 阶段
imp = (IMP)_objc_msgForward_impcache;
//填充 cache
cache_fill(cls, sel, imp, inst);

我们看到 直接返回了一个固定的IMP_objc_msgForward_impcache 然后将其放到子类的缓存中。

_objc_msgForward_impcache

我们先来看下_objc_msgForward_impcache的调用位置

STATIC_ENTRY __objc_msgForward_impcache //进入 _objc_msgForward_impcache
// Method cache version
// 这是一个不能调用的c函数
// Out-of-band Z is 0 (EQ) for normal, 1 (NE) for stret
beq	__objc_msgForward // 标志寄存器中Z标志位等于零时, 跳转到BEQ后标签__objc_msgForward处
b	__objc_msgForward_stret //否则跳转  __objc_msgForward_stret
END_ENTRY __objc_msgForward_impcache //结束 _objc_msgForward_impcache
ENTRY __objc_msgForward
// Non-stret version
MI_GET_EXTERN(r12, __objc_forward_handler)
ldr	r12, [r12]
bx	r12
END_ENTRY __objc_msgForward
ENTRY __objc_msgForward_stret

这也是通过汇编进行调用的,从上面我们看出实际上是调用的__objc_msgForward或者__objc_msgForward_stret方法,而这两个方法实际都是调用了__objc_forward_handler方法,下面我们重点看下这个方法

__objc_forward_handler

__attribute__((noreturn)) struct stret
objc_defaultForwardStretHandler(id self, SEL sel)
{
objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;

进而我们在看下objc_defaultForwardHandler方法的实现:

__attribute__((noreturn)) void
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);
}

看到这里我们一脸懵逼,因为我们知道后续是有一个消息转发的过程的 为何在这里我们只是看到了_objc_forward_stret_handler的实现 且实现为objc_defaultForwardHandler。消息转发的过程是如何被触发的呢?

我们看到在objc-runtime.mm类中实际上下面这个方法:

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}

也就是说 实际上系统给我们提供了一个默认的实现方法,但是这个方法是可以改变的,因此这里我猜想可能是因为系统在其他位置重新设置了_objc_forward_handler的值所以我们在调用的时候才有转发的流程。

消息转发

这里我们先来看下如果我调用了一个不存在的方法时错误的堆栈的样子:

2020-11-15 00:14:55.738458+0800 Runtime_MsgSend[72610:1510129] -[SubClass aaa]: unrecognized selector sent to instance 0x6000034780a0
2020-11-15 00:14:55.748137+0800 Runtime_MsgSend[72610:1510129] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SubClass aaa]: unrecognized selector sent to instance 0x6000034780a0'
*** First throw call stack:
(
0   CoreFoundation                      0x00007fff2043a126 __exceptionPreprocess + 242
1   libobjc.A.dylib                     0x00007fff20177f78 objc_exception_throw + 48
2   CoreFoundation                      0x00007fff20448c6f +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0
3   CoreFoundation                      0x00007fff2043e666 ___forwarding___ + 1489
4   CoreFoundation                      0x00007fff20440698 _CF_forwarding_prep_0 + 120

从上面的log中我们看出在给出错误之前实际上是调用了 CoreFoundation 的_CF_forwarding_prep_0和___forwarding___方法。那么这两个方法时何时被调用的呢?

我们打开CoreFoundation的源码发现有一个跟运行时相关的类:CFRuntime.h 但是类中我们并没法发现调用objc_setForwardHandler的位置也没有发现直接调用_CF_forwarding_prep_0或者___forwarding___的地方。

那么究竟在什么时机调用的这些方法呢?

在查找这个问题的时候我们看到有使用Hoop Disassemble反编译方法时可以看到下面的调用关系:

图中红框圈出来的方法是__CFInitialize方法。

但是很显然,我们在CFRuntime.h的__CFInitialize方法中并没有找到调用图中方法的位置:

void __CFInitialize(void) {
if (!__CFInitialized && !__CFInitializing) {
__CFInitializing = 1;
#if DEPLOYMENT_TARGET_WINDOWS || DEPLOYMENT_TARGET_IPHONESIMULATOR
if (!pthread_main_np()) HALT;   // CoreFoundation must be initialized on the main thread
#endif
// move this next line up into the #if above after Foundation gets off this symbol
_CFMainPThread = pthread_self();
#if DEPLOYMENT_TARGET_WINDOWS
// Must not call any CF functions
__CFTSDWindowsInitialize();
#elif DEPLOYMENT_TARGET_LINUX
__CFTSDLinuxInitialize();
#endif
__CFProphylacticAutofsAccess = true;
for (CFIndex idx = 0; idx < sizeof(__CFEnv) / sizeof(__CFEnv[0]); idx++) {
__CFEnv[idx].value = __CFEnv[idx].name ? getenv(__CFEnv[idx].name) : NULL;
}
// --------------此处没有找到调用相关的方法
#if !defined(kCFUseCollectableAllocator)
kCFUseCollectableAllocator = objc_collectingEnabled();
#endif
if (kCFUseCollectableAllocator) {
#if !defined(__CFObjCIsCollectable)
__CFObjCIsCollectable = (bool (*)(void *))objc_isAuto;
#endif
}
//........ 省略部分代码
}

所以这里我们猜测苹果在开源时删除了这部分方法调用。

由于对汇编熟悉,这里我们也不再进行更加深入的解读,但是在 Hmmm, What’s that Selector? 文章中作者进行了介绍并猜想___forwarding___的实现:

forwarding

int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwarding != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 僵尸对象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// selector 是否已经在 Runtime 注册过
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}

上面的方法实际上分了三步:

  • 先调用 forwardingTargetForSelector 方法获取新的 target 作为 receiver 重新执行 selector,如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),那就进入第二步
  • 调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,进入第三步。
  • 调用 doesNotRecognizeSelector 方法。

最终消息的发送就完成了。

下面我们来汇总下消息发送的流程:

看完了实现原理 下面我们通过一个demo来验证下我们上面的结论:

示例

动态解析

我们都知道,如果在类的方法列表中无法找到对应的方法,我们首先进行动态解析,即调用resolveInstanceMethod方法判断是否需要动态解析,我们可以在这个方法里动态的为这个类新增一个方法,实现可调用到对应方法的目的。

MessageForward.h

@interface MessageForward : NSObject
- (void)testMethod;
@end

MessageForward.m

@implementation MessageForward
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"testMethod"]) {
Method testmethod = class_getInstanceMethod(self, @selector(test));
IMP testIMP = method_getImplementation(testmethod);
class_addMethod(self, sel, testIMP, method_getTypeEncoding(testmethod));
return YES;
}
return NO;
}
- (void)test {
NSLog(@"MessageForward %s",__func__);
}
@end

当外部调用MessageForward的testMethod方法时,发现方法列表中无法找到对应的方法实现,那么就会判断是否需要动态解析,我们看到我们在resolveInstanceMethod方法中当sel为testMethod时我们返回了yes,这样就会导致方法调用位置会重新查找一次类中的方法列表,我们在返回值之前动态的通过class_addMethod方法给类添加了testMethod方法的实现。

这样外部在调用testMethod时,我们会动态解析为test方法进行调用。

消息转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"testMethod"]) {
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return signature;
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"testMethod"]) {
[self test];
} else {
[super forwardInvocation:anInvocation];
}
}

同样当外部调用testMethod方法时,由于我们并没有实现动态解析的方法,因此默认是走转发的逻辑,在转发的逻辑中我们实现了methodSignatureForSelector和forwardInvocation方法,如果要实现消息的转发逻辑这两个方法是必须全部实现的,在forwardInvocation方法中,我们判断如果当前调用的是testMethod方法,我们会直接调用test方法。

至此 消息发送和消息转发的逻辑就整理完成了,后面我们会继续看下Runtime中关联属性相关的实现逻辑。

参考文章

leewong
我还没有学会写个人说明!
上一篇

小程序mpvue怎么点击按钮获取button里面的值

下一篇

谷歌向美国员工提供了每周例行的COVID-19免费检测

你也可能喜欢

评论已经被关闭。

插入图片