1. 初探
1.1 消息
objc_msgSend
当一条消息被发送到一个实例对象时:
- 通过对象的
isa
指针找到类结构体,在该类结构中查找分派表中的方法选择器。 - 如果找不到选择器,
objc_msgSend
将找到父类的类结构体,在父类结构中查找分派表中的方法选择器。 - 如果一直找不到,继续查找父类直到
NSObject
类。 - 一旦找到选择器,函数就会调用表中的方法,并将接收对象的数据结构传递给它。
为了加快消息传递过程,运行时系统会在使用方法时缓存方法的选择器和地址。
每个类都有一个单独的缓存,它可以包含继承方法和类中定义的方法的选择器。在搜索分派表之前,消息传递例程首先检查接收对象类的缓存。如果方法选择器在缓存中,则消息传递仅比函数调用稍慢。一旦程序运行了足够长的时间来“预热”其缓存,它发送的几乎所有消息都会找到一个缓存方法。当程序运行时,缓存会动态增长以容纳新消息。
获取方法的地址
避免动态绑定的唯一方法是获取方法的地址,并像调用函数一样直接调用它。当一个特定的方法将被连续执行多次,并且你希望避免每次执行该方法时消息传递的开销时,这在极少数情况下可能是合适的。
对于在NSObject
类methodForSelector:
中定义的方法,可以请求指向实现方法的过程的指针,然后使用该指针调用该过程。methodForSelector:
返回的指针必须仔细转换为正确的函数类型。返回类型和参数类型都应包含在转换中。
下面的示例显示如何调用实现setFilled:
方法的过程:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
传递给过程的前两个参数是接收对象(self)和方法选择器(_cmd)。这些参数隐藏在方法语法中,但在将方法作为函数调用时必须显式。
使用methodForSelector:
绕过动态绑定可以节省消息传递所需的大部分时间。但是,只有在特定消息重复多次的情况下(如上面所示的for循环中所示),节省的开销才是显著的。
注意methodForSelector:由Cocoa运行时系统提供;它不是Objective-C语言本身的特性。
1.2 动态方法解析
比如我们声明了一个方法但没有实现它:
@interface TestPerson : NSObject
- (void)resolveThisMethodDynamically;
@end
@implementation TestPerson
@end
我们可以在resolveInstanceMethod:
中通过class_addMethod
进行添加:
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"%s", __func__);
}
@implementation TestPerson
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
NSLog(@"%s", __func__);
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
如果在resolveInstanceMethod:
中设置了断点:
![](https://img.haomeiwen.com/i3344530/f27264a6c94f6737.png)
我们可以看到首先方法没有被缓存,调用_objc_msgSend_uncached()
,之后查找IMP
或者转发lookUpImpOrForward()
,然后来到resolveInstanceMethod:
。
这是发生在消息转发之前的,在执行完class_addMethod
并返回YES
后就把选择器和对应的实现添加到类里面了,同时会进行缓存。
2020-03-25 13:31:38.152558+0800 RuntimeDemo[61884:2844585] +[TestPerson resolveInstanceMethod:]
2020-03-25 13:31:54.485904+0800 RuntimeDemo[61884:2844585] dynamicMethodIMP
2020-03-25 13:31:54.486149+0800 RuntimeDemo[61884:2844585] dynamicMethodIMP
可以看到,resolveInstanceMethod:
只调用了一次,就是因为这个选择器和实现已经被缓存起来了。
1.3 消息转发
当一条消息被发送到一个实例对象时:
- 通过对象的
isa
指针找到类结构体,在该类结构中查找分派表中的方法选择器。 - 如果找不到选择器,
objc_msgSend
将找到父类的类结构体,在父类结构中查找分派表中的方法选择器。 - 如果一直找不到,继续查找父类直到
NSObject
类。 - 一旦找到选择器,函数就会调用表中的方法,并将接收对象的数据结构传递给它。
当一条消息被发送到一个实例对象时,和上面类似,但不同的是类方法是存储在元类中的。
![](https://img.haomeiwen.com/i3344530/a4baa47db7c779f1.png)
如果一个实例方法不能在类和继承链的方法列表中不能被找到,则会进入方法解析和消息转发流程:
- 首先判断当前实例的类对象是否实现了
resolveInstanceMethod:
方法,如果实现的话,会调用resolveInstanceMethod
方法。这个时候我们可以在resolveInstanceMethod
方法里动态的添加该SEL
对应的方法。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现resolveInstanceMethod:
方法,则会进入消息转发。 - 调用
forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil
,继续下一个尝试。 - 调用
methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常。 - 调用
forwardInvocation:
方法,将第3步获取到的方法签名包装成Invocation
传入,如何处理就在这里面了。如果调用[super forwardInvocation:]
则直接调用doesNotRecognizeSelector
抛出异常。
![](https://img.haomeiwen.com/i3344530/2037c479d2355e84.png)
2. 底层探究
2.1 消息
实例方法
Objective-C的对象是基于Runtime创建的结构体。
@interface Father : NSObject
@end
@implementation Father
@end
int main(int argc, const char * argv[])
{
@autoreleasepool {
Father *father = [[Father alloc] init];
}
return 0;
}
alloc
方法会为对象分配一块内存空间,空间的大小为 isa_t
(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化isa_t
,而isa_t
是一个union
类型的结构体(或者称之为联合体),它的结构是在Runtime里被定义的。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
从isa_t
的结构可以看出,isa_t
可以存储struct
、uintptr_t
或者Class
类型。
init
方法就直接返回了初始化好的对象,father
指针指向这个初始化好的对象。
类方法
这个对象只存放了一个isa_t
结构体和成员变量,对象的方法在哪里?
struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
我们看到,类对象里同样储存着一个isa_t
的结构体,super_class
指针,cache_t
结构体,class_data_bits_t
指针。
其中class_data_bits_t
指向类对象的数据区域,数据区域存放着这个类的实例方法链表。
类方法存在元类对象的数据区域。也就是说,有对象,类对象,元类对象三个概念:
- 对象是在运行时动态创建的,可以有无数个。
- 类对象和元类对象在
main
方法之前创建的,分别只会有一个。
objc_msgSend
在源码中objc_msgSend
是以汇编的形式存在的,我们来看一下arm64
的:
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in x17
* x16 reserved for our use but not used
*
********************************************************************/
#if SUPPORT_TAGGED_POINTERS
.data
.align 3
.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
.fill 16, 8, 0
.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
.fill 256, 8, 0
#endif
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 1.是否为空,直接返回
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
// 2.isa处理完毕
LGetIsaDone:
// 3. 查找缓存
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
可以看到整个过程是这样的:
- LReturnZero:判断了是否为空,直接返回
- LGetIsaDone:isa处理完毕
- CacheLookup NORMAL:查找缓存
下面看CacheLookup NORMAL部分:
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 label we may have loaded
// an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd$1,
// then our PC will be reset to LLookupRecover$1 which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
// 1.缓存命中
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
// 2.没有找到方法
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
// 继续查找
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
主要有3个方法:
-
CacheHit:缓存命中,直接调用或返回
IMP
- CheckMiss:没有找到方法
- add:继续查找
下面我们看CheckMiss:
.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
我们调用的是CacheLookup NORMAL,那么我们会跳转到__objc_msgSend_uncached
。
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
下面我们来到MethodTableLookup
:
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
里面最重要的就是_lookUpImpOrForward
,这时我们已经在汇编中搜不到了,但是其实这个C函数,我们在工程里面搜索lookUpImpOrForward
就能找到。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 如果cache是YES,则从缓存中查找IMP。
if (cache) {
// 通过cache_getImp函数查找IMP,查找到则返回IMP并结束调用
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
// 判断类是否已经被创建,如果没有被创建,则将类实例化
if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();
// 对类进行实例化操作
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
// 第一次调用当前类的话,执行initialize的代码
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
// 对类进行初始化,并开辟内存空间
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
retry:
runtimeLock.assertReading();
// 尝试获取这个类的缓存
imp = cache_getImp(cls, sel);
if (imp) goto done;
{
// 如果没有从cache中查找到,则从方法列表中获取Method
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 如果获取到对应的Method,则加入缓存并从Method获取IMP
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
// 循环获取这个类的缓存IMP 或 方法列表的IMP
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
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_t对象。如果找到则缓存查找到的IMP
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// 如果没有找到,则尝试动态方法解析
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// 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 = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
lookUpImpOrForward
比较长,我们看核心的部分:
- 如果
cache
是YES,则从缓存中查找IMP
。也就是说如果之前执行过的方法,就在缓存中有,就不需要下面的操作了。 - 判断类是否已经被创建,如果没有被创建,则将类实例化。
- 第一次调用当前类的话,执行
initialize
的代码。 - 尝试获取这个类的缓存。
- 如果没有从
cache
中查找到,则从自己的方法列表中获取method
。 - 如果还没有,就从父类缓存或者方法列表获取
IMP
。 - 如果没有找到,则尝试动态方法解析。
- 如果没有
IMP
被找到,并且动态方法解析也没有处理,则进入消息转发阶段。
这里再扩展一下,类对象里有一个cache_t
结构体用于方法命中后的缓存,它的结构如下:
struct cache_t {
struct bucket_t *_buckets; //一个散列表,用来方法缓存
mask_t _mask; // 分配用来缓存bucket的总数
mask_t _occupied; // 表明目前实际占用的缓存bucket的个数
}
struct bucket_t {
private:
cache_key_t _key; // 缓存key
IMP _imp; // 方法实现imp
}
如果没有命中,则会从类对象的class_data_bits_t
指针找到数据区域,数据区域里用链表存放着类的实例方法。实例方法也是一个结构体,其结构为:
struct method_t {
SEL name; // 方法选择器
const char *types; // 编译器将每个方法的返回值和参数类型编码为一个字符串
IMP imp; // 方法实现imp
};
objc_msgSend
会在类对象的方法链表里按链表顺序去匹配SEL
,匹配成功则停止,并将此方法加入到类对象的 _buckets
缓存起来。如果没找到则会通过类对象的superclass
指针找到其父类,去父类的方法列表里寻找,直到NSObject
。
注意:父类中查找也是从缓存开始的,然后才是方法链表。
2.2 动态方法解析
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
这里就到了我们熟悉的动态方法解析。
- 如果我们调用的是实例方法,那么
cls
就不是元类,就会执行实例方法的动态解析。 - 如果我们调用的是类方法,
cls
就是元类,先调用类方法的动态解析。如果没有找到,我们还会调用实例方法的动态解析。这里调用元类的实例方法,会从根元类(元类isa指向根元类)开始找,最终会找到NSObject
的resolveInstanceMethod
实例方法。
注意:类方法,存储在元类中是实例方法。
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, 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(inst, sel, cls);
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));
}
}
}
static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}
resolveInstanceMethod
和resolveClassMethod
都会看cls
是否实现了对应的方法,然后给这个类发消息,如果动态解析提供了方法,那么下次lookUpImpOrNil
就会命中。resolveMethod_locked
最后就会返回对应的IMP
。
2.3 消息转发
// 如果没有IMP被发现,并且动态方法解析也没有处理,则进入消息转发阶段
imp = (IMP)_objc_msgForward_impcache;
这部分又回到了汇编代码__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
下面我们又回到了C函数_objc_forward_handler
:
// Default forward handler halts the process.
__attribute__((noreturn, cold)) 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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
这就是我们最常看到的unrecognized selector sent to instance了。但是,等一下,说好的消息转发呢?
这部分苹果没有开源,所以没法看。但是我们怎么知道中间有这些过程的呢...其实我们还有一个神器:
@interface Father : NSObject
- (void)test;
@end
@implementation Father
@end
// 内部的一个打印信息的函数
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
[[[Father alloc] init] test];
instrumentObjcMessageSends(NO);
}
return 0;
}
这个方法开启之后会在/private/tmp目录下创建一个msgSends-xxx
,xxx
是内部生成的一个编号。
![](https://img.haomeiwen.com/i3344530/396c13ec10150f8a.png)
看到调用里面,对对象进行了初始化,然后调用了
- resolveInstanceMethod
- forwardingTargetForSelector
- methodSignatureForSelector
- resolveInstanceMethod
- doesNotRecognizeSelector
你可能要问,那forwardInvocation
呢?这是因为我们有处理methodSignatureForSelector
,这时是不会调用forwardInvocation
的。现在我们加上:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s", __FUNCTION__);
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
现在再运行一次,就有了:
![](https://img.haomeiwen.com/i3344530/ce126e721dfa5b06.png)
如果你有Hopper Disassembler
我们先看看崩溃时的调用堆栈:
2020-03-28 16:11:36.785540+0800 KCObjcTest[8089:212794] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Father test]: unrecognized selector sent to instance 0x100530f90'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff3c8218ab __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00000001002cf1e0 objc_exception_throw + 48
2 CoreFoundation 0x00007fff3c8a0b61 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff3c785adf ___forwarding___ + 1427
4 CoreFoundation 0x00007fff3c7854b8 _CF_forwarding_prep_0 + 120
5 KCObjcTest 0x0000000100000f40 main + 64
6 libdyld.dylib 0x00007fff73e497fd start + 1
7 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
现在我们的首要目标就是找到_CF_forwarding_prep_0
。
首先,把这个Mach-O拷出来,并拖入Hopper Disassembler:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
找到___forwarding_prep_0___
:
![](https://img.haomeiwen.com/i3344530/70d1812d6eaa3be9.png)
下一步是____forwarding___
:
![](https://img.haomeiwen.com/i3344530/eff6ed25128147d6.png)
在____forwarding___
发现了forwardingTargetForSelector
:
![](https://img.haomeiwen.com/i3344530/2bc3c8abee81bcf4.png)
阅读之后发现,没有实现或者返回为空则会跳转到loc_140277
:
![](https://img.haomeiwen.com/i3344530/67807b122c900190.png)
在loc_140277
下面几行发现了methodSignatureForSelector
,如果没有实现、或返回为空会跳转。我们这里继续看,如果没有实现_forwardStackInvocation:
,则会跳转loc_14041b
。
![](https://img.haomeiwen.com/i3344530/b8d0215f22eeb54b.png)
在这里我们看到,如果实现了forwardInvocation:
,则会调用forwardInvocation:
。
这样也可以看到整个流程。
3. Runtim简单的例子
我们做一个简单的例子,在ViewController
动态创建另一个VC,然后push出来。
- (void)pushToAnyVCWithData:(NSDictionary *)dataDict
{
// 实例化对象
id instance = nil;
//从字典获取类名
const char *clsName = [dataDict[@"class"] UTF8String];
// 获取类对象
Class cls = objc_getClass(clsName);
// 尝试从Storyboard初始化
@try {
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
instance = [sb instantiateViewControllerWithIdentifier:dataDict[@"class"]];
} @catch (NSException *exception) {
// 如果Storyboard中没有,则动态创建类
if (!cls) {
// 获取父类对象
Class superClass = [UIViewController class];
cls = objc_allocateClassPair(superClass, clsName, 0);
class_addIvar(cls, "ending", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
class_addIvar(cls, "show_lb", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
objc_registerClassPair(cls);
//⚠️注意: 考点1,添加ivar需要在objc_registerClassPair之前,下面这样是不行的
//class_addIvar(cls, "ending", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
//class_addIvar(cls, "show_lb", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
// 把方法添加到动态类对象中
// ⚠️注意: 考点2,这个可以在objc_registerClassPair之后
Method method = class_getInstanceMethod([self class], @selector(lg_instancemethod));
IMP methodIMP = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
BOOL rest = class_addMethod(cls, @selector(viewDidLoad), methodIMP, types);
NSLog(@"rest == %d",rest);
}
instance = [[cls alloc] init];
} @finally {
NSLog(@"OK");
}
NSDictionary *dict = dataDict[@"data"];
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 检测是否存在key的属性
if (class_getProperty(cls, [key UTF8String])) {
[instance setValue:obj forKey:key];
}
// 检测是否存在key的变量
else if (class_getInstanceVariable(cls, [key UTF8String])){
[instance setValue:obj forKey:key];
}
}];
[self.navigationController pushViewController:instance animated:YES];
}
我们再来看看动态添加的方法:
- (void)lg_instancemethod
{
// ⚠️注意:考点,self是谁?
// 答案:objc_msgSend,谁调用是谁。self只是型参。
[super viewDidLoad];
// 动态添加的方法,只能通过KVC实现赋值
[self setValue:[UIColor orangeColor] forKeyPath:@"view.backgroundColor"];
[self setValue:[[UILabel alloc] initWithFrame:CGRectMake(100, 200, 200, 30)] forKey:@"show_lb"];
UILabel *show_lb = [self valueForKey:@"show_lb"];
[self.view addSubview:show_lb];
show_lb.text = [self valueForKey:@"ending"];
show_lb.font = [UIFont systemFontOfSize:14];
show_lb.textColor = [UIColor blackColor];
show_lb.textAlignment = NSTextAlignmentCenter;
show_lb.backgroundColor = [UIColor whiteColor];
NSLog(@"hello word");
}
网友评论