消息传递机制的学习
之前有写过这个机制的学习,第十一条 effectiveOC2.0阅读笔记(二对象\消息\运行期)
在对象上调用方法,术语就叫做传递消息,消息有名称和选择器(方法),可以接受参数,还可能有返回值。
在很多语言,比如 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);
我们需要注意,@selector等同于是把方法名翻译成SEL方法名,其仅仅关心方法名和参数个数,并不关心返回值与参数类型
生成SEL的过程是固定的,因为它只是一个表明方法的ID,不管是在哪个类写这个dayin方法,SEL值都是固定一个
在Runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。
那么不同的类可以拥有相同的方法,不同类的实例对象执行相同的selector时会在各自的方法列表中去根据SEL去寻找自己类对应的IMP。
IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。
objc_msgSend()的执行流程
- 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法
- 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现
- 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理
消息发送阶段
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
🐴 cmp p0, #0
#if SUPPORT_TAGGED_POINTERS
🐴 b.le LNilOrTagged
#else
b.eq LReturnZero
#endif
ldr p13, [x0]
GetClassFromIsa_p16 p13
LGetIsaDone:
🐴 CacheLookup NORMAL
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged://如果接收者为nil,跳转至此
🐴 b.eq LReturnZero 如果消息接受者为空,直接退出这个函数
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
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
#endif
LReturnZero:
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 ,来看一下具体的实现
.macro CacheLookup
ldp p10, p11, [x16, #CACHE]
#if !__LP64__
and w11, w11, 0xffff
#endif
and w12, w1, w11
add p12, p10, p12, LSL #(1+PTRSHIFT)
ldp p17, p9, [x12]
1: cmp p9, p1
b.ne 2f
CacheHit $0
2:
CheckMiss $0
cmp p12, p10
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]!
b 1b
3:
add p12, p12, w11, UXTW #(1+PTRSHIFT)
ldp p17, p9, [x12]
1: cmp p9, p1
b.ne 2f
🐴 CacheHit $0
2:
🐴 CheckMiss $0
cmp p12, p10
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]!
b 1b
3:
JumpMiss $0
.endmacro
在缓存中找到了方法那就直接调用,下面看一下从缓存中没有找到方法怎么办 没有找到会执行CheckMiss,我们对其进行查看
.macro CheckMiss
.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
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
那我们就来看一下__objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
🐴 MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
通过MethodTableLookup 这个字面名称我们就大概知道这是从方法列表中去查找方法。我们再查看一下它的结构
.macro MethodTableLookup
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
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)]
mov x2, x16
🐴 bl __class_lookupMethodAndLoadCache3
mov x17, x0
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
因为汇编的函数比C++的多一个下划线 看一下_class_lookupMethodAndLoadCache3 的实现
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES, NO, YES);
}
主要就是实现了lookUpImpOrForward()这个方法,然后我们再查找一下这个方法
学下面这个代码的时候我们需要明白什么是方法缓存
苹果认为如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率
继续开干
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
🐴 if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
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;
}
}
🐴
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
🐴 imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
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;
goto done;
}
}
}
。。。。。。。。。
省略部分涉及到动态方法解析和消息转发
怎么从类对象中查找方法,主要是在getMethodNoSuper_nolock() 这个方法中
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
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;
}
动态解析阶段
动态解析阶段流程
- 在自己类对象的缓存和方法列表中都没有找到方法,并且在父类的类对象的缓存和方法列表中都没有找到方法时,这时候就会启动动态方法解析
- 省略部分的动态解析我们再看一下
if (resolver && !triedResolver) {
runtimeLock.unlock();
🐴 _class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
大体流程我们知道了 来看一下_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);
if (!lookUpImpOrNil(cls, sel, inst,
NO, YES, NO))
{
🐴 _class_resolveInstanceMethod(cls, sel, inst);
}
}
}
其实实现很简单,就是判断是类对象还是元类对象,如果是类对象则说明调用的实例方法,则调用类的resolveInstanceMethod: 方法, 如果是元类对象,则说明是调用的类方法,则调用类的resolveClassMethod: 方法。
resolveClassMethod: 默认返回值是NO,如果你想在这个函数里添加方法实现,你需要借助class_addMethod
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
@cls : 给哪个类对象添加方法
@name : SEL类型,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types : 就是表示返回值和参数类型的字符串
动态解析测试
实现一个类,类在.h文件中声明一个方法,但在.m文件中并没有实现这个方法
我们在外部调用这个方法就会导致程序崩溃
其实很容易理解是为啥
- 第一步查找方法中,在自己的类对象以及父类的类对象中都没有找到这个方法的实现
- 所以转向动态方法解析,动态方法解析我们什么也没做,
- 所以进行第三步,转向消息转发,消息转发我们也什么都没做,最后产生崩溃
- 动态方法解析
- 当第一步中方法查找失败时会进行的,当调用的是对象方法时,动态方法解析是在resolveInstanceMethod方法中实现的
- 当调用的是类方法时,动态方法解析是在resolveClassMethod中实现的
- 利用动态方法解析和runtime,我们可以给一个没有实现的方法添加方法实现。
#import "Person.h"
#import <objc/message.h>
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
Method method = class_getInstanceMethod(self, @selector(test2));
class_addMethod(self, sel, method_getImplementation(method), "123");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)test2 {
NSLog(@"动态方法解析");
}
@end
消息转发阶段
消息转发流程
if (resolver && !triedResolver) {
runtimeLock.unlock();
🐴 _class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
🐴 triedResolver = YES;
🐴 goto retry;
}
🐴
🐴 imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
🐴 runtimeLock.unlock();
所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。
看一下进行消息转发的函数__objc_msgForward_impcache的具体实现
STATIC_ENTRY __objc_msgForward_impcache
jne __objc_msgForward_stret
jmp __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
movq __objc_forward_handler(%rip), %r11
jmp *%r11
END_ENTRY __objc_msgForward
可惜__objc_msgForward_handler并没有开源 看个这个函数实现的伪代码拿来学习一下
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);
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);
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
🐴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);
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);
}
🐴
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);
}
kill(getpid(), 9);
}
消息转发测试
如果没有在发消息阶段找到该方法的实现 动态方法解析和消息转发阶段也是什么都没有做,那么就会崩溃。
第一阶段消息发送结束后会进行第二阶段动态消息解析,没有就会进入第三阶段-消息转发。
消息转发
- 首先依赖于
- (id)forwardingTargetForSelector:(SEL)aSelector 这个方法,若是这个方法直接返回一个消息转发对象,则直接通过objc_msgSend()把这个消息转发给消息转发对象。 - 若是没实现或为nil,则会执行
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 这个函数以及-(void)forwardInvocation:(NSInvocation *)anInvocation 这个函数
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(testAge:)){
return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
}
return [super methodSignatureForSelector:aSelector];
}
"v@:I"中对应关系为 v : void @ : eat : : sel I : NSInteger 为了协助运行时系统,编译器用字符串为每个方法的返回值和参数类型和方法选择器编码。使用的编码方案在其他情况下也很有用,所以它是public 的,可用于@encode() 编译器指令。当给定一个类型参数,返回一个编码类型字符串。类型可以是一个基本类型如int,指针,结构或联合标记,或任何类型的类名,事实上,都可以作为C sizeof() 运算符的参数。这个机制也是为了提高Runtime的效率.
编码翻译表:
这里为什么只需要返回 返回值类型和参数类型?
- person方法调用
- (void)testAge:(int)age 这个过程,我们就需要知道方法调用者,方法名,方法参数。 - 而在Person.m中我们肯定知道方法调用者是person对象,方法名也知道是"testAge:",那么现在不知道的就是方法参数了
- 那么这个方法签名就是表示这个方法参数的,包括返回值和参数,这样方法调用者,方法名和方法参数就都知道了。
再看最后一个函数- (void)forwardInvocation:(NSInvocation *)anInvocation 的实现
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%@ %@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
int age;
[anInvocation getArgument:&age atIndex:2];
NSLog(@"%d", age);
[anInvocation invokeWithTarget:[[Student alloc] init]];
}
在这个方法中有一个NSInvocation类型的anInvocation参数,我们可以通过这个参数获取person对象调用- (void)testAge:(int)age方法这个过程中的方法调用者,方法名,方法参数。 然后我们可以通过修改方法调用者来达到消息转发的效果,这里是把方法调用者修改为了student对象。这样就完成了转发消息给student对象。
总结
我们要先明白OC方法调用的本质就是消息发送 消息发送是SEL-IMP的查找过程
我们先进行正常的消息发送 在一个函数找不到时,OC提供了三种方式去补救
- 调用resolveInstanceMethod或者resolveClassMethod给机会给一个没有实现的方法添加方法函数
- 调用forwardingTargetForSelector让别的对象去执行这个函数
- forwardInvocation灵活的将目标函数以其他形式执行(比如触发消息前,先以某种方式改变消息内容,比如追加另一个参数、或者改变选择子等等)
如果失效,抛出异常
再以一个流程图来体会一下这其中消息传递和消息转发的过程
同样 消息转发也涉及到了一个知识点-------多继承 多继承可以允许子类从多个父类派生,而OC并不支持多继承,不过我们可以通过协议、分类、消息转发来间接实现 iOS多继承的实现及区别 这个作者用demo很好的示范了这三种情况来实现多继承 嫖一张总截图
|