iOS底层探索之Runtime(四): 动态方法解析

442 阅读7分钟

1. 回顾

iOS底层探索之Runtime(一):运行时&方法的本质

iOS底层探索之Runtime(二): objc_msgSend&汇编快速查找分析

iOS底层探索之Runtime(三): lookUpImpOrForward慢速查找分析

在上一篇博文中,介绍了Runtime的慢速查找流程lookUpImpOrForward,本章内容主要分析动态方法解析流程。

在缓存中、自己的class_rw_t中、父类的cache中、父类的class_rw_t中都没有找到imp,就会进入objc_msgSend的第二个阶段动态方法解析

2. 动态方法解析

2.1 resolveMethod_locked

从源码中可以发现,在lookUpImpOrForward里面循环遍历没有找到imp之后就会进入下面这个判断,注释也写的很明显了。

// No implementation found. Try method resolver once.
//behavior = 3 , LOOKUP_RESOLVER = 2
// 3 & 2 = 2,就是找两个的相同值
//0011
//0010
//0010 -> 2 
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;//异或:相同为0,不同为1
        // 3 ^= 2 -> 1 ,behavior=1,下次再进来就是behavior & LOOKUP_RESOLVER -> 相当于1 & 2=0,为0这个方法也就只执行一次,相当于单例的作用
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
  • resolveMethod_locked
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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

resolveMethod_locked方法里面可以看出,主要是判断是不是元类,非元类和元类两种情况,分情况进行处理。

  • 非元类:调用resolveInstanceMethod:
  • 元类:调用resolveClassMethod:

在正式开始分析之前,我们先看看下面这个例子 在JPStudent的父类JPPerson类里面,声明了一个方法jp_sayHello,但是没有实现,那么JPStudent子类调用了父类的方法,会出现什么情况呢?

JPPerson *jp = [[JPPerson alloc]init];
		 [jp jp_sayHello]

很显然程序是会奔溃的,奔溃信息如下:

[9441:373947] -[JPStudent jp_sayHello]: unrecognized selector sent to instance 0x100709660
[9441:373947] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JPStudent jp_sayHello]: unrecognized selector sent to instance 0x100709660'

那么为什么会报这个unrecognized selector sent to instance经典的错误呢?我们从源码来分析 在lookUpImpOrForward方法里面,如果所有的父类里面没有找到imp,就会给imp赋值forward_impbreak跳出,最后返回imp


 const IMP forward_imp = (IMP)_objc_msgForward_impcache;
  IMP imp = nil;
  
 .....代码省略......
 
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
            
 .....代码省略......
 
    return imp;

2.2 _objc_msgForward_impcache

  • _objc_msgForward_impcache

那么赋值的这个imp,也就是_objc_msgForward_impcache,到底是个什么imp呢? 在源码里面全局搜索objc_msgForward_impcache

objc_msgForward_impcache

发现objc_msgForward_impcache只有一行代码,然后再继续搜索__objc_msgForward

__objc_msgForward

__objc_msgForward里面又调用了TailCallFunctionPointer,然后再搜索TailCallFunctionPointer

.macro TailCallFunctionPointer
	// $0 = function pointer value
	braaz	$0
.endmacro

意思是要跳转$0,那么得先看x17x17就是__objc_forward_handler

__objc_forward_handler

那么再全局搜索__objc_forward_handler,发现没有找到,那么再去掉下划线搜索

__objc_forward_handler

我的天哪!原来报错信息是在这里打印的啊!这一波操作666啊!

666

 _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);

2.3 知识点补充

OC底层是不分实例方法(-)和对象方法(+)的,底层会自动给你加上-号和+号,然后按格式打印报错信息。还记得isa走位图吗?iOS底层探索之类的结构(上):ISA isa走位 从图中,可以知道,根元类的superClass指向了根类,我相信大家都有疑问?

  • 类方法,存在元类里面,实例方法存在类里面。
  • 当调用类方法,会沿着继承链,往上找,一直找到元类都没有找到类方法,就回会去根类里面找,根类里面发现有个同名的实例方法,就会直接调用。
  • 如果根类里面没有同名的实例方法就会报错unrecognized selector sent to instance

这就解释了OC其实是不分+方法和-方法的,对于底层来说,都是消息发送,sel都一样。所以会有根元类的superClass指向了根类这么一个指针的指向。

那么如果方法没有实现,就只能程序崩溃,报错了吗?有没有什么补救措施,好让我拯救地球呢!

有的靓仔,苹果工程师会满足你的!请继续往下看! 我要拯救地球了,😁

2.4 resolveInstanceMethod

JPPerson类的.m文件下,实现resolveInstanceMethod方法

resolveInstanceMethod

