Runtime系列1之-类与对象的本质

简介

  1. Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。
  2. 这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。这个运行时系统即Objc RuntimeObjc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。
  3. 对于C语言,函数的调用在编译的时候会决定调用哪个函数,如果调用未实现的函数就会报错。
  4. 对于OC语言,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用.
  5. 编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)
  6. 比如我们创建了一个对象 [[NSObject alloc]init],最终被转换为几万行代码,截取最关键的一句可以看到底层是通过runtime创建的对象.

     ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    
    1. 删除掉一些强制转换语句如下:

       objc_msgSend(objc_getClass("NSObject"),sel_registerName("alloc"), sel_registerName("init"));
      
    2. 可以看出:调用方法本质就是发消息,[[NSObject alloc]init]语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。利用这个功能(把OC代码转为运行时代码)我们可以探究底层,比如block的实现原理。
    3. 注意: 使用objc_msgSend()、sel_registerName()方法需要导入头文件<objc/message.h>,若不使用直接导入<objc/runtime.h>就即可
  7. Runtime的作用
    1. 封装:既然C语言中没有对象/类这些概念,那么Runtime中OC中的对象/类对应的是什么呢?
      1. 在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
    2. 翻译代码:将OC的代码翻译成runtime代码

类与对象

从上面Runtime的作用分析来看,我们首先看看,类与对象在runtime中到底是什么

  1. 注意:这里所说的类,都是类对象
  2. 在OC代码中我们表示一个类用Class这个关键字修饰,如下:

     Class class = [NSObject class];
    
    1. Xcode中点击进入Class这个关键字查看,如下:

       typedef struct objc_class *Class;
      
  3. 从这里(此时的文件是objc.h)可以看出,Class关键字,也就是所谓的类,其实就是一个struct objc_class类型的结构体指针.那么objc_class结构体的定义是什么呢?
  4. 那么我们就只能到runtime.h中查看了,如何找到runtime.h文件呢?
    1. 方式1:在Xcode中导入头文件#import <objc/runtime.h>然后点击进入查看
    2. 方式2: Finder->应用程序->Xcode->显示包内容->Developer->paltforms->iPhone OS->Developer->SDKs->usr(system是苹果的所有框架)->include->objc这里面包含了很多最基层的头文件,objc/runtime/message/NSObject等
  5. 我们打开runtime.h,找到objc_class这个结构体定义如下:

     struct objc_class {
        
     // 指向metaclass
         Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
        
     #if !__OBJC2__
     // 指向其父类
         Class _Nullable super_class                              OBJC2_UNAVAILABLE;
         //类名
         const char * _Nonnull name                               OBJC2_UNAVAILABLE;
         // 类的版本信息,初始化默认为0
         long version                                             OBJC2_UNAVAILABLE;
         // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
         long info                                                OBJC2_UNAVAILABLE;
          // 该类的实例变量大小(包括从父类继承下来的实例变量);
         long instance_size                                       OBJC2_UNAVAILABLE;
         // 用于存储每个成员变量的地址
         struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
          // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
         struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
          // 指向最近使用的方法的指针,用于提升效率;
         struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
         // 存储该类遵守的协议
         struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
     #endif
        
     } OBJC2_UNAVAILABLE;
     /* Use `Class` instead of `struct objc_class *` */
    
  6. 下面我们分别解析一下这个结构体的成员变量:
    1. isa: 在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)
    2. super_class: 指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
    3. version: 我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
    4. info:一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
    5. cache: 用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
    6. 针对cache,我们用下面例子来说明其执行过程:

       NSArray *array = [[NSArray alloc] init];
       其流程是:
       1. [NSArray alloc]先被执行。因为NSArray没有`+alloc`方法,于是去父类NSObject去查找。
       2. 检测NSObject是否响应`+alloc`方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把`isa`指针指向NSArray类。同时,`+alloc`也被加进cache列表里面。
       3. 接着,执行`-init`方法,如果NSArray响应该方法,则直接将其加入`cache`;如果不响应,则去父类查找。
       4. 在后期的操作中,如果再以`[[NSArray alloc] init]`这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。
      

