runtime编程指南小结

本章节作为Objective-C 2.0运行时系统编程指南的小结;也算是一次对系统性书籍的知识吸收,对零散知识的复习

首先第一个问题:runtime到底是什么?

runtime是一套由C、C++、汇编语言编写的数据结构函数
这些函数使得访问运行时系统成为了可能

本文章包括以下内容:

  1. 运行时系统的版本和平台
  2. 和运行时系统的交互
  3. 消息
  4. 动态方法解析
  5. 消息转发
  6. 类型编码
  7. 属性声明

运行时系统的版本和平台

早期版本和现在的版本
  1. 早期版本是Objective-C 1。 在早期版本中,如果你改变类中的实例变量的布局,你必须重新编译该类的所有子类

  2. 现在的版本是Objective-C 2。 在现在的版本中,如果你改变类中的实例变量的布局,你无需重新编译该类的子类。
    现行版本还支持声明property的synthesis属性

平台

iPhone程序和Mac OS X v10.5及以后的系统中的64位程序使用的都是Objective-C 2.0的版本
其他情况(Mac OS X系统中的32位程序)使用的是早期的版本

和运行时系统的交互

Objective-C 程序有三种途径和运行时系统交互:

通过Objective-C源码

大部分情况下,运行时系统会在底层自动运行,我们只需要编写Objective-C源码

当我们编译OC类和方法的时候,编译器为实现语言动态特性将会自动创建一些数据结构和函数。这些数据结构包含了类的信息,运行时系统的主要功能就是根据OC代码去发送消息。

说白了,我们的OC代码,被动的被系统利用到运行时。

通过NSObject类的方法

OC中绝大部分类都是NSObject类的子类,所以拥有了NSObject类的所有行为(NSProxy是一个例外,在消息转发中有提到)
以下方法,都属于利用了运行时

  • Class 返回对象的类
  • isKindOfClass 、isMemberOfClass 检查对象是否在指定的继承体系中
  • respondsToSelector 检查对象是否能响应某方法
  • conformsToProtocol 检查对象是否实现了指定的协议方法
  • methodForSelector 则返回指定方法实现的地址
通过运行时系统的函数

runtime有一套公开的API,这些API声明在/usr/include/objc中。用来供开发者使用,提供运行时能力。
这里不列举了,只是一些函数名而已

消息

本节内容描述了方法如何转化为对objc_msgSend的调用,如何通过名字来指定一个方法,以及如何使用objc_msgSend函数

获得方法地址

在明白方法调用时,是通过isa指针->类->父类....->NSObject 这一前提下。
还要知道类中有一个cache ,是用来存放方法缓存的。他是一个hashmap,key值为方法签名(Selector)
value为函数地址(IMP)。当一个方法被调用时,那么他可能还会被调用,当要频繁得调用某一方法时,使用方法地址就会大大提高效率
利用NSObject类中的methodForSelector: 方法,你可以获得一个指向方法实现的指针,并且可以使用该指针,直接调用函数。返回的指针和赋值的变量类型必须完全一致。

下面是一个例子,利用指针来调用setFilled:的方法实现

void (*setter)(id, SEL, BOOL);
int I;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];

for ( i = 0; i < 1000, i++ ){
setter(targetList[i], @selector(setFilled:), YES);
}

函数指针setter的第一个参数是接受消息的对象self, 第二个参数是方法选标(_cmd) 。这两个参数是所有方法都有的,是一个隐式参数,但是在函数的表达中必须显示的给出。

使用methodForSelector: 来避免动态绑定可以减少大部分的开销
注意:methodForSelector: 是runtime提供的功能,不是OC语言本身的功能

objc_msgSend函数

在OC中,消息是直到运行的时候才和方法实现绑定的。编译器会把一个方法的表达式:

[receiver message]

转换成对objc_msgSend函数的调用。 该函数有两个主要的参数:消息接受者(id类型)和消息对应的方法名字(_CMD类型):

objc_msgSend(receiver, selector)

同时接受消息中的任意数目的参数:

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

该函数做了动态绑定所需要的一切:

  1. 它首先找到了方法选标对应的方法实现。 因为不同的类对同一方法可能会有不同的实现,所以找到的方法实现依赖于消息接受者的类型
  2. 然后将消息接收者对象以及方法中指定的参数传给找到的方法实现
  3. 最后,将方法实现的返回值作为该函数的返回值返回。

