iOS开发——方法动态决议和消息转发机制

1. 动态方法决议

经过探索objc_msgSend源码,当慢速查找依然没有找到IMP时,会进入方法动态解析阶段,源码以下:面试

在通过_class_resolveMethod方法后,在进行一次retry,从新进行一遍方法的查找流程,而只有一次动态方法解析的机会就是在_class_resolveMethod方法中。缓存

_class_resolveMethod源码以下:bash

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    // 是不是元类
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst); // 已经处理
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // 对象方法 决议
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
复制代码
复制代码

由于类方法是存储在元类中,因此在_class_resolveMethod中的处理有所不一样学习

元  类:说明是对元类中的类方法进行处理,可是元类中的方法是在根元类中以实例方法的形式存储的,因此
最终会查找根元类的实例方法,调用实例方法解析查找。

非元类:对储存在类中的实例方法进行处理。
复制代码
复制代码

做为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:413038000,无论你是小白仍是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!ui

1.1 实例方法动态解析 _class_resolveInstanceMethod

_class_resolveInstanceMethod方法中对实例方法动态解析,源码以下:spa

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // 1\. 判断系统是否实现SEL_resolveInstanceMethod方法
    // 即+(BOOL)resolveInstanceMethod:(SEL)sel, 
    // 继承自NSObject的类,默认实现,返回NO
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        // 不是NSObject的子类,也未实现+(BOOL)resolveInstanceMethod:(SEL)sel,
        // 直接返回,没有动态解析的必要
        return;
    }

    // 2\. 系统给你一次机会 - 你要不要针对 sel 来操做一下下
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // 3\. 再次寻找IMP
    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)); } } } 复制代码 复制代码

由此:咱们能够在+(BOOL)resolveInstanceMethod:(SEL)sel方法中对未实现的方法指定已实现方法的IMP,并添加到类中,实现方法动态解析,防止系统崩溃。3d

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    NSLog(@"来了老弟:%s - %@",__func__,NSStringFromSelector(sel));

    if (sel == @selector(saySomething)) {
        NSLog(@"说话了");
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        const char *sayHType = method_getTypeEncoding(sayHMethod);
        return class_addMethod(self, sel, sayHIMP, sayHType);
        }

    return [super resolveInstanceMethod:sel];
}
复制代码
复制代码

咱们也能够在此方法中根据方法的前缀、路由、事务,跳转的不一样的页面,进行bug收集。调试

1.2 _class_resolveClassMethod

若是是元类,在_class_resolveClassMethod方法中对相关类方法的进行动态解析,该方法的实现步骤和实例方法的实现步骤相似,区别是消息发送的时候获取的是元类,即:日志

_class_resolveClassMethod源码以下:code

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
    // 1\. 判断系统是否实现SEL_resolveClassMethod方法
    // 即+(BOOL)resolveClassMethod:(SEL)sel, 
    // 继承自NSObject的类,默认实现,返回NO 
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //2\. 系统给你一次机会 
    //   经过objc_msgSend调用一下+(BOOL)resolveClassMethod:(SEL)sel方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    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 //3\. 再次查找IMP 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));
        }
    }
}

复制代码
复制代码

所以,当咱们要进行类方法的动态解析时,须要添加+ (BOOL)resolveClassMethod:(SEL)sel进行动态方法解析:

+ (BOOL)resolveClassMethod:(SEL)sel{

    NSLog(@"来了类方法:%s - %@",__func__,NSStringFromSelector(sel));

     if (sel == @selector(sayLove)) {
         NSLog(@"说- 说你你爱我");
         IMP sayHIMP = class_getMethodImplementation(self, @selector(sayObjc));
         Method sayHMethod = class_getClassMethod(self, @selector(sayObjc));
         const char *sayHType = method_getTypeEncoding(sayHMethod);
         // 类方法在元类 objc_getMetaClass("LGStudent")
         return class_addMethod(self, sel, sayHIMP, sayHType);
     }
     return [super resolveClassMethod:sel];
}
复制代码
复制代码

小结

  1. 方法先通过缓存查找,方法列表查找后,后进入动态方法解析阶段
  2. 实例方法解析须要实现resolveInstanceMethod方法
  3. 类方法解析须要实现resolveClassMethod方法
  4. 类方法存储在元类之中,因为元类继承自根元类根元类最终继承自NSObject,所以对类方法解析的时候,最终会查找到NSObject。因为元类和根源类由系统建立,没法修改,因此能够再根元类的父类NSObject中,添加对应的实例方法resolveInstanceMethod进行动态解析。

2. 消息转发

在方法查找过程当中,通过缓存查找,方法列表查找和动态方法解析,若是以上步骤都没有查找到IMP,也没有进行方法动态解析,那么就会进入最后一步,崩溃。

