OC语言能在从编译时、连接时到运行时推迟许多决策。尽可能的,语言动态执行。这意味着语言不仅仅需要一个编译器,而且需要一个运行时系统来执行编译的代码。运行时系统扮演了一个OC的操作系统角色,让语言正常运行。
下面深入介绍NSObject class和OC程序如何与运行时交互,也介绍关于如何动态加载新class和对其他对象的消息转发,也提供关于在程序运行时如何找到队形的信息。

Runtime交互的Level
OC程序在3个直接层面和Runtime交互: 通过OC代码,通过 NSObject 里定义的方法, 通过直接调用运行时的functions。
OC源码
大部分情况,运行时系统在幕后自动工作。你通过编写和编译OC代码来使用它。
当你编译包含OC classes和method的代码是,编译器创建了实现动态语言特征的 数据结构和function。数据结构捕获在class,category和protocol里定义的信息,包括:OC中定义类和协议里讨论的类和协议对象,以及方法选择器selector,instance variable templates和从源代码提取的其他信息。主要的Runtime funtion之一是 sends message,将会在 Messaging 中讨论。
NSObject Methods
大部分 Cocoa里的对象都是NSObject的子类,所以大部分对象继承了NSObject的方法(例外是NSProxy类,参考Message Forwarding )。因此,NSObject的方法建立了每个实例和每个类对象所固有的行为。但是,在少数情况下,NSObject类仅仅定义了一个模板,用于如何完成某些工作,它不提供所有必要的代码。
比如,NSObject类定一了一个descrtiption实例方法,返回一个类的内容描述字符串。这是GDB print-object返回的内容。NSObject实现这个方法,不知道类包含了什么,所以它返回对象的一个name和内存地址。NSObject的子类可以实现这个方法来返回更多信息。比如,NSArray返回一个它包含的对象的列表。
某些NSObject方法简单查询运行时系统来获取信息。这些方法允许对象来执行自我检查。比如一些class 方法:isKindOfClass: 或者 isMemberOfClass: 能测试一个对象在继承层级的位置;respondsToSelector方法能确定对象是否能接受一个特别的消息;conformsToProtocol: 能确定一个对象要求在指定协议中实现方法;还有 methodForSelector: 能提供method实现的地址。以上这些方法能让对象自己检查自己。
Runtime Functions
Runtime 系统是以恶动态共享库,有一组在header文件里的函数和数据结构组成的公开接口,存储在 /usr/include/objc。这些方法中的许多方法允许你使用纯C来复制编译器在编译Objective-C代码时所做的事情。Others form the basis for functionality exported through the methods of the NSObject class. 。其他来自功能基础的方法通过NSObject 类的方法导出。这些functions 让开发runtime 系统的其他接口成为可能,也能产生增强开发环境的工具,这些方法在编写OC时时不必要的,但是,部分function可能在编写OC程序时很有用。所有functions都在 Objective-C Runtime Reference
关于消息 Messaging
下面介绍了消息表达式转为 objc_msgSent的调用、还有如何通过name来引用methods。然后解释了如何利用objc_msgsend和如何在需要的情况下避免动态绑定。
objc_msgSend 函数
OC中,消息知道运行时才绑定到方法的实现。编译器转换消息表达式 [receiver message]
为一个messaging 函数的调用——objc_msgSend。这个函数(也就是method selector)拿到了接收者receiver和message中关于方法的name作为两个主要参数,转换为:objc_msgSend(receiver, selector)。任何method的参数,凡是传入objc_msgSend的,也被他处理为:objc_msgSend(receiver, selector, arg1, arg2, …)
消息函数为动态绑定做了以下事情:
  • 先找到selector引用的方法实现——因为相同的方法可以通过不同的类实现,所以它找到实现的精确过程取决于接收方的类。
  • 然后调用方法实现,给方法实现传递接收对象(一个指向其数据的指针),以及为该方法指定的任何参数。
  • 最后,它传递方法实现的返回值作为其自身的返回值。