消息机制的关键在于编译器为类和对象生成的数据结构。每个类的结构中至少包含两个基本元素:

  1. 指向父类的指针
  2. 类的方法表。方发表就是一个hashmap,通过key - value的方式关键方法选标和方法实现

当新的对象被创建时,其内存同时被分配,实例变量也同时被初始化。对象的第一个实例变量是一个指向该对象的类的指针,叫isa指针,通过这个指针,找到对象的类及其父类。


方法查找流程.png

当对象收到消息时,消息函数首先根据该对象isa指针找到该对象所对应的类的方发表,并从表中寻找改消息对应的方法选标。如果找不到,objc_msgSend将继续从父类中寻找,知道NSObject类。一旦找到了方法选标,objc_msgSend则以消息接受者对象为参数调用,调用该选标对应的方法实现。这就是在运行时系统中选择方法实现的方式。在面向对象编程中,一般称作方法和消息动态绑定的过程.
为了加快消息的处理过程,runtime会将使用过的方法选标和方法实现的地址放入缓存中。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。消息函数会首先检查消息接受者对象对应的类的缓存。如果在缓存中已经有了需要的方法选标,则消息仅仅比函数调用慢一点点,这个过程称作快速查到,其底层是运行的汇编代码,其原因有两点:

  1. c、c++语言无法做到:传入一个未知的对象,和一个未知的方法名,跳转到任意指针
  2. 此行为是程序运行中最为频繁的,用汇编,更快

动态方法解析

动态方法解析

如果你需要动态的提供一个方法的实现,你可以通过实现 resolveInstanceMethod: 和 resolveClassMethod: 来动态地实现给定选标的对象方法或者类方法
可以通过resolveInstanceMethod: 将一个dynamicMethodIMP函数作为类方法resolveThisMethodDynamically方法的实现

void dynamicMethodIMP(id self, SEL _cmd) {
//mentation .... 
}

@implementation MyClass

+ (BOOL)resolveInstanceMethod: (SEL)aSEL
{
      if (aSEL == @selector(resolveThisMethodDynamically)) {
           class_addMethod([self Class], aSEL, (IMP)dynamicMethodIMP, "v@:")
           return YES;
      } 
      return [super resolveInstanceMethod: aSEL]
}

通常来说,消息转发和动态方法解析是互不相干的。在进入消息转发机制之前,respondsToSelector: 和 instancesRespondToSelector: 会被首先调用。可以在这个两个方法中为传进来的选标提供一个IMP。如果你实现了resolveInstanceMethod: 方法,但还是希望继续走消息转发流程,只需要返回NO就可以了。


消息转发流程.png

动态加载

OC可以在运行时链接和载入新的类。新载入的类在程序启动时载入的类并没有区别。

消息转发

消息转发

如果一个对象收到一条为实现的消息,runtime会在抛出错误前,给该对象发送一条forwardingInvocation: 消息,该消息的唯一参数是一个NSInvocation类型的对象,该对象封装了原始的消息和消息的参数。

你可以实现forwardingInvocation: 方法来对没有实现的消息做一些处理,也可以以其他某种方式来避免错误被抛出。正如forwardingInvocation: 的名字所示,它通常用来将消息转发给其他对象。

关于消息转发的作用,你可以考虑如下场景:假如你需要设计一个能够响应 negotiate 方法的对象,并且能够包括其他类型的对象对消息的响应。通过在 negotiate 方法的实现中将 negotiate 消息转发给其他对象,很容易达到这个目的。

更进一步,假如你希望你的对象和另外一个类的对象对 negotiate 消息的响应完全一致。一种方式就是让你的类继承于它。 但有时候,你的类和此类需要在不同的继承体系中。虽然你的类无法继承其他类的 negotiate 方法,你还可以这么做:提供一个方法实现,这个方法实现只是简单的将 negotiate 消息转发给其他类的对象:

-(id)negotiate {
    if ([someOtherObj respondsTo:@selector(negotiate)]) {
         return [someOtherObj negotiate];
    }
    return self;
}

这种方式不太灵活,特别是有很多消息你都希望传递给其他对象时,你必须在每一种消息都做同样的事情。此外,这种方式不能处理未知的消息。消息类型是死的,但实际上,消息的类型可能会随着运行时而发生变化。

forwardInvocation: 方法给这个问题提供了一个动态的解决方案: 当一个对象由于没有相应的方法实现时,runtime会通过 forwardInvocation: 消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation: 方法。而NSObject中的方法实现只是简单的调用了 doesNotRecognizeSelector: 。通过重写forwardInvocation: 方法,你可以在该方法实现中将消息转发给其他对象。

