用过OC都应该或多或少知道runtime这个黑魔法。OC强大的原因就是运行时这个特性,虽然实际开发中需要用到的runtime知识很少,但是学习它能有助于自己更好的理解OC的语言特性,搞清楚底层的一些相关实现。于是找了一些相关资料。
Objective-C Runtime 运行时之一:类与对象
Objective-C Runtime 运行时之二:成员变量与属性
Objective-C Runtime 运行时之三:方法与消息
Objective-C Runtime 运行时之四:Method Swizzling
Objective-C Runtime 运行时之五:协议与分类
Objective-C Runtime 运行时之六:拾遗
Objective-C 的动态提示和技巧
Objective-C Runtime
Objective-C的hook方案(一): Method Swizzling
关于runtime的资料很多,我觉得这些都是不错的。特别是前几篇循序渐进,讲的比较细致。读了之后虽然还有些不明白的地方,但还是受益匪浅。现在记下一些收获和不解的地方。
首先先了解类的数据结构。它是一个结构体指针,包含了许多信息,其中有一个objc_cache * cache
这个是用来缓存该类调用过的方法的,每次调用方法的时候先从方法缓存的结构体的数组开始查询,因为常用的方法就那么一些,这样就避免的每次都去遍历该类的方法列表或者父类的方法列表。从效率上来说要更好一点吧。类的结构体还包含父类,类名,版本,类信息,实例变量的大小,类的成员变量链表,方法定义链表,协议链表就不一一细说了,runtime都提供了相关的API 可以去查询这些信息,不过需要注意的是,我在使用的时候发现有些函数是MRC下才能使用的。
还有一个概念 元类(Meta Class)。当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。它存储着一个类的所有类方法。任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
第二篇讲到成员变量和属性 其中有个点是关联对象,这个倒是有用过。当你写了一个类别,想要给他添加属性的时候是不被允许的 。这个时候有个解决方案就是关联对象,运行时添加。将对象通过给定的key连接到类的一个实例上。不过由于是C接口,所有key是一个void指针(const void *)。
方法与消息。有三个概念 Method、SEL、IMP
SEL应该很熟悉,@selector(...) 这个获得的就是SEL,它是一个指向方法的指针。。明显它是由方法名决定的,所有我们在编写代码的时候会发现 在同一个类里不能有两个同名的方法。
IMP 是一个函数指针,指向方法实现的首地址。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。换言之就是跳过了Runtime的消息传递机制,直接执行IMP指向的函数实现,这样会更高效。
Method 是一个结构体,包含了一个SEL与IMP。
typedef struct objc_method *Method; struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; // 方法实现 }
理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。
方法调用流程: 在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend.这个函数首先去找selector对应的方法实现,如果没找到,然后就通过父类的指针找到父类,然后在父类的方法list找。如果没有找到就一直沿着继承体系找,直到NSObject。如果最后还是没有定位到selector就会走消息分发流程。
[object message] 如果object无法响应的话,编译器会报错
performSelector 这个是运行时才确定object能否接受message消息
当object无法接受message消息的时候就会启动消息转发的机制。
消息转发的机制基本分三步:
1.动态方法解析
2.备用接收者
3.完整转发
动态方法解析 :当对象接受到未知的消息的时候,首先会调用所属类的的类方法+ (BOOL)resolveInstanceMethod:(SEL)sel
在这里我们可以让它调用我们指定的方法,或者动态添加一个方法来调用。
备用接收者 :如果上一步无法处理消息,则runtime会继续调以- (id)forwardingTargetForSelector:(SEL)aSelector
如果实现了这个方法,可以返回一个非nil的结果。然后这个对象就会作为消息的新接收者。且消息会被分发到这个对象,当然这个对象不能是self自身,否则就会出现无限循环。
完整转发 :这一步是给消息接收者最后一次机会将消息转发给其他对象。调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。然后我们可以在forwardInvocation方法中选择将消息转发给其它对象。
Method Swizzling
简单的来说,Method Swizzling 从字面意思理解就是方法互调,利用Runtime提供的函数,将对应的方法实现换掉。明显可以用来重写自带的方法。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。Swizzling应该总是在dispatch_once中执行
+load会在类初始加载时调用
+initialize会在第一次调用类的类方法或实例方法之前被调用
对比之下,发现在+load重写更合适。+initialize有可能不被调用会出现一些不可估计的问题。
Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。
关于Super struct objc_super { id receiver; Class superClass; };
receiver:即消息的实际接收者与self相同
superClass:指针当前类的父类
调用父类方法,会用到super。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。发送消息时,不是调用objc_msgSend函数,而是调用objc_msgSendSuper函数。
[super viewDidLoad]; objc_msgSendSuper ( struct objc_super *super, SEL op, ... ); objc_msgSend(objc_super->receiver, @selector(viewDidLoad))
objc_msgSend(self, @selector(viewDidLoad))