OC底层-动态方法决议

679 阅读6分钟

上一篇文章介绍了慢速查找的流程,今天我们看下当寻找IMP找不到的时候,系统是如何进行处理的,经过了哪些操作呢?

unrecognized selector sent to instance报错底层原理

1、之前介绍消息查找流程的时候,我们详细介绍过这个函数lookUpImpOrForward,函数中主要是进行遍历查找IMP,今天我们再次来看下这个函数,首先看如下代码:

image.png

2、系统在进入函数后,首先创建了一个forward_imp,然后使用这个IMP是在遍历找不到IMP后,将forward_imp赋值给imp然后返回出去了。那么我们就先去看下_objc_msgForward_impcache这个函数的具体实现。

3、通过全局搜索,发现这个函数也是汇编实现的,然后我们进入汇编代码能够看到如下代码:

image.png

4、这个汇编特别简单就是直接跳转到了__objc_msgForward函数,那么我们就继续查找__objc_msgForward函数的实现,能够在刚才函数的下方看到这样的函数实现:

image.png

5、下面我们详细翻译下这几行汇编代码,翻译结果如下:

ENTRY __objc_msgForward

// 调用__objc_forward_handler函数,读取返回值然后存储到x17寄存器
adrp	x17, __objc_forward_handler@PAGE

// 从x17读取__objc_forward_handler的返回值,然后写入到p17寄存器中
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]

// TailCallFunctionPointer函数中传入x17寄存器中的值
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

6、然后我们继续看下TailCallFunctionPointer这个函数,通过搜索发下这个函数实现很简单就是直接跳转$0,具体代码如下:

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

7、那么我们就回头去看下__objc_forward_handler这个函数,因为是这个函数中返回的$0直接进行跳转了,我们通过全局搜索发现这个函数不是汇编实现了,所以转换成C/C++函数名继续搜索,然后发下如下函数实现:

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

8、这个函数是对objc_defaultForwardHandler的一个指向,那么我们继续看下objc_defaultForwardHandler这个函数:

image.png

571626427390_.pic.jpg

9、好熟悉的一串英文,我们先看下如下代码:

image.png

10、然后我们运行代码,看下最后运行结果:

image.png

11、和上面找到的好像,那么我们就能发现,当我们调用方法的时候,如果找不到,系统就是经过这个流程给我们抛出了异常信息,那么我们只能看到这里,然后什么也不做吗?不是的系统到这里之前就提供了补救的方法,也就是动态方法决议,下面我们一起探索下动态方法决议

动态方法决议

1、我们继续回到lookUpImpOrForward这个函数,在这个函数中,我们之前介绍的时候也大概描述过一些,那么今天就主要是看下以下代码:

image.png

2、在这块系统使用了一个类似单利的操作,保证操作只进行一次,然后我们看下resolveMethod_locked这个函数:

image.png

3、在这个函数中,系统首先判断了是否是元类,那么下面我们开始分析看下这两个分支的实现过程以及系统这么设计的原因。

对象方法动态决议

1、首先看下这个resolveInstanceMethod函数的实现:

image.png

2、系统在这个函数中,先获取了一个SEL然后去判断类是否实现这个SEL,一般来说我们没有手动实现,但是系统为了防止程序员开发过程中没有实现这个@selector(resolveInstanceMethod:)导致程序不稳定,自己默认实现了这个SEL,我们可以通过全局搜索resolveInstanceMethod:看到在NSObject.mm文件中有如下实现:

image.png

3、所以上面的检测@selector(resolveInstanceMethod:)是否实现不会走,然后我们可以看到系统在这里进行了一次消息发送。调用resolveInstanceMethod来提供一个容错的机会,让程序员在这个方法中进行一次补救,我们在KGTeacher类,实现这个方法,然后动态添加一个say666方法,看看是否补救成功,具体代码如下:

image.png

4、然后我们运行程序,看下输出结果:

image.png

5、我们可以看到补救成功了,然后我们继续通过断点,看下刚才系统进行消息发送的地方,结果如下:

image.png

6、我们可以看到,当我们实现了resolveInstanceMethod方法后,进行了动态添加重新指向IMP后,查找过程中可以找到IMP了,而且是我们动态添加的。

类方法动态决议

1、首先看下这个resolveClassMethod函数的实现:

image.png

2、这个函数的实现和对象方法的动态决议有点相似,我们先看下函数的具体代码逻辑,系统还是按照惯例进行了判断是否实现resolveClassMethod这个方法,我们在上面对象方法的动态决议中能够看到系统默认在NSObject.mm中实现了这个方法,所以我们可以忽略这个判断了,然后因为是类方法,所以进行了一系列的判断,来确定类是否创建完成,是否已经调用+initialize方法。

3、然后进行了消息发送,调用了resolveClassMethod方法,那么我们继续去KGTeacher类中实现这个方法,给一些补救操作,看看能否成功,下面看下具体代码:

image.png

4、然后我们运行程序,看下结果输出:

image.png

5、我们可以看到补救成功了,然后我们继续通过断点,看下刚才系统进行消息发送的地方,结果如下:

image.png

6、我们可以看到,当我们实现了resolveClassMethod方法后,进行了动态添加重新指向IMP后,查找过程中可以找到IMP了,而且是我们动态添加的。

7、然后我们能够发现,当执行完resolveClassMethod方法后,又经过处理,进行了一次resolveInstanceMethod函数的调用,这块这么处理的原因就在于苹果的isa走位,因为对于继承链来说,类方法存储在元类中,然后元类的继承链最后还是走向了NSObject,所以这块又走了一次resolveInstanceMethod

扩展

1、上面我们能够看到,无论是对象方法的动态决议还是类方法的动态决议,我们都是走了resolveInstanceMethod,那么我们是否只实现这一个方法,达到动态决议对象方法和类方法的效果呢?下面进行尝试,我们既然已经知道了类方法也走的原因在于isa走位,那么我们就直接到NSObject类中处理,所以我们先创建一个NSObject的分类KGObject类,然后在这个类中实现具体代码:

image.png

2、然后我们运行程序,看下最后输出结果:

image.png

3、可以看到效果是一抹抹一样样的。那么我们能否在这块做一些bug监控呢?有什么优缺点呢?

  • 优点:

    -- 能够提取公共类

    -- 无侵入方式添加监控

  • 缺点:

    -- 增加性能消耗

    -- 苹果的消息转发机制失效

总结

通过本片文章,我们大概了解了苹果对于方法调用的时候找不到方法提供的第一次补救机会,以及使用这种方式的优缺点。