注意: 编译器生成messaging function的调用。你不必在代码中直接调用。
消息传递的关键在于编译器为每个类和对象构建的结构。每个类结构包括以下两个基本的元素:
  • 一个指向 superclass的指针。
  • 一个调度表。该表具有将方法选择器method selector与它们识别的方法的类的特定地址相关联的条目。OC中的setOrigin::方法的selector和实现setOrigin::的地址关联;display method的selector和 display的地址关联。
当一个新对象创建的时候,内存已经初始化,他的实例变量也初始化。在这个对象变量的首位是一个指向他类结构的指针。这个指针就是 isa 指针,让对象方位到他的类而且通过这个类能找到所有这个类的继承来源。
注意:尽管不是该语言的一部分,但isa指针是一个对象与Objective-C运行时系统一起使用所必需的。 一个对象需要与一个struct objc_object(在objc / objc.h中定义的)在这个struct定义的任何字段中等效 如果你很少需要创建自己的root object,那么从NSObjectNSProxy继承的对象自动具有isa变量。
这些类的元素和对象结构表示如下:
当消息发送给对象时,messaging function跟随这个对象的isa指针指向到的类结构,并且messaging function在这个类结构的调度表中查找 method selector。如果在这个类结构里找不到selectorobjc_msgSend会跟随指向superclass的指针,并尝试在superclass的调度表中找selector。如果连续失败,会导致objc_msgSend一直在类层级中向上寻找,一直找到NSObject类。一旦它找到了selectormessaging function将调用进入表中的method并将接收对象的数据结构传递给他。
这是在运行时如何选择(找到)方法实现的方式 – 或者,在面向对象编程的术语中,方法(负责实现的)是动态绑定到消息(负责调用的)的
为了加速消息传递过程,运行时系统在使用方法时缓存selector和实现method的地址。每个类都有一个单独的缓存,它可以包含继承method的selector以及类中定义的method。在搜索调度表之前,消息传递例程首先检查接收对象类的缓存(理论上曾经使用过的方法可能会再次使用)。如果方法选择器在缓存中,则消息传递仅比函数调用慢一点。一旦程序运行了足够长的时间来让缓存“热身”,几乎所有发送的消息都会找到一个缓存的方法。程序运行时,缓存会动态增长以适应新的消息。
使用隐藏的参数
当 objc_msgSend 找到方法实现的步骤,他会调用实现并且给方法实现传递所有参数。他也会传递两个隐藏参数给这个实现,这两个参数是:接收对象,methodselector
这些参数给每个方法实现明确的、关于调用它的消息表达式的两部分信息。这两个参数称为“隐藏”因为他们不在一定义的method的源码中公开。他们在编译时插入到实现中。
虽然参数不是显示公开的,源码依然能引用他们(和源码引用接收对象的实例变量一样)。方法将接收对象称为self,他自己的选择器是_cmd下面的例子中_cmd指代Strange方法,self指代接收strange消息的对象:
– strange
{
id  target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
self是两个参数中比较常用的那个。事实上,self是接收对象的实例变量对method 定义起作用的一种方式。
得到一个方法的地址
避免动态绑定的唯一方法就是得到一个方法的地址,如果是一个函数就直接调用他。这在某些少量场景中使用,比如一个特殊的方法会被调用很多次,你想避免每次方法执行是都进行消息传递时。
用在NSObject中定义的methodForSelector:方法,你可以求得一个指向实现方法步骤的指针,然后使用这个指针来调用实现步骤。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);
上面例子中,前两个传递给方式实现的参数是:接收对象(self)和method selector(_cmd).这些参数在method愈发中是隐藏的,但是在方法作为函数调用时,必须被显示标明。
使用 methodForSelector 方法来避免动态绑定省去了messaging需要的时间,但是,只有当一个特定的消息被重复多次时,节省的成本才会显著,比如在 for 循环中。
注意,methodForSelector是Cocoa runtime系统的,不是OC语言的一个feature。
动态方法解析 Dynamic Method Resolution
接下来描述你如何动态的提供一个方法实现。
Dynamic Method Resolution
有些情况下,当你想要动态提供一个方法实现时。比如,OC中公开的properties feature中包含的 @dynamic 指令:
@Dynamic propertyName;
这个语句,告诉编译器和property关联的方法会动态的提供。
你可以实现 resolveInstanceMethod:resolveClassMethod: 方法来动态的分别为一个实例方法和类方法为一个给定的selector提供一个实现。
一个OC方法是一个简单的C函数,接收至少两个参数——self和_cmd. 你可以使用 函数 class_addMethod 给一个类添加一个函数作为method. 因此,有以下函数:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ….
}
你可以用 resolveInstanceMethod 动态的添加上面的函数给一个class作为method(调用 resolveThisMethodDynamically),如下:
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
  if (aSEL == @selector(resolveThisMethodDynamically)) {
    class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP,     “v@:”);
    return YES;
  }
  return [super resolveInstanceMethod:aSEL];
}
@end
方法转发(在Message Forwarding中描述)和动态方法解析很大程度上是有交集的,可能会互相影响。一个类在转发机制介入前有机会动态解析一个method。如果 respondsToSelector:  或者 instancesRespondToSelector: 被调用了, 这个动态方法解析被赋予了一个首先为selector提供IMP指针的机会。如果你实现  resolveInstanceMethod: 但是想要特别的selector来实际被转发机制转发,你应该为respondsToSelector:instancesRespondToSelector:返回NO。
动态加载 Dynamic Loading
一个OC程序可以在运行时加载了连接心得类和category。新代码被合并到程序中,并且相同的处理给先前加载的类和category。
动态加载可以被用于做不同的事,比如System Preferences application的许多模块都是同台加载的。
在Cocoa环境,动态加载一般用于允许app被定制。其他人可以编写程序在运行时加载的模块 – 就像Interface Builder加载自定义调色板一样,OS X系统偏好设置应用程序加载自定义首选项模块。可加载模块扩展了你的应用程序可以执行的操作。他们以你允许的方式为其作出贡献,但无法预估或定义自己。你提供了框架,但其他人提供了代码。
尽管在Mach-O文件(objc_loadModules,在objc / objc-load.h中定义)中有一个运行时函数执行动态加载Objective-C模块,但Cocoa的NSBundle类为动态加载提供了更方便的接口—— 一个是object-oriented并与相关服务相结合。有关NSBundle类及其用法的信息,请参阅Foundation框架参考中的NSBundle类规范。
消息转发 Message Forwarding
发送一个消息给一个对象,这个对象如果不处理这个消息会出现一个error。但是,在发布error前,Runtime system让接收着有第二次处理消息的机会。
转发 Forwarding
如果你发送一个消息给一个不处理消息的对象,在广播 error前,runtime 发送给对象一个 forwardInvocation: 消息,带着一个NSInvocation 对象作为它唯一的参数。这个NSInvocation对象包含了原始的消息和传递给消息的参数。
你可以实现 forwardInvocation方法来给予一个默认的对message的响应,或者来其他避免产生error的方式。就像名字提及的,forwardInvocation普遍用于转发消息给其他对象。
要了解转发的范围和意图,可以想象以下情况:首先,假设你设计的对象可以响应名为negotiate的消息,并且你希望其响应包含另一种对象的响应。 你可以通过将negotiate消息传递给你实施的negotiate 方法主体中的其他对象来轻松实现此目的。
进一步考虑这一点,并假设你希望对象对negotiate消息的响应完全是在另一个类中实现的响应。 一种方法可以让你的类继承另一个类的方法。 但是,这样安排可能是不可能的。 因为你的类和实现negotiate的类在继承层次结构的不同分支中。
即使你的类不能继承negotiate方法,你仍然可以通过实现一个只将消息传递给另一个类的实例的方法来“借用”它:
– (id)negotiate
{
  if ( [someOtherObject respondsTo:@selector(negotiate)] )
    return [someOtherObject negotiate];
  return self;
}
这么做可能会有一点笨重,特别是如果有很多消息你想要你的对象传递给其他对象。你必须实现一个method来涵盖你想要从其他class借用的每个方法。此外,你不可能处理你不知道的情况,在你编写代码的时候,你可能想要转发的全部消息集合。 该集合可能取决于运行时的事件,并且可能随着未来实现新的方法和类而改变。
说了这么多负面的情况,那么forwardInvocation:message提供的第二个机会为上面的问题提供了一个非临时的解决方案,而且是一个动态而非静态的解决方案。 它的工作原理如下:当一个对象由于没有匹配消息中选择器的方法而无法响应消息时,运行时系统通过发送forwardInvocation:消息来通知对象。 每个对象都从NSObject类继承forwardInvocation:方法。 但是,NSObject的方法版本内只是调用了doesNotRecognizeSelector :. 通过覆盖NSObject的版本并实现自己的版本,你可以利用forwardInvocation:消息提供的将消息转发给其他对象的机会。
要转发一个message,所有 forwardInvocation 方法要做一下两点:
  • 决定message要去的位置
  • 带着原有的参数发送message