_objc_msgForward_impcache是汇编方法,以下:

STATIC_ENTRY __objc_msgForward_impcache
	// Method cache version

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band Z is 0 (EQ) for normal, 1 (NE) for stret

	beq	__objc_msgForward
	b	__objc_msgForward_stret

	END_ENTRY __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_impcache中,调用__objc_msgForward,而后调用__objc_forward_handler,转掉_objc_forward_handler OC方法以下,而后就是经典崩溃。

那么,在崩溃时,为何会打印如上图的一系列堆栈信息呢 ?

经过查看lookUpImpOrForward源码,如上图,当查找到IMP时,会调用log_and_fill_cache方法,进行缓存填充和日志存储。

log_and_fill_cache如上图,经过控制objcMsgLogEnabled来控制日志存储,日志会记录在/tmp/msgSends目录下,而objcMsgLogEnabled的赋值是在instrumentObjcMessageSends之中,能够暴露这个方法,来达到外部打日志的操做。

在查看/tmp/msgSends目录下的文件,如图:

发现调用resolveInstanceMethod:, forwardingTargetForSelectormethodSignatureForSelectordoesNotRecognizeSelector一系列方法,进行消息转发。

2.1 快速转发流程

经过查看forwardingTargetForSelector的官方文档,

- (id)forwardingTargetForSelector:(SEL)aSelector方法,

1\. 返回一个对象。若是这个对象非空、非nil,系统会将消息转发给这个对象执行,不然,继续查找其余流程。
系统给了将这个SEL转给其余对象的机会。
2\. 若是返回nil,或者没有处理消息转发,会走到forwardInvocation:方法进行处理,进入慢速消息转发流程。
复制代码
复制代码

能够经过一下代码,将saySomething方法的消息转发到LGTeacher类中实现,而不会引发系统崩溃,至此消息快速转发结束。

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
复制代码
复制代码

2.2 慢速转发流程

进入慢速查找流程,首先必须先实现methodSignatureForSelector方法,返回一个签名,这个方法签名里面封装了返回值类型,参数类型等信息。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
复制代码
复制代码

而后还必须实现- (void)forwardInvocation:(NSInvocation *)anInvocation;

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];

    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}
复制代码
复制代码

注意:

1\. forwardInvocation 方法和 methodSignatureForSelector 方法必须同时实现
2\. methodSignatureForSelector 会生成一个签名,NSInvocation对象,将NSInvocation对象做为
参数传给 forwardInvocation 方法的
3\. 在forwardInvocation方法里面将消息给能处理该消息的对象,以免对象调用
didNotRecognizeSelector 方法致使崩溃
4\. forwardInvocation 这个方法相似于将消息当作事务堆放起来,在这里谁能够操做就在这里面操做,
就算不操做也不会崩溃,这里也是防崩溃的最后处理机会。
复制代码
复制代码

接下来看一下系统NSObjectforwardInvocation的实现:

+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

复制代码
复制代码

因而可知,系统最后是在doesNotRecognizeSelector方法中抛出异常的,因此重写forwardInvocation方法后,无论里面有么有实现,或者执行父类的方法,程序都是不会崩溃的。

消息转发流程图:

总结

当调用了未实现的方法,三个解决途径:

一、resolveInstanceMethod:为发送消息的对象的添加一个IMP,而后再让该对象去处理
二、forwardingTargetForSelector:将该消息转发给能处理该消息的对象
三、methodSignatureForSelector和 forwardInvocation:第一个方法生成方法签名,而后建立 
NSInvocation 对象做为参数给第二个方法,而后在forwardInvocation  方法里面作消息处理,
只要在第二个方法里面不执行父类的方法,即便不处理也不会崩溃
复制代码
复制代码

关于resolveInstanceMethod 方法调用两次的问题?

  1. 当查找IMP时,没有找到时,进入方法动态解析时,会第一次调用resolveInstanceMethod
  2. 而后进入消息转发流程,调用forwardingTargetForSelector,将该消息转发给能处理该消息的对象
  3. 调用methodSignatureForSelectorforwardInvocation,返回签名
  4. 调用forwardInvocation

在断点调试时,经过汇编,发现第二次resolveInstanceMethod调用在第三步和第四步之间,猜想,当签名处理完毕时,会匹配返回的签名和原始调用方法的签名,那么怎么找到原始调用方法的签名呢?从新发送一次消息。调用class_getInstanceMethod,从新查找一次方法,再一次发送resolveInstanceMethod[图片上传中...(image-724fcb-1588230283775-0)]

做者:亮亮不想说话

相关文章
相关标签/搜索