目录:
- 参考博客:
- 学习新闻传递机制
-
- 选择子SEL
-
- 小的总结:
- objc_msgSend()执行过程
- objc_msgSend
-
- 在cache中快速查找
- 在方法类表中找到
- 总结缓存搜索和方法列表
- resolveMethod动态分析(动态决策)
- 消息转发
-
- 替换新闻接收者
- 全新闻转发
- 总结与思考
参考博客:
Objective-C 信息发送和转发机制的原理 [iOS开发]信息传递和信息转发机制 iOS八股文(六)objc_msgSend找到源码分析的方法 iOS八股文(七)objc_msgSend动态分析和新闻转发
学习新闻传递机制
我以前学过这个机制的一些内容:对象、信息和运行期
调用对象的方法,术语称为传递信息,信息有名称和选择器(方法),可以接受参数,也可能有返回值。
很多语言,比如 C ,调用一种方法实际上是跳到内存中的某一点,并开始执行代码。没有任何动态特性,因为它决定了编译。
而在 Objective-C 中,[object foo] 语法不会立即执行 foo该方法的代码。运行时给它object 发送一条叫 foo 的消息。这个消息可能是由的 object 为了处理,它可能会被转发给另一个对象,或者假装没有收到这个消息。同样的方法也可以实现多个不同的消息。这些都是在程序运行时决定的。
学习信息传输机制实际上是理解OC如何调用方法?
id returnValue = [someObject messageName:parameter];
这样一条代码编译器会将其处理成
id returnValue = objc_msgSend(someObject, @selectro(messageName:), parameter);
选择子SEL
OC根据方法名称(包括参数序列)进行编译,生成唯一用来区分这种方法的方法ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名称(包括参数序列)相同,他们就会ID是一样的。所以不管是父子,名字都一样。ID是一样的。
SEL sell1 = @selector(eat:); NSLog(@"sell1:%p", sell1); SEL sell2 = @selector(eat); NSLog(@"sell2:%p", sell2); //sell1:0x100000f63 //sell2:0x100000f68
需要注意的是:@selector等于把方法名翻译成方法名SEL方法名。它只关心方法名和参数数数,而不关心返回值和参数类型
生成SEL这个过程是固定的,因为它只是一种表示方法的方法ID,不管是哪一类,这个都是写的eat方法,SEL值是固定的
在Runtime中维护一个SEL的表,这个表存储SEL不按类别存储,只要相同SEL它将被视为一个并存储在表中。项目加载时,所有方法都将加载到表中,动态生成方法也将加载到表中。
不同类别可以有相同的方法,不同类别的实例对象执行相同的方法selector按照各自的方法列表,时间会去SEL找到自己类对应的IMP。
IMP本质上是一个函数指针,它包含一个接收信息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。所以我们可以通过SEL获得它对应的IMP,获得函数指针后,意味着我们获得了需要执行方法的代码入口,这样我们就可以像普通C语言函数调用一样使用函数指针。
小的总结:
但与C语言中的函数指针不同,该指针直接保存了该方法的地址,但SEL只是方法编号。**IMP:**保存方法地址的函数指针
每一个继承NSObject所有类别都可以自动获得runtime的支持。在这样的一个类中,有一个isa指针是指由编译器编译的数据结构(需要继承)NSObject)创建的.该结构包括指向其父类定义的指针和指针 Dispatch table. Dispatch table是一张SEL和IMP的对应表。也就是说,方法编号SEL最后还是要通过Dispatch table找到相应的表IMP,IMP是函数指针,然后执行这种方法
1.通过方法获取方法编号:SEL methodId=@selector(methodName);
或者SEL methodId = NSSelectorFromString(methodName);
2.通过方法编号执行该编号的方法: [self performSelector:methodId withObject:nil];
4.通过方法编号获得IMP IMP methodPoint = [self methodForSelector:methodId];
5.执行IMP void (*func)(id, SEL, id) = (void *)imp; func(self, methodName,param);
**注意分析:**如果方法没有传入参数时:void (*func)(id, SEL) = (void *)imp; func(self, methodName);
如果方法传入一个参数时:void (*func)(id, SEL,id) = (void *)imp; func(self, methodName,param);
如果方法传入俩个参数时:void (*func)(id, SEL,id,id) = (void *)imp; func(self, methodName,param1,param2);
objc_msgSend()的执行流程
- 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法
- 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段:负责动态地添加方法实现
- 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理
对OC Runtime已经有一定的了解,消息,Class的结构,selector、IMP、元类等等
objc_msgSend
此函数是消息发送必经之路,但只要一提 objc_msgSend
,都会说它的伪代码如下或类似的逻辑,反正就是获取 IMP 并调用:
id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}
objc_msgSend
在调用的时候有两个默认参数,第一个参数是消息的接收者,第二个参数是方法名。
这一点可以通过oc代码重写成cpp代码来证明。
int object_c_source_m() {
OSTestObject1 *obj1 = [[OSTestObject1 alloc] init];
[obj1 print];
return 0;
}
重写后:
int object_c_source_m() {
OSTestObject1 *obj1 = ((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OSTestObject1"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)obj1, sel_registerName("print"));
return 0;
}
可以看到print
的调用转化成了objc_msgSend
调用并传入 objc1
和 print
。如果方法本身有参数,会把本身的参数拼接到这两个参数后面。
用伪代码的原因就是objc_msgSend
是用汇编语言写的,针对不同架构有不同的实现。苹果为什么objc_msgSend这部分代码要使用汇编来编写呢?答案很简单–效率。汇编的效率是比c/c++更快的,因为汇编大多是直接对寄存器的读写,相比较对内存的操作更底层,效率也更高。另外苹果在所有的汇编方法命值钱都会用下划线开头,目的是为了防止符号冲突。
下方就是arm64结构下的源码:
ENTRY _objc_msgSend//进入消息转发
UNWIND _objc_msgSend, NoFrame
//p0寄存器,消息接收者
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)//b是跳转,le是小于等于,也就是p0小于等于0时,跳转到LNilOrTagged
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//缓存查找
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged://如果接收者为nil,跳转至此
b.eq LReturnZero // nil check如果消息接受者为空,直接退出这个函数
GetTaggedClass
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//结束
- 首先从
cmp p0,#0
开始,这里p0是寄存器,存放的是消息接受者。b.le LNilOrTagged
,b是跳转到的意思。le是如果p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged
,执行b.eq LReturnZero
直接退出这个函数 - 如果消息接受者不为nil,汇编继续跑,到
CacheLookup NORMAL
,CacheLookup
这个宏是在类的缓存中查找 selector 对应的 IMP(放到p10
)并执行。如果缓存没中,那就得到 Class 的方法表中查找了来看一下具体的实现
其实只需要看注释就能知道大概流程。 这部分其实是objc_msgSend
开始到找类对像cache
方法结束的流程。 首先判断receiver
是否存在,以及是否是taggedPointer
类型的指针,如果不是taggedPointer
类型,我们就取出对象的isa
指针(x13寄存器中),通过isa
指针找到类对象(x16寄存器),然后通过CacheLookup
,在类对象的cache
中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached
分支。
在cache中快速查找
下面就是CacheLookup
的源码:
//objc_msgSend开始找到类对象cache方法结束的流程中的 CacheLookup 方法的源码如下:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
// Restart protocol:
//
// As soon as we're past the LLookupStart\Function 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\Function,
// then our PC will be reset to LLookupRecover\Function 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
//
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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 p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
and p10, p11, #0x007ffffffffffffe // p10 = buckets
autdb x10, x16 // auth as early as possible
#endif
// x12 = (_cmd - first_shared_cache_sel)
adrp x9, _MagicSelRef@PAGE
ldr p9, [x9, _MagicSelRef@PAGEOFF]
sub p12, p1, p9
// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
// bits 63..60 of x11 are the number of bits in hash_mask
// bits 59..55 of x11 is hash_shift
lsr x17, x11, #55 // w17 = (hash_shift, ...)
lsr w9, w12, w17 // >>= shift
lsr x17, x11, #60 // w17 = mask_bits
mov x11, #0x7fff
lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
and x9, x9, x11 // &= mask
#else
// bits 63..53 of x11 is hash_mask
// bits 52..48 of x11 is hash_shift
lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
lsr w9, w12, w17 // >>= shift
and x9, x9, x11, LSR #53 // &= mask
#endif
ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
cmp x12, w17, uxtw
.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
SignAsImp x0
ret
.else
b.ne 5f // cache miss
sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
.if \Mode == NORMAL
br x17
.elseif \Mode == LOOKUP
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
add x16, x16, x9 // compute the fallback isa
b LLookupStart\Function // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES
.endmacro
大致看看注释,不用深究汇编代码逻辑,大概应该是通过类对象内存平移找到cache
,然后再获取buckets
,然后再查找方法。 注释解释:如果没有找到返回NULL,查找的时候x0存放方法接收者,x1存放方法名,x16存放isa指针。
如果没有找到,直接走_objc_msgSend_uncached
流程(走方法列表查询流程)
方法类表中查找
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
//下面两行代码是关键
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
从上方代码中我们发现执行了MethodTableLookup
方法进行方法列表查询,该方法如下:
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward//此处调用了loolUpImpOrForward方法
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
搜索lookUpImpOrForward
方法如下:
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//cache缓存中查找
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
//方法列表中查询
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
//找不到实现,方法解析器也没有帮助。
//使用转发。
imp = forward_imp;//由这一步进入消息转发
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
其中关键代码如下:
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//cache缓存中查找
imp = cache_getImp(curClass, se