神奇的Runtime

Objective-C中Runtime的概括总结

Posted by Ted on March 5, 2016

一、本质

[receiver message]不是一个简单地方法调用,而是在编译阶段被编译器转化为

objc_msgSend(Class receiver,SEL selector, arg1, arg2, ...)

,只是在编译阶段确定了要向receiver发送message这条消息,而receiver如何响应这条消息,要看运行时来决定,消息的receiver能够找到对应的selector,那么就相当于直接执行了receiver这个对象的特定方法;否则,消息要么被转发,或是临时向receiver动态添加这个selector对应的实现内容,要么就干脆崩溃掉。

NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

二、Class结构[objc_msgSend(Class receiver,SEL selector, arg1, arg2, …)之Receiver]

@interface NSObject <NSObject> {
    Class isa;
}

有一个Class类型的isa属性,typedef struct objc_class *Class,所以Class是一个objc_class结构类型的指针;

struct objc_class {  
    struct objc_class *isa;  	     //指向该对象所属类型的类型对象(Class Object),而类的isa指针指向它的metaclass.
    struct objc_class *super_class;  //指向父类,如果该类已经是最顶层的根类(如 NSObject 或 NSProxy),那么 super_class 就为 NULL.
    const charchar *name;            //类名称
    long version; 		     //类的版本信息
    long info; 			     //运行期使用的标志位,比如0x1(CLS_CLASS)表示该类为普通class,0x2(CLS_META)表示该类为metaclass 
    long instance_size; 	     //实例大小,即内存所占空间
    struct objc_ivar_list *ivars;    //指向成员变量列表的指针
    struct objc_method_list **methodLists; //根据info标志位的不同可能指向不同,比如可能指向实例方法列表,或者指向类方法列表 
    struct objc_cache *cache;        //因为Objective-C的消息转发需要查找dispatch table甚至可能需要遍历继承体系,所以缓存最近使用的方法。  
    struct objc_protocol_list *protocols;  //类需要遵守的协议
  }

isa&superclass

每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的isa指针指向类。

每一个类本质上都是一个对象,类其实是元类(meteClass)的实例。元类定义了类方法的列表。类通过类的isa指针指向元类。

所有的元类最终继承一个根元类,根元类isa指针指向本身,形成一个封闭的内循环

superclass指向该类的父类, 如果该类已经是最顶层的根类(如 NSObject 或 NSProxy),那么 super_class 就为 NULL.

上图实线是 super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。

需要深刻理解 [self class] 与 object_getClass(self) 甚至 object_getClass([self class]) 的关系,其实并不难,重点在于 self 的类型:

当 self 为实例对象时,[self class] 与 object_getClass(self) 等价,因为前者会调用后者。object_getClass([self class]) 得到元类。

当 self 为类对象时,[self class] 返回值为自身,还是 self。object_getClass(self) 与 object_getClass([self class]) 等价。

objc_ivar_list 成员变量的数组,成员变量生成后变不能修改

struct objc_ivar_list {  
    int ivar_count;  /* variable length structure */  
    struct objc_ivar ivar_list[1]; 
} 

objc_method_list 方法列表指针,存储着objc_method列表,可以动态修改方法列表的值来添加成员方法

struct objc_method_list {  
    struct objc_method_list *obsolete;  
    int method_count;  /* variable length structure */  
    struct objc_method method_list[1];  
}

objc_cache 指向最近使用的方法.用于方法调用的优化.

struct objc_cache {  
    unsigned int mask /* total = mask + 1 */;  
    unsigned int occupied;  
    Method buckets[1];  
};  

protocols 协议的数组指针

struct objc_protocol_list {  
    struct objc_protocol_list *next;  
    long count;  
    Protocol *list[1];  
}; 

三、SEL[objc_msgSend(Class receiver,SEL selector, arg1, arg2, …)之selector]

Seloctor:方法选择器,其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。SEL类型的数据结构是SEL:typedef struct objc_selector *SEL;每个selector会对应一个IMP来实现函数(methodForSelector可以获取Selector对应的IMP);

Method:一种代表类中的某个方法的类型。

在Class的objc_method_list里有一个objc_method列表

struct objc_method {
    SEL method_name                           
    char *method_types     存储着方法的参数类型和返回值类型                                 
    IMP method_imp     指向了方法的实现,本质上是一个函数指针                                 
} 

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

1、methodForSelector->Method

2、method_getImplementation->IMP

四、消息发送机制:

伪代码:

id objc_msgSend(id self, SEL _cmd, ...) {
  Class class = object_getClass(self);
  IMP imp = class_getMethodImplementation(class, _cmd);
  return imp ? imp(self, _cmd, ...) : 0;
}

receiver查找:

1、判断接受者receiver,也就是第一个参数self,是否为空? 如果为空,调用nil-handler, 默认为什么都不做。

方法查找

2、检查class的方法调用cache,是否调用过此方法。找到了则分发,否则

3、用objc-class.mm中_class_lookupMethodAndLoadCache3检查从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector。否则

4、寻找父类的method list,并依次往上寻找(会找到NSObject),直到找到selector,填充到缓存中,并返回selector,否则

