首页 > 代码库 > Runtime

Runtime

参考资料:http://www.jianshu.com/p/19f280afcb24

 

 

类和对象

 

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。

 

这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。这个运行时系统即Objc Runtime。Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。

 

 

 

runtime 概念


Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了 runtime 运行时来处理,可以说 runtime 是我们 Objective-C 幕后工作者。

  • runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。

  • 对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。

  • OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

  • 事实证明:在编译阶段,OC 可以 调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而 C 语言 调用未实现的函数 就会报错

 

Runtime库主要做下面几件事:

封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。
Objective-C runtime目前有两个版本:Modern runtime和Legacy runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。

 

类与对象基础数据结构

Class

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

typedef struct objc_class *Class;

 

查看objc/runtime.h中objc_class结构体的定义如下:
 1 struct objc_class {
 2     Class isa  OBJC_ISA_AVAILABILITY;
 3 
 4 #if !__OBJC2__
 5     Class super_class                       OBJC2_UNAVAILABLE;  // 父类
 6     const char *name                        OBJC2_UNAVAILABLE;  // 类名
 7     long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
 8     long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
 9     long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
10     struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
11     struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
12     struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
13     struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
14 #endif
15 
16 } OBJC2_UNAVAILABLE;

 

在这个定义中,下面几个字段是我们感兴趣的
  • isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。

  • super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。

  • cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

  • version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

runtime 常见作用


  • 动态交换两个方法的实现

  • 动态添加属性

  • 实现字典转模型的自动转换

  • 发送消息

  • 动态添加方法 (面试用到)

  • 拦截并替换方法

  • 实现 NSCoding 的自动归档和解档

 

动态添加方法

应用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

注解:OC 中我们很习惯的会用懒加载,当用到的时候才去加载它,但是实际上只要一个类实现了某个方法,就会被加载进内存。当我们不想加载这么多方法的时候,就会使用到 runtime 动态的添加方法。

 

实现NSCoding的自动归档和解档

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。

 

runtime 消息机制


我们写 OC 代码,它在运行的时候也是转换成了 runtime 方式运行的。任何方法调用本质:就是发送一个消息(用 runtime发送消息,OC 底层实现通过 runtime 实现)。

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。

每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。

 

示例代码:OC 方法-->runtime 方法

说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;
技术分享
 1 // Person *p = [Person alloc];
 2 // 底层的实际写法
 3 Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
 4 
 5 // p = [p init];
 6 p = objc_msgSend(p, sel_registerName("init"));
 7 
 8 // 调用对象方法(本质:让对象发送消息)
 9 //[p eat];
10 
11 // 本质:让类对象发送消息
12 objc_msgSend(p, @selector(eat));
13 objc_msgSend([Person class], @selector(run:),20);
14 
15 //--------------------------- <#我是分割线#> ------------------------------//
16 // 也许下面这种好理解一点
17 
18 // id objc = [NSObject alloc];
19 id objc = objc_msgSend([NSObject class], @selector(alloc));
20 
21 // objc = [objc init];
22 objc = objc_msgSend(objc, @selector(init));
View Code

 

runtime 方法调用流程「消息机制」


面试:消息机制方法调用流程

  • 怎么去调用eat方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
    • 1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。
    • 2.注册方法编号(这里用方法编号的好处,可以快速查找)。
    • 3.根据方法编号去查找对应方法。
    • 4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。
  • 补充:一个objc 对象的 isa 的指针指向什么?有什么作用?
    • 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
 

runtime 下Class的各项操作

 

  • 获取属性列表

    1 objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    2 for (unsigned int i=0; i<count; i++) {
    3    const char *propertyName = property_getName(propertyList[i]);
    4    NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
    5 }

     

  • 获取方法列表
    1 Method *methodList = class_copyMethodList([self class], &count);
    2 for (unsigned int i; i<count; i++) {
    3    Method method = methodList[i];
    4    NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
    5 }
    
    

     

    
    
  • 获取成员变量列表

  • 1 Ivar *ivarList = class_copyIvarList([self class], &count);
    2 for (unsigned int i; i<count; i++) {
    3     Ivar myIvar = ivarList[i];
    4     const char *ivarName = ivar_getName(myIvar);
    5     NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
    6 }

     

  • 获取协议列表

  •  

    1 __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    2 for (unsigned int i; i<count; i++) {
    3     Protocol *myProtocal = protocolList[i];
    4     const char *protocolName = protocol_getName(myProtocal);
    5     NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
    6 }

     

 

现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法

 

  • 获得类方法

    1 Class PersonClass = object_getClass([Person class]);
    2 SEL oriSEL = @selector(test1);
    3 Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);

     

  • 获得实例方法
  • 1 Class PersonClass = object_getClass([xiaoming class]);
    2 SEL oriSEL = @selector(test2);
    3 Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);

     

  • 添加方法
  • BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));

     

    
    
  • 替换原方法实现
  • class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

     

  • 交换两个方法的实现

    method_exchangeImplementations(oriMethod, cusMethod);
runtime 几个参数概念

 

1、objc_msgSend

这是个最基本的用于发送消息的函数。其实编译器会根据情况在objc_msgSend, objc_msgSend_stret,,objc_msgSendSuper, 或 objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 Super 的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。

 

2、SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()``或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

 

3、id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

 

4、runtime.h里Class的定义

 1 struct objc_class {
 2     Class isa  OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针
 3 
 4 #if !__OBJC2__
 5     Class super_class                                        OBJC2_UNAVAILABLE;//父类
 6     const char *name                                         OBJC2_UNAVAILABLE;//类名
 7     long version                                             OBJC2_UNAVAILABLE;//类版本
 8     long info                                                OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出)
 9     long instance_size                                       OBJC2_UNAVAILABLE;//实例大小
10     struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址
11     struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等
12     struct objc_cache *cache                                 OBJC2_UNAVAILABLE;//缓存
13     struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//协议
14 #endif
15 
16 } OBJC2_UNAVAILABLE;
17 
18  

 

 

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
objc_class结构体中:`ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category`实现的原理。

 

什么是 method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换

  • Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的

  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP

技术分享
selector --> 对应的IMP
    • 交换方法的几种实现方式
      • 利用 method_exchangeImplementations 交换两个方法的实现
      • 利用 class_replaceMethod 替换方法的实现
      • 利用 method_setImplementation 来直接设置某个方法的IMP
        技术分享
                                        交换方法

         

         

         

 

Runtime