元类(Meta Class)

  1. 我们知道对象类的关系是:对象内部有一个isa指针指向其类,每个类内部有个一superclass指针指向其父类,然后一直指向NSObject这个基类
  2. 一个对象调用一个方法,首先通过isa指针到其类的方法列表中查找,如果找不到,那么通过superclass到其父类的方法中查找就这样一直往上进行,直到NSObject这个类,如果没有那么就是找不到方法了
  3. 那么问题就来了,如果是一个类调用他的类方法呢?类也有isa指针?即使有那么他的isa指向了谁呢?
    1. 类本身也是一个对象,又叫类对象
    2. 从获得类对象的方法分析Class class = [NSObject class];,类对象就是一个struct objc_class类型的结构体指针
    3. 它内部有一个isa指针,这个指针就指向了元类(meta-class)
    4. 即:元类就是类对象的类
    5. 元类中存储的是类方法列表
    6. 当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法,此时这个objc_class结构体中的info对应的类型为:CLS_CLASS (0x1L);而向一个类发送消息时,会在这个类的meta-class的方法列表中查找,此时这个objc_class结构体中的info对应的类型为:CLS_META (0x2L);
    7. meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
    8. 元类(meta-class)也是一个类对象,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。NSObject的meta-class的类地址为0x0;
    9. 每个类对象都有对应的元类,每个类(根类除外)都有一个superclass,同样每个元类也有一个superclass,并且子类与子元类、父类与父元类分别在同一个层次。这种关系借用网上的一张图来说明,一目了然。

    10. 注意:根元类的superclass不是nil而是根类。对于OC原生的类,根元类的父类就是系统的根类NSObject。但根类不一定是NSObject,因为后面介绍的objc_allocateClassPair函数也可以创建出一个根类。
  4. 举例:

     NSArray *array = [NSArray new];
     1.同过类对象(NSArray)的isa指针去到对应的元类中的方法列表中找,没有找到
     2.同过superclass指针到他的父类(NSObject)的元类中的方法列表中去找,找到
     3.调用+new
    

对象

  1. 代码: NSObject *obj = [NSObject new]; Xcode中点击进入NSObject,可以看到如下:

     @interface NSObject <NSObject> {
         Class isa  OBJC_ISA_AVAILABILITY;
     }
    
    1. 可以看到这仅仅是OC的一个类,但是我们可以看出来,一个对象内部只有一个Class类型的isa指针
    2. 那么怎么才能得到C代码呢?
    3. 我们知道NSObject* <=> id ,那么我们进入id看看呢?
  2. 在objc.h中我们得到如下:

     /// Represents an instance of a class.
     struct objc_object {
         Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
     };
        
     /// A pointer to an instance of a class.
     typedef struct objc_object *id;
    
    1. 可以看到id是一个struct objc_object类型的机构体指针,因此可以得出结论:对象是一个struct objc_object类型的结构体指针
    2. objc_object结构体中仅仅有一个成员变量isa,这也正好印证了,对象中有一个isa指针指向其类(类对象)
  3. 注意:

    1. 这个isa指向的是当前对象的类,也就是当前对象的类对象
    2. 这个isa指向的Class即objc_class结构体的info成员变量是CLS_CLASS (0x1L)类型的,即成员变量objc_method_list中存放的只有对象方法

总结:

  1. 在OC中,一个实例对象/类对象/元类对象默认都会有一个isa成员变量(注意:这里说的是OC对象的成员变量,可不是结构体的成员变量)
  2. 实例对象的isa指向了其类对象,类对象的isa指向了其元类对象,元类对象的isa指针指向了根类(NSObject)的元类对象,根类(NSObject)的元类对象指向了他自己

