趣谈iOS运行时之方法调用原理

导语

一个成熟的计算机语言必然有丰富的体系,复杂的容错机制,处理逻辑以及判断逻辑。但这些复杂的逻辑都是围绕一个主线丰富和展开的,所以在学习计算机语言的时候,先掌握核心,然后了解其原理,明白程序语言设计的实质和当时选择这种处理方式的原因是极其必要的,而且也是学习语言的捷径。

所以在学习的过程中,需要把握几个核心

先专注主线,后丰富周边;
先宏观了解,后微观精通;
多设身处地思考,理解代码设计的原因;
理解代码设计的原理和优化


OC中处理方法的业务逻辑和其他语言不同,OC语言是动态语言(动态绑定动态加载(dynamatic binding),动态类型)。其中动态加载就涉及到OC的运行时。在OC中,方法是动态实现的,调用方法实际就是在发送消息
试想一下,一个方法的实现必然包含三个部分:

1.执行方法的对象
2.方法名称
3.不确定的参数

SEL只是一个方法名称,IMP才是执行方法最终的函数。IMP 是一个函数指针,包含一个接收消息的对象 id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的参数,并返回一个 id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。


提问时间到了:

  • 动态和静态有什么区别?
  • 执行方法是怎么实现的?
  • OC的方法和C语言的函数原理一样么?

动态和静态有区别的;首先我们从最表层理解,一个方法的实现必然要包含执行者,方法名和不确定的参数和返回值。无论是静态或者动态方法都必须这三个必要元素(动态和静态的区别就在于在何时确定这些必要元素)。
方法的执行包含编译和运行两个过程。

  • 静态方法是在编译时已经确定了三个要素,且不能更改。若类型不对,就会直接发出警告。
  • 而OC的动态方法可以直接跳过编译,在运行时才开始添加函数调用,决定执行方法的三个要素。这就是动态方法(至于怎么执行,下面开始讲解)

这三个元素是如何确定的呢?首先我们看一段示例代码

1
2
Dog *aDog = [[Dog alloc]init];
[aDog run];

在执行方法时,是怎么确定的呢?
此时我们需要注意OC内部方法的实质:OC中,方法实现实质就是发送消息。
[aDog run];代码的实质就是[ objc_sendMsg],它会找到执行方法的三个要素,找到就按照规则执行。
发送消息是通过 objc_send(id, SEL, ...) 来实现的,它首先会在对象的类对象的 cachemethodlist 以及父类对象的 cache,methodlist 中依次查找 SEL 对应 的 IMP;
如果没有找到且实现了动态方法决议机制就会进行决议。

如果没有实现动态方法决议机制或决议失败且实现了消息转发机制就会进入消息转发流程,否则程序 crash。
也就是说如果同时提供了动态方法决议消息转发,那么动态方法决议先于消息转发,只有当动态方法决议依然无法正确决议 selector 的 实现,才会尝试进行消息转发。当然,实际过程不可能那么简单,在开发语言之初,肯定会完善各种复杂场景和做了很多优化,接下来我们一起研究下OC对方法执行和扩展和优化:

  • 第一步:先找方法
  • 第二步:动态方法决议
  • 第三部:消息转发
  • 最后: 报错

消息转发

通常,给一个对象发送它不能处理的消息会得到出错提示,然而,Objective-C运行时系统在抛出错误之前, 会给消息接收对象发送一条特别的消息 forwardInvocation 来通该对象,该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对 象来处理,而不抛出错误。

  • 1,首先去该类的方法cache中查找,如果找到了就返回它;
  • 2,如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将 它加入 cache 中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次 调用再次查找的开销。
  • 3,如果在该类的方法列表中没找到对应的IMP,在通过该类结构中的super_class指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的IMP,返回它,并加入cache中;
  • 4,如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则看是不是可以进行动态方法决议(后 面有专文讲述这个话题);
  • 5,如果动态方法决议没能解决问题,进入下面要讲的消息转发流程。便利函数:我们可以通过 NSObject 的一些方法获取运行时信息或动态执行一些消息:

class 返回对象的类:

1
2
3
4
5
isKindOfClass,isMemberOfClass 检查对象是否在指定的类继承体系中;
respondsToSelector 检查对象能否相应指定的消息;
conformsToProtocol 检查对象是否实现了指定协议类的方法;
methodForSelector 返回指定方法实现的地址;
performSelector:withObject 执行 SEL 所指代的方法

OC做为一门面向对象语言,自然具有面向对象的语言特性,如封装继承多态。他具有静态语言的特性(如C++),又有动态语言的效率(动态绑定、动态加载等)。整体来说,确实是一门不错的编程语言。


OC的动态语言特性

现在,让我来想想OC的动态语言特性。OC的动态特性表现为了三个方面:
动态类型动态绑定动态加载
之所以叫做动态,是因为必须到运行时(runtime)才会做一些事情。

(1)动态类型

动态类型,说简单点就是id类型。动态类型是跟静态类型相对的。像内置的明确的基本类型都属于静态类型(int、NSString等)。静态类型在编译的时候就能被识别出来。所以,若程序发生了类型不对应,编译器就会发出警告。而动态类型就编译器编译的时候是不能被识别的,要等到运行时(runtime),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。

(2)动态绑定

动态绑定(dynamic binding)貌似比较难记忆,但事实上很简单,只需记住关键词@selector/SEL即可。先来看看“函数”,对于其他一些静态语言,比如c++,一般在编译的时候就已经将将要调用的函数的函数签名都告诉编译器了。静态的,不能改变。而在OC中,其实是没有函数的概念的,我们叫“消息机制”,所谓的函数调用就是给对象发送一条消息。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法。最终形成的这个SEL变量就代表一个方法的引用。这里要注意一点:SEL并不是C里面的函数指针,虽然很像,但真心不是函数指针。SEL变量只是一个整数,他是该方法的ID。以前的函数调用,是根据函数名,也就是字符串去查找函数体。但现在,我们是根据一个ID整数来查找方法,整数的查找字自然要比字符串的查找快得多!所以,动态绑定的特定不仅方便,而且效率更高。

(3)动态加载

动态加载就是根据需求动态地加载资源。我对动态加载比较陌生,所以就没什么可总结的啦。等以后慢慢完善。

写在最后

技术学习绝不能孤胆英雄独闯天涯,而应在一群人的交流碰撞,享受智慧火花的狂欢。
希望我的文章能成为你的盛宴,也渴望你的建议能成为我的大餐。