message可以带着 invokeWithTarget method发送:
– (void)forwardInvocation:(NSInvocation *)anInvocation
{
  if ([someOtherObject respondsToSelector:
      [anInvocation selector]])
    [anInvocation invokeWithTarget:someOtherObject];
  else
    [super forwardInvocation:anInvocation];
}
被转发的消息的返回值将返回给原始发送者。 可以将所有类型的返回值传递给发送者,包括ID类型,结构和双精度浮点数。
forwardInvocation:方法可以充当未识别消息的分发中心,将它们分发给不同的接收者。 或者它可以是一个中转站,将所有消息发送到相同的目的地。 它可以将一条消息翻译成另一条消息,或者简单地“吞下”一些消息,因此没有响应,也没有错误。 forwardInvocation:方法也可以将多个消息合并为一个响应。 forwardInvocation:所做的事由实现者负责。 然而,它提供的连接转发链中对象的机会为程序设计提供了可能性。
注意:forwardInvocation:方法只有在它们调用不到接收方中的现有方法时才会处理消息。例如,如果你希望你的对象将negotiate消息转发给另一个对象,则它不能拥有自己的negotiate方法。如果是这样,该消息将永远不会达到forwardInvocation :
转发和多继承
转发模拟了多继承,并可用于将多重继承的一些影响借给Objective-C程序。下图显示了,一个通过转发来响应消息的对象似乎借用或“继承”了另一个类中定义的方法实现。