Runtime的运行时系统

  1. 消息机制是运行时里面最重要的机制,OC中任何方法的调用,本质都是发送消息。
  2. 使用运行时,发送消息需要导入框架<objc/message.h>并且Xcode5之后,苹果不建议使用底层方法,如果想要使用运行时,需要关闭严格检查objc_msgSend的调用,BuildSetting->搜索msg 改为NO
  3. 在Objective-C中,使用[receiver message]语法并不会马上执行receiver对象的message方法的代码,而是向receiver发送一条message消息,这条消息可能由receiver来处理,也可能由转发给其他来处理对象,也有可能假装没有接收到这条消息而没有处理。其实[receiver message]被编译器转化为:

     id objc_msgSend ( id self, SEL op, ... );
    
  4. 下来看一下实例方法调用底层实现

     Person *p = [[Person alloc] init];
     [p eat];
     // 底层会转化成
     //SEL:方法编号,根据方法编号就可以找到对应方法的实现。
     [p performSelector:@selector(eat)];
     //performSelector本质即为运行时,发送消息,谁做事情就调用谁
     objc_msgSend(p, @selector(eat));
     // 带参数
     objc_msgSend(p, @selector(eat:),10);
    
  5. 类方法的调用底层:

     // 本质是会将类名转化成类对象,初始化方法其实是在创建类对象。
     [Person eat];
     // Person只是表示一个类名,并不是一个真实的对象。只要是方法(不管是类方法还是对象方法)必须要对象去调用。
     // RunTime 调用类方法同样,类方法也是类对象去调用,所以需要获取类对象,然后使用类对象去调用方法。
     //1.创建类对象
     Class personclass = [Persion class];
     //2.类对象调用方法
     [[Persion class] performSelector:@selector(eat)];
     //3. 转化成运行时, 类对象发送消息
     objc_msgSend(personclass, @selector(eat));
    

如何动态查找方法

  1. SEL是一个方法选择器。SEL其主要作用是快速的通过方法名字查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个Int类型的地址,地址中存放着方法的名字。
  2. 对于一个类中。每一个方法对应着一个SEL。所以一个类中不能存在2个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。
  3. 运行时发送消息的底层实现:
    1. 每一个类都有一个方法列表 Method List,保存这类里面所有的方法,根据SEL传入的方法编号找到方法,相当于value - key的映射。然后找到方法的实现。去方法的实现里面去实现。如图所示。

  4. 下面我们就以p实例的eat方法来看看具体消息发送之后是怎么来动态查找对应的方法的。
    1. 实例方法[p eat];底层调用[p performSelector:@selector(eat)];方法,编译器在将代码转化为objc_msgSend(p, @selector(eat));
    2. 在objc_msgSend函数中。首先通过p的isa指针找到p对应的class。在Class中先去cache中通过SEL查找对应函数method,如果找到则通过method中的函数指针跳转到对应的函数中去执行。
    3. 若cache中未找到。再去methodList中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
    4. 若methodlist中未找到,则去superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

动态语言与静态语言

  1. 静态语言
    1. 比如调用一个方法的时候,编译器的本质就是直接编译成调用哪个函数的汇编语言
    2. 即在编译时期就确定了调用那个方法,所以是静态的

       //函数
       test(2);
       //编译成汇编
       //汇编指令call   函数地址
       call  0x100000db0
      
  2. 为什么说OC是一门动态语言?
    1. 所谓的动态语言,就是在运行时候才能决定到底调用那个方法
    2. 那么它是如何实现运行的时候才能决定调用到哪个方法呢?一旦编译器一编译不就决定调用那个方法了吗? 下面实现思路:
      1. 设想一下,如果有一个方法数组,根据一个key值比对才能决定到底调用这个方法数组里面的哪个方法呢?
      2. 那这就需要,真正运行到调用这个遍历方法时,通过key值一一比对,才能决定到底要调用那个方法
      3. 这就实现了动态执行方法
    3. OC方法的调用本质
      1. 调用方法:[p eat]
      2. 本质是 : [p performSelector:@selector(eat)];
      3. 本质是: objc_msgSend(p, @selector(eat));
      4. 本质是:通过p的isa指针到对应的类的Method List中查找,相当于findIMPfromMethodArray(@selector(eat))
      5. 然后通调用函数
    4. 那么编译器编译的时候,就不能确定到底是调用了那个方法,因为这需要执行findIMPfromMethodArray遍历才能找到具体的那个方法,因此是动态的
    5. 上面的2、3、4、5都是runtime的API实现的,因此也可以说明OC之所以是一个动态的语言就是因为它有runtime,runtime将方法调用转化成动态查询的方式。
  3. 这就是为什么静态语言的方法必须是已经实现的,否则编译不通过。动态语言的方法可以只有声明,编译也可以通过。
Table of Contents