oc - runtime运行机制

时间:2023-12-30 17:45:26

  Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时做的事放到了运行时来处理。同时OC也是一门简单的语言,很大一部分是C的内容,只是在语言层面上加了关键字和语法,真正让OC强大的是它的运行时,它很小却很强大,其中核心是消息分发。这种动态语言的优势在于:我们写代码时更加灵活,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现。

这种特性意味着OC不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于OC来说,这个运行时系统就像一个操作系统一样。这个运行时系统即Runtime,是用C和汇编写的,这个库使得C语言有了面向对象的能力。其中最主要的是消息机制。对于C语言,函数的调用在编译的会决定调用哪个函数,编译完成之后顺序执行,无任何二义性。OC的函数调用称为消息发送,属于动态调用过程。在编译的时候并不能决定真正的调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错。)只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

用一句话说:我们编写的OC代码,程序运行过程中,其实都是转化为runtime的C语言代码,runtime算是OC幕后工作者。runtime属于OC的底层,可以进行非常底层的操作(用OC是无法实现的。)

Runtime库主要做下面几件事:

1.封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上一些额外的特性。这些结构体和函数被Runtime函数封装后,我们就可以在程序运行时创建、检查、修改类、对象和他们的方法了。

2.找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),Runtime会根据消息接收者是否能响应该消息而做出不同的反应。

OC的Runtime目前有两个版本:Modern Runtime和Legacy runtime.  Modern Runtime主要用于64位应用。Legacy runtime主要用于32位应用。目前一般都是64位了。

关于执行效率问题,“静态语言执行效率要比动态语言高”应该是没问题的。因为一部分的CPU计算损耗在Runtime中。

那OC是怎么实现动态调用的呢?假如在OC中写了如下代码:

  1. [obj makeText];

其中obj是一个对象,makeText是一个函数名称。执行一个方法,有些语言,编译器会执行一些额外的优化和错误检查,因为调用关系很直接也很明显。但是对于消息分发来说,就不那么明显了,在发消息之前不必知道某个对象是否能够处理消息。你把消息发给他,他可能会处理,也可能转给其他的Object来处理。一个消息不必对应一个方法,一个对象可能实现一个方法来处理多条消息。

在编译时RunTime会将上述代码转化为:

  1. objc_msgSend(obj,@selector(makeText));

所以其实

  1. objc_msgSend(obj,@selector(makeText));
  2. <pre name="code" class="plain">[obj makeText];

是等价的。

再比如:
[obj setTT:@"111" isOK:YES];

objc_msgSend(obj,@selector(setTT:isOK:),@"111",YES)
也是等价的。注意有参数的OC函数名的表达方式。

首先我们来看看obj这个对象,iOS中的obj都继承与NSObject。

  1. @interface NSObject{
  2. Class isa OBJC_ISA_AVAILABILITY
  3. }

在NSObject中存在一个Class的isa指针。然后我们看看Class是什么东西:这是在objc.h中定义的。

  typedef struct objc_class *Class;

  1. struct objc_class{
  2. Class isa; //指向metaclass
  3. Class super_class ;//指向父类
  4. const char *name;//类名
  5. long version;  //类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改读取;
  6. long info;   //一些标识信息,如CLS_CLASS(0x1L)表示该类为普通的class,其中包含对象方法和成员变量;CLS_META(0x2L)表示该类为metaclass,其中包含类方法;
  7. long instance_size;  该类的实例变量大小(包括从父类继承下来的实例变量);
  8. struct objc_ivar_list *ivars;  //用于存储每个成员变量的地址;
  9. struct objc_method_list **methodLists;   //与info的一些标志位有关,如CLS_CLASS(0x1L),则存储对象方法;
  10. struct objc_cache *cache;  //指向最近使用的方法的指针,用于提升效率;
  11. struct objc_protocol_list *protocols;  //存储该类遵守的协议;
  12. }

对于一个Class类中,存在很多东西,我们来解释一下重要的东西:

Class isa :指向metaclass ,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对象方法(-开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(+开头的方法)。

Class super_class:指向父类,如果这个类是根类,则为NULL。

我们通过下面这个图来了解类和对象的继承关系:

oc - runtime运行机制

注意:所有metaclass中isa指针都指向根metaclass(也就是Root Class Meta),而根metaclass则指向自身。Root metaclass是通过继承Root Class产生的。与root class结构成员一致,也就是前面提到的结构。不同的是Root metaclass的isa指针指向自身。

然后再来看看方法:

@selector(makeText):这是一个SEL方法选择器。SEL的主要作用就是通过方法名字(makeText)查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个int类型的一个地址,地址中存放着方法的名字。对于一个类中,每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。同时,我们也应该知道,OC是没有方法重载的。

我们再来看看具体的消息发送之后是怎么样来动态查找对应的方法的。

首先,编译器将代码[obj makeText];转化为objc_msgSend(obj,@selector(makeText));在objc_msgSend函数中,首先通过obj的isa指针找到对应的class。在Class中先去cache中通过SEL查找对应函数method,若cache中未找到,再去methodList中查找,若methodList中未找到,则取superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

class的方法列表其实是一个字典,key为selector,value为IMP函数指针。一个IMP是指向方法在内存中的实现。很重要的一点,selector和IMP之间的关系是在运行时才决定的,而不是编译时。

对于面向对象而言,万物皆对象,在OC中,类也是对象。The class is Object,也可以处理消息。所以你现在知道为什么会有类方法和实例方法了。

Method Swizzling

我们上面讲过,方法由两个部分组成。selector相当于一个方法的id,IMP是方法的实现。这样分开的一个遍历就是selector和IMP之间的对应关系可以被改变。比如一个IMP可以有多个selectors指向它。

而Method Swizzling可以交换两个方法的实现。在OC中,两种扩展class的途径。首先是subclass.你可以重写某个方法,调用父类的实现,这也意味着你必须使用这个subclass的实例。但是我们如果使用Category分类,重写某个方法之后,就不能再调用原来的方法了。

Method Swizzling可以搞定这个问题,你可以重写某个方法而不用继承,同时还可以调用原先的实现。通常的做法是在Category中添加一个方法,可以通过method_exchangeImplementations这个运行时方法来交换实现。