在此图中,Warrior类的实例将negotiate消息转发给Diplomat类的实例。Warrior似乎会像Diplomat一样进行negotiate。它似乎对negotiate信息作出了回应,并且出于所有实际目的,它确实做出了回应(尽管它确实是一名正在从事这项工作的Diplomat)。
因此转发消息的对象从继承层次的两个分支——它自己的分支和响应该消息的对象的分支“继承”方法。在上面的例子中,看起来好像Warrior类继承自Diplomat,就像继承自自己的超类一样。
转发提供了您通常希望从多重继承中获得的大部分功能。然而,两者之间有一个重要的区别:多重继承在单个对象中组合了不同的功能。它倾向于大型,多面的对象。另一方面,转发将单独的责任分配给不同的对象。它将问题分解成更小的对象,但以对消息发送者透明的方式关联这些对象。
替代对象Surrogate Objects
转发不仅可以模拟多重继承,还可以开发表达或者“涵盖”更多实体对象的轻量级对象。“替代”代表另一个对象并向其发送消息。
Objective-C编程语言中“Remote Messaging” 中讨论的代理就是这样的代理。代理proxy负责将消息转发到远程接收方的管理细节,确保参数值在连接中被复制和检索,等等。但它并不试图做其他事情;它不会复制远程对象的功能,而只是给远程对象一个本地地址,这是一个可以在另一个应用程序中接收消息的地方。
其他种类的代理对象也是可能的。例如,假设你有一个处理大量数据的对象 – 也许它会创建一个复杂的图像或读取磁盘上文件的内容。设置这个对象可能会非常耗时,所以您宁愿懒惰地去做 – 当它真的需要时或系统资源暂时闲置时。同时,为了使应用程序中的其他对象正常工作,您至少需要此对象的占位符。
在这种情况下,你初始化的创建,不是创建完整的对象,而是是一个轻量级的替代品。这个对象可以自己做一些事情,比如回答有关数据的问题,但大多数情况下它只是为更大的对象提供一个占位,并且在时机到了时将消息转发给它。当代理的forwardInvocation:方法首先接收到发往另一个对象的消息时,它将确保该对象存在并且如果它不存在那么创建该消息。对于较大对象的所有消息都通过代理,所以就程序的其余部分而言,代理和较大的那个对象将是相同的。
转发和继承
机关转发模拟了继承,NSObject类不会混淆两者。类似respondsToSelector: and isKindOfClass:的方法仅在继承层级中查找,不会在转发链中查找。比如,如果一个Warrior对象被请求它是否响应 negotiate 消息,代码如下:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
结果是NO,尽管它能接受 negotiate消息,而且能响应而且没有error,在场景中,转发他们给一个Diplomat对象。
在其他情况,NO 是正确答案。但也可能不是,如果你使用转发来设置代理对象或者扩展一个class的兼容性,那么转发机制应该可能尽可能的比继承透明。如果你想让你的对象表现得好像他们真的继承了他们转发信息的对象的行为,你需要重新实现 responseToSelector和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:之外,instancesRespondToSelector:方法还应该对应转发策略。 如果使用protocol,同样应该将conformsToProtocol:方法添加到列表中。 同样,如果一个对象转发它接收到的任何远程消息,它应该有一个某种版本的methodSignatureForSelector:它可以返回最终响应转发消息的方法的准确描述;。例如,如果某个对象能够将消息转发给其代理,则可以实现methodSignatureForSelector:如下所示:
– (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
  NSMethodSignature* signature = [super methodSignatureForSelector:selector];
  if (!signature) {
    signature = [surrogate methodSignatureForSelector:selector];
  }
  return signature;
}
你可能考虑把转发策略放到某种私有代码位置,并在所有代码里包含forwardInvocation而且调用他。
注意:这是一个高级技巧,仅适用于没其他方法可用时。他不会代替继承。如果你必须使用这个技术,确定你完全明白class做的转发行为和你要转发的类。
类型编码 Type Encodings
为了协助运行时系统,编译器将字符串中每个方法的返回值和参数类型进行编码,并将字符串与method selector相关联。 它使用的编码方案在其他情况下也很有用,因此可以通过@encode()编译器指令公开获得。 当给定类型规范时,@encode()返回一个编码该类型的字符串。 该类型可以是基本类型,如int,指针,标记的struce或union,或者class name ——实际上可以用作C的 sizeof()运算符参数的任何类型。
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
关于编码,不再继续翻译,详情参阅 https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1
公开的属性 Declared Properties
当编译器遇到属性声明时,它会生成与封装类、category或protocol相关联的描述性元数据。你可以访问此元数据——通过使用支持在类或协议上按名称查找属性;以@encode字符串形式获取属性类型;将属性属性列表复制为C字符串数组的函数。 已声明属性的列表可用于每个类和协议。
属性类型和函数
property结构定义了一个不透明处理属性描述符。
typedef struct objc_property *Property;
你可以使用 class_copyPropertyList和protocol_copyPropertyList函数来处理一个和类或者categories或者协议关联的属性数组。
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
比如,给定一下类定义:
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以得到一个属性列表,使用:
id LenderClass = objc_getClass(“Lender”);
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以使用property_getName函数来发现property的name:
const char *property_getName(objc_property_t property)
你可以使用函数 class_getProperty和protocol_getProperty来得到一个property的引用,使用一个给定名称:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以打印一个和class关联的属性列表,如下:
id LenderClass = objc_getClass(“Lender”);
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, “%s %s\n”, property_getName(property), property_getAttributes(property));
}
接下来介绍了 属性类型字符串,不再翻译,详情参考:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1

留下评论

Your email address will not be published. Required fields are marked *