动态解析,这里动态添加方法

5、如果找到可以动态resolve为一个selector(调用 resolveInstanceMethod:resolveClassMethod: 方法添加实例方法实现和类方法实现。),不缓存,方法返回,否则

消息快速转发,这里将消息分配给其他对象处理

6、消息转发机制执行前,Runtime 系统允许我们替换消息的接收者为其他对象。通过 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。

- (id)forwardingTargetForSelector:(SEL)sel { return _otherObject; }

因为这一步不会创建任何新的对象,但下一步转发会创建一个 NSInvocation 对象,所以相对更快点。

消息普通转发

7、首先runtime发送methodSignatureForSelector:消息

生成Selector对应的方法签名,即参数与返回值的类型信息。

如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation(NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息),向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数;

methodSignatureForSelector:无方法签名返回,则向当前对象发送doesNotRecognizeSelector:消息,程序抛出异常退出。

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL sel = invocation.selector;
    if([someObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:someObject];
    } 
    else { //或者崩掉
        [self doesNotRecognizeSelector:sel];
    }
}

与快速转发二者区别:快速转发不用专门生成NSInvocation,快速消耗少,普通转发可以转发给多个对象。

五、动态添加属性

category可以动态添加方法,借助Runtime还可以添加属性,但是不能添加成员变量,Category可以通过runtime.h中objc_getAssociatedObject / objc_setAssociatedObject来访问和生成关联对象。通过这种方法来模拟生成属性,但是与对象还是有点区别,因为对象属性会编译器自动生成setter和getter方法,会默认给你生成一个以下划线开头的成员变量,而category不手动去生成setter和getter的话,会报错。

1、不能添加成员变量,会报错

这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了

六、KVC与KVO

1、KVC

KVC运用了isa-swizzing技术。isa-swizzing就是类型混合指针机制。KVC通过isa-swizzing实现其内部查找定位。isa指针(is kind of 的意思)指向维护分发表的对象的类,该分发表实际上包含了指向实现类中的方法的指针和其他数据。

比如说如下的一行KVC代码:

[site setValue:@"sitename" forKey:@"name"];

会被编译器处理成

SEL sel = sel_get_uid(setValue:forKey);
IMP method = objc_msg_loopup(site->isa,sel);
method(site,sel,@"sitename",@"name");

每个类都有一张方法表,是一个hash表,值是还书指针IMP,SEL的名称就是查表时所用的键。 SEL数据类型:查找方法表时所用的键。定义成char*,实质上可以理解成int值。 IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。

2、KVO

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

简单而言:在被观察时,生成派生类,对于观察属性重写setter方法,然后在valuewillchange方法和valuesdidchanged方法里发出通知

1、当一个object有观察者时,动态创建这个object的类的子类

2、对于每个被观察的property,重写其set方法

3、在重写的set方法中调用- willChangeValueForKey:和- didChangeValueForKey:通知观察者

4、当一个property没有观察者时,删除重写的方法

5、当没有observer观察任何一个property时,删除动态创建的子类

七、Method Swizzling原理

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

每个类都有一个方法列表,存放着selector的名字和IMP方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,

我们可以利用 method_setImplementation 来直接设置某个方法的IMP

八、利用Runtime可以作用的一些方法:

class_getName:获取类名

class_getSuperclass:获取父类

class_getInstanceSize:获取实例大小

class_getInstanceVariable:获取实例成员变量

class_getClassVariable:获取类成员变量

class_getProperty:获得属性

class_getInstanceMethod:获得实例方法

class_getClassMethod:获得类方法class_getMethodImplementation:获得IMP

class_copyIvarList:获取成员变量列表

class_copyMethodList:获取方法列表

class_copyProtocolList:获取协议列表

class_addIvar:添加成员变量(添加成员变量只能在运行时创建的类,且不能为元类)

class_addProperty:添加属性

class_addMethod:添加方法

class_addProtocol:添加协议

class_replaceProperty:替换属性的信息(如果没有原属性会新建一个属性)

class_replaceMethod:替代方法的实现

class_respondsToSelector:查看类是否相应指定方法

class_isMetaClass:查看类是否为元类

class_conformsToProtocol:查看类是否遵循指定协议

object_getInstanceVariable:获取实例的成员变量

object_getIvar:获取成员变量的值

object_getClassName:获取指定对象的类名

object_getClass:获取指定对象的类

objc_getMetaClass:获取指定类的元类

object_copy:拷贝指定对象

objc_getProtocol:获取指定名字的协议

object_setInstanceVariable:设置指定实例指定名称的成员变量的值

object_setIvar:设置指定对象的指定的成员变量的值

objc_setAssociatedObject:设置关联对象的值

objc_getAssociatedObject:获取关联对象的值

objc_removeAssociatedObjects:移除关联对象

ivar_getName:获取成员变量名

ivar_getTypeEncoding:获取成员变量类型编码

ivar_getOffset:获取成员变量的偏移量

property_getName:获取属性名

property_copyAttributeValue:获取属性中指定的特性

method_invoke:调用指定方法的实现

method_getName:获取方法名

method_getImplementation:返回方法的实现