程序运行起来报错了,但是我们发现,在报错奔溃之前 走了resolveInstanceMethod方法,那么也就是说,我们可以提前处理,不让程序奔溃。


@implementation JPPerson

- (void)jp_sayNB {
	NSLog(@"%@,%s",self,__func__);
}

+(BOOL)resolveInstanceMethod:(SEL)sel {
	// 方法匹配
	NSString *methodName = NSStringFromSelector(sel);

	if ([methodName isEqualToString:@"jp_sayHello"]) {

		IMP sayNBImp = class_getMethodImplementation(self, @selector(jp_sayNB));
		Method method = class_getClassMethod(self, @selector(jp_sayNB));
		const char *type = method_getTypeEncoding(method);
		return class_addMethod(self, sel, sayNBImp, type);//添加方法
	}
	return [super resolveInstanceMethod:sel];
}

程序跑起来,看看结果如何

添加方法

程序并没有奔溃,而是走了我们添加的方法里面去,这就是对象方法的动态解析。

2.5 resolveClassMethod

那么我们再来看看,类方法的动态解析。

resolveClassMethod

从图中可以看出,在调用了resolveClassMethod方法,也调用了resolveInstanceMethod方法,这是怎么回事呢?

resolveClassMethod

iOS底层探索之Runtime(四): 动态方法解析从底层代码来看,处理流程差不多,那么我们现在去实现类方法的动态解析。

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

class_getClassMethod方法,是获取元类的实例方法。在类里面实现resolveClassMethod,相当于元类里面以对象方式存在

JPPerson类调用没有实现方法[JPPerson jp_sayHappy],在JPPerson中实现resolveClassMethod方法解析动态解析

+ (void)jp_say666 {
	NSLog(@"%@,%s",self,__func__);
}

// 相当于 元类中的对象方法
+(BOOL)resolveClassMethod:(SEL)sel {
	//获取元类的对象方法
	NSString *methodName = NSStringFromSelector(sel);

	if ([methodName isEqualToString:@"jp_sayHappy"]) {

		IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("JPPerson"), @selector(jp_say666));
		Method method = class_getClassMethod(objc_getMetaClass("JPPerson"), @selector(jp_say666));
		const char *type = method_getTypeEncoding(method);
		return class_addMethod(objc_getMetaClass("JPPerson"), sel, sayNBImp, type);//添加方法
	}
	return [super resolveClassMethod:sel];
}

resolveClassMethod运行结果

动态方法解析,么得问题,jp_sayHappy方法并没有实现,但是在动态方法解析的时候,可以添加jp_say666方法来防止崩溃。上面提到了,resolveInstanceMethod方法还走了一次,这是因为类方法,存在元类里面,但是同时也可以以对象方法的形式存在,就有两条路径,上面isa的走位图,也验证了这个说法,其实也可以解释底层是不分+/-方法的。

但是又暴露出了一个问题,就是每个类都得写一次resolveInstanceMethodresolveClassMethod,那要是有多个类呢?都写一遍就太麻烦了。那么给NSObject写一个分类NSObject+JPResolver就可以很好的解决这种问题。

+(BOOL)resolveInstanceMethod:(SEL)sel {
		// 方法匹配
	NSString *methodName = NSStringFromSelector(sel);

	if ([methodName isEqualToString:@"jp_sayHello"]) {

		IMP sayNBImp = class_getMethodImplementation(self, @selector(jp_sayNB));
		Method method = class_getClassMethod(self, @selector(jp_sayNB));
		const char *type = method_getTypeEncoding(method);
		return class_addMethod(self, sel, sayNBImp, type);//添加方法
		
	}else if ([methodName isEqualToString:@"jp_sayHappy"]) {

		IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("JPPerson"), @selector(jp_say666));
		Method method = class_getClassMethod(objc_getMetaClass("JPPerson"), @selector(jp_say666));
		const char *type = method_getTypeEncoding(method);
		return class_addMethod(objc_getMetaClass("JPPerson"), sel, sayNBImp, type);//添加方法
	}
	return NO;
}

那要是上面两种情况,程序员都不处理呢? 苹果:给你机会,你得珍惜啊!你不处理,我也不能崩溃啊!那就再来一次吧!

那么再来一次是什么机会呢?请听下回分解

iOS底层探索之Runtime(五): 消息转发

3.总结

  • lookUpImpOrForward慢速查找imp没有找到会进入动态方法解析流程
  • resolveInstanceMethod实例方法解析,可以动态添加方法,防止崩溃。
  • resolveClassMethod类方法解析,也会调用resolveInstanceMethod,根据isa走位图可以知道,类方法是存在元类里面,但是也会以对象方法形式存在在类中,主要是因为底层不区分+-

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