要转发消息给其他对象,forwardInvocation: 方法所必须做的有:

  1. 决定将消息转发给谁
  2. 将消息和原来的参数一起转发出去

消息可以通过invokeWithTarget: 方法来转发:

- (void)forwardInvocation(NSInvocation *)anInvocation {
        if ([someObj respondsToSelector:[anInvocation selector]]) {
                [anInvocation invokeWithTarget: someObj]
        } else {
                [super forwardInvocation: anInvocation]
        }
}

转发消息后的返回值将返回给原来的消息发送者

forwardInvocation: 方法就像一个不能识别消息的分发中心,将这些消息转发给不同接受对象;或者它也可以想消息都发送给同一个接受对象;可以将一个消息翻译成另外一个消息,或者简单的 吃掉 某些消息,因此没有响应也没有错误。

注意:forwardInvocation: 方法只有在消息接受者不能正常响应消息时才会被调用。所以如果你希望你的对象将negotiate消息转发给其他对象,你的对象不能有negotiate方法的实现。否则将不会调用forwardInvocation

消息转发和多重继承

消息转发很像继承,一个对象通过转发来响应消息,看起来就是像是从别的类继承了方法实现一样。


WeChatae00f1cd5ac02a472f6c356eb9b820e8.png

在上图中,warrior类的一个实例对象将 negotiate 消息转发给 Diplomat 类的一个实例。看起来,warrior类似乎和Diplomat类一样。响应 negotiate 消息,并且行为和Diplomat一样(实际上是Diplomat类响应了该消息)

转发消息的对象看起来有两个继承分支:自己的和响应消息的对象的。在上面的例子中,warrior看起来同时继承自己的父类和Diplomat类。

消息转发提供了多重继承的很多特性。然后两者又有很大不同:多重继承是将不同的行为封装到单个的对象中,有可能导致庞大的,复杂的对象。而消息转发是将问题分解到更小的对象中,但是又以一种对消息发送对象来说完全透明的方式将这些对象联系起来。

消息转发和类继承

消息转发很像继承,但它不是继承。
例如在NSObject类中,方法respondsToSelector: 和 isKindOfClass: 只会出现在继承链中。例如向一个Warrior类的对象发送此消息:

if ([aWarrior respondsToSelector:@selector(negatiate)])  {
}

返回值是NO,尽管该对象能够接受和响应negotiate
如果你需要其返回YES ,比如:
使用消息转发来创建一个代理对象以扩展某个类的能力,这里的消息转发必须和继承一样,尽可能的对用户透明。如果你希望代理对象看起来就像是继承自代表它代表的对象一样,你需要重新实现respondsToSelector: 方法和isKindOfClass: 方法

- (BOOL)respondsToSelector:(SEL)aSelector 
{ if ( [super respondsToSelector:aSelector] ) return YES; else { /* Here, test whether the aSelector message can * * be forwarded to another object and whether that * * object can respond to it. Return YES if it can. */ } return NO; }

除了respondsToSelector: 和 isKindOfClass: 外,instanceRespondToSelector: 方法也必须重新实现。如果你使用的是协议类,还需要重写conformsToProtocol: 方法。如果对象需要转发远程消息,则methodSignatureForSelector: 方法必须能够返回实际响应消息的方法的描述。

注意:消息转发是一个比较高级的技术,仅适用于没有其他更好的解决办法的情况。它并不是用来代替继承的。

类型编码

为了和运行时系统协作,编译器将方法的返回值类型和参数类型都编码成一个字符串。并且和方法选标关联在一起。

WeChat25d1deebeed90835d9f18d4d8f21e6ce.png

WeChat3f5009d077d69fe0bcbd99faafeb801b.png

上图为对应表

属性声明

当编译器遇到一个Property声明时,编译器会产品一些元数据与属性所在的类或者协议类关联。 我们可以通过一些函数访问它们。每个类或者协议类都维护了一个声明了的属性列表。

属性类型和相关函数

函数class_copyPropertyList 和 protocol_copyPropertyList 来或者类或者协议类中的属性列表

函数property_getName 获取属性的名字

函数property_getAttributes 可以获取属性的名字和@encode编码
还有更多函数,这里只是举例说明
关于属性的相关内容,还有

  1. 属性类型编码
  2. 属性特征的描述范例
    暂时觉得没有太大的用处,后续用到会更新------

以上就是runtime编程指南学习总结 若有不对,还请指出


码海无涯,学无止境

你可能感兴趣的:(runtime编程指南小结)