个性化阅读
专注于IT技术分析

Objective-C运行时Runtime完全解读

Objective-C语言尽可能地将编译时间和链接时间推迟到运行时。只要有可能,它就会动态地执行操作。这意味着该语言不仅需要一个编译器,还需要一个运行时系统来执行编译后的代码。运行时系统作为Objective-C语言的一种操作系统,它使语言起作用。

本文介绍NSObject类以及Objective-C程序如何与运行时系统交互。特别是,它研究了在运行时动态加载新类和将消息转发给其他对象的范例。它还提供了有关如何在程序运行时查找有关对象的信息的信息。

你应该阅读本文以了解Objective-C运行时系统是如何工作的,以及如何利用它。但是,通常情况下,你没有理由需要了解和理解这些材料来编写Cocoa应用程序。

本文大纲

  • 运行时简介
  • 运行时版本和平台
  • 与运行时交互
  • 消息传递
  • 动态方法解析
  • 消息转发
  • 类型编码
  • 声明属性

运行时简介

Objective-C运行时是一个运行时库,它支持Objective-C语言的动态属性,因此所有的Objective-C应用程序都链接到它。Objective-C运行时库支持函数是在/usr/lib/libobjc.A.dylib的动态共享库中实现的。

在使用Objective-C编程时,通常不需要直接使用Objective-C运行时库。此API主要用于开发Objective-C和其他语言之间的桥接层,或用于低级调试。

Objective-C运行时库的macOS实现是Mac所独有的。对于其他平台,GNU编译器集合提供了具有类似API的不同实现。本文档只涉及macOS实现。

低级的Objective-C运行时API在OS X 10.5版本中得到了显著的更新。许多函数和所有现有的数据结构都被替换为新函数。旧的函数和结构在32位模式下被弃用,而在64位模式下则不支持。即使在64位模式类计数、协议计数、每个类的方法、每个类的ivars、每个方法的参数、每个方法的sizeof(所有参数)和类版本号中,API也将几个值限制为32位int。此外,新的Objective-C ABI(这里没有描述)进一步将sizeof(实例)限制为32位,将其他三个值限制为24位—每个类有三个方法,每个类有三个方法,还有sizeof(单个ivar)。最后,过时的NXHashTable和NXMapTable被限制为40亿项。

字符串编码

应该认为运行时API中的所有char *都具有UTF-8编码。

Objective-C Runtime中提供一些列的函数,这些函数在objc/runtime.h头文件中,如果你需要使用,需要使用import导入该头文件。

运行时版本和平台

在不同的平台上有不同版本的Objective-C运行时。

传统和现代版本

有两个版本的Objective-C运行时——“modern”和“legacy”。现代版本是在Objective-C 2.0中引入的,包含了许多新特性。运行时的遗留版本的编程接口在Objective-C 1运行时参考中有描述;现代版本运行时的编程接口在Objective-C运行时参考中有描述。

最值得注意的新功能是实例变量在现代运行时是“非脆弱的”:

  • 在遗留运行时中,如果更改类中实例变量的布局,则必须重新编译从其继承的类。
  • 在现代运行时中,如果更改类中实例变量的布局,则不必重新编译继承自该类的类。

此外,现代运行时支持声明属性的实例变量合成(请参阅Objective-C编程语言中的声明属性)。

平台

iPhone应用程序和OS X v10.5上的64位程序,以及后来使用的现代版本的运行时。

其他程序(OS X桌面的32位程序)使用运行时的遗留版本。

与运行时交互

Objective-C程序与运行时系统在三个不同的层次上进行交互:通过Objective-C源码;通过在Foundation框架的NSObject类中定义的方法;并通过直接调用运行时函数。

Objective-C源码

在大多数情况下,运行时系统自动地在后台工作。只需编写和编译Objective-C源代码即可使用它。

当你编译包含Objective-C类和方法的代码时,编译器会创建数据结构和函数调用来实现语言的动态特性。数据结构捕获类和类别定义以及协议声明中的信息;它们包括在Objective-C编程语言中定义类和协议时讨论的类和协议对象,以及从源代码中提取的方法选择器、实例变量模板和其他信息。主体运行时函数是发送消息的函数,如消息传递中所述。它由源代码消息表达式调用。

NSObject方法

Cocoa中的大多数对象都是NSObject类的子类,所以大多数对象都继承了它定义的方法。(值得注意的例外是NSProxy类;有关更多信息,请参见消息转发。)因此,它的方法建立了每个实例和每个类对象固有的行为。但是,在一些情况下,NSObject类仅仅定义了一个模板,用于处理应该如何处理的事情;它本身并没有提供所有必需的代码。

例如,NSObject类定义了一个描述实例方法description,该方法返回一个描述类内容的字符串。这主要用于调试——GDB print-object命令打印从这个方法返回的字符串。这个方法的NSObject实现不知道类包含什么,所以它返回一个带有对象名称和地址的字符串。NSObject的子类可以实现这个方法来返回更多的细节。例如,类NSArray返回它所包含对象的描述列表。

一些NSObject方法只是在运行时系统中查询信息。这些方法允许对象执行内省。这类方法的例子是类方法,它要求一个对象识别它的类;isKindOfClass:和isMemberOfClass:,它们测试对象在继承层次结构中的位置;respondsToSelector:表示对象是否可以接受特定的消息;一致性协议:,conformsToProtocol:表示一个对象是否声明要实现在特定协议中定义的方法;以及methodForSelector:,它提供了一个方法实现的地址。这样的方法使对象能够反省自身。

运行时功能

运行时系统是一个动态共享库,具有一个公共接口,该接口由位于/usr/include/objc.目录下的头文件中的一组函数和数据结构组成其中许多函数,允许你使用纯C来复制编译器在编写Objective-C代码时所做的工作。其他的则构成了通过NSObject类的方法导出的功能的基础。这些功能使得开发运行时系统的其他接口成为可能,并产生增强开发环境的工具;在Objective-C中不需要它们。但是,在编写Objective-C程序时,一些运行时函数可能会有用。所有这些函数都记录在Objective-C运行时参考文档中。

消息传递

这里描述如何将消息表达式转换为objc_msgSend函数调用,以及如何通过名称引用方法。然后解释如何利用objc_msgSend,以及如果需要,如何绕过动态绑定。

objc_msgSend函数

在Objective-C中,消息直到运行时才绑定到方法实现。编译器转换消息表达式,

[receiver message]

为——调用消息传递函数objc_msgSend。该函数将接收方和消息中提到的方法的名称(即方法选择器)作为两个主要参数:

objc_msgSend(receiver, selector)

在消息中传递的任何参数也会传递给objc_msgSend:

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

消息传递函数objc_msgSend执行动态绑定所需的所有操作:

  • 它首先找到选择器引用的过程(方法实现)。由于不同的类可以以不同的方式实现相同的方法,因此它找到的精确过程取决于接收者的类。
  • 然后调用该过程,将接收对象(指向其数据的指针)以及为该方法指定的任何参数传递给它。
  • 最后,它将过程的返回值作为自己的返回值传递。

注意:编译器生成对消息传递函数的调用。你不应该在你写的代码中直接调用它。

消息传递的关键在于编译器为每个类和对象构建的结构。每个类结构都包含这两个基本元素:

  • 指向超类的指针
  • 类调度表。此表具有将方法选择器SEL与它们标识的特定于类的方法的地址IMP相关联的项。setOrigin::方法的选择器与(实现它的过程)setOrigin::的地址相关联,display方法的选择器与display的地址相关联,等等。

当创建一个新对象时,为它分配内存,并初始化它的实例变量。首先,对象的变量中有一个指向类结构的指针。这个名为isa(is a)的指针让对象访问它的类,并通过类访问它继承的所有类。

注意:虽然不是语言的严格组成部分,但isa指针是对象与Objective-C运行时系统一起工作所必需的。对象需要在结构定义的任何字段中与结构objc_object(在objc/objc.h中定义)“等效”。但是,很少需要创建自己的根对象,并且从NSObject或NSProxy继承的对象自动具有isa变量。

这些类元素和对象结构如图3-1所示。

图3-1消息传递框架

当消息被发送到一个对象时,消息传递函数遵循对象的isa指针指向类结构,在类结构中查找分派表中的方法选择器。如果它在那里找不到选择器,则objc_msgSend跟随指向超类的指针,并尝试在其分派表中找到选择器。连续的失败会导致objc_msgSend爬上类层次结构,直到它到达NSObject类。找到选择器后,函数调用表中输入的方法,并将接收对象的数据结构传递给它。

这是在运行时选择方法实现的方式—或者,用面向对象编程的术语来说,方法是动态绑定到消息的。

为了加快消息传递过程,运行时系统在使用选择器和方法地址时缓存它们。每个类都有一个单独的缓存,它可以包含继承方法和类中定义的方法的选择器。在搜索分派表之前,消息传递例程首先检查接收对象的类的缓存(理论上,使用一次的方法可能会再次使用)。如果方法选择器在缓存中,消息传递只比函数调用稍微慢一点。一旦一个程序运行了足够长的时间来“预热”它的缓存,它发送的几乎所有消息都会找到一个缓存的方法。缓存在程序运行时动态增长,以适应新的消息。

使用隐藏的参数

当objc_msgSend找到实现方法的实现过程时,它调用该过程并将消息中的所有参数传递给它。它还传递了两个隐藏参数:

  • 接收对象
  • 方法的选择器

这些参数为每个方法实现提供了关于调用它的消息表达式的两部分的显式信息。它们被称为“隐藏的”,因为它们没有在定义方法的源代码中声明。在编译代码时将它们插入到实现中。

虽然没有显式地声明这些参数,但是源代码仍然可以引用它们(就像它可以引用接收对象的实例变量一样)。方法将接收对象引用为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更有用。实际上,它是方法定义使用接收对象实例变量的方式。

获取方法地址

规避动态绑定的惟一方法是获取方法的地址,并像调用函数一样直接调用它。当一个特定的方法将连续执行许多次,并且你希望避免每次执行该方法时的消息传递开销时,这可能是适当的。

使用在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)和方法选择器(_cmd)。这些参数隐藏在方法语法中,但必须在方法作为函数调用时显式显示。

使用methodForSelector:绕过动态绑定可以节省消息传递所需的大部分时间。但是,只有在重复多次特定消息的情况下(如上面所示的for循环),节省的开销才会很大。

注意methodForSelector:是由Cocoa运行时系统提供的,它不是Objective-C语言本身的一个特性。

动态方法解析

这里描述如何动态地提供方法的实现。

动态方法解析

在某些情况下,你可能希望动态地提供方法的实现。例如,Objective-C声明的属性特性(请参阅Objective-C编程语言中声明的属性)包括@dynamic指令:

@dynamic propertyName;

它告诉编译器与属性关联的方法将被动态提供。

你可以实现方法resolveInstanceMethod:和resolveClassMethod:分别为实例和类方法的给定选择器动态提供实现。

Objective-C方法就是一个C函数,它至少包含两个参数self和_cmd。可以使用函数class_addMethod将函数作为方法添加到类中。因此,给出如下函数:

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

你可以使用resolveInstanceMethod将它作为一个方法(称为resolveThisMethodDynamic)动态地添加到一个类中。

@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

转发方法(如消息转发中所述)和动态方法解析在很大程度上是正交的。类有机会在转发机制启动之前动态解析方法。如果调用了respondsToSelector:或instancesRespondToSelector:,则动态方法解析器将有机会首先为选择器提供IMP。如果你实现了resolveInstanceMethod:但是希望通过转发机制实际转发特定的选择器,那么你将为这些选择器返回NO。

动态加载

Objective-C程序可以在运行时加载和链接新的类和类别。新代码被合并到程序中,并与开始时加载的类和类别进行相同的处理。

动态加载可以用来做很多不同的事情。例如,系统首选项应用程序中的各个模块是动态加载的。

在Cocoa环境中,动态加载通常用于允许定制应用程序。其他人可以编写程序在运行时加载的模块—就像Interface Builder加载自定义调色板和OS X系统首选项应用程序加载自定义首选项模块一样。可加载模块扩展了应用程序的功能。他们以你允许的方式贡献,但你自己却无法预料或定义。你提供框架,其他人提供代码。

尽管有一个运行时函数在Mach-O文件中执行Objective-C模块的动态加载(objc_loadModules,在objc/objc-load.h中定义),Cocoa的NSBundle类为动态加载提供了一个明显更方便的接口——一个面向对象的、与相关服务集成的接口。有关NSBundle类及其使用的信息,请参阅基础框架参考中的NSBundle类规范。有关Mach-O文件的信息,请参阅OS X ABI Mach-O文件格式参考。

消息转发

向不处理该消息的对象发送消息是错误的。然而,在宣布错误之前,运行时系统给接收对象第二次机会来处理消息。

转发

如果向不处理该消息的对象发送消息,在宣布错误之前,运行时将向该对象发送一个forwardInvocation:以NSInvocation对象作为惟一参数的消息——NSInvocation对象封装了原始消息和随消息传递的参数。

你可以实现forwardInvocation:方法来为消息提供默认响应,或者以其他方式避免错误。顾名思义,forwardInvocation:通常用于将消息转发给另一个对象。

要查看转发的范围和意图,请设想以下场景:首先,假设你正在设计一个对象,该对象可以响应一条名为negotiate的消息,你希望它的响应包含另一种对象的响应。通过将negotiate消息传递给你实现的negotiate方法体中的其他对象,你可以轻松地完成此任务。

更进一步,假设你希望对象对negotiate消息的响应与在另一个类中实现的响应完全相同。实现此目的的一种方法是让你的类从另一个类继承该方法。然而,以这种方式安排事情是不可能的。你的类和实现了negotiate的类位于继承层次结构的不同分支中可能有很好的理由。

即使你的类不能继承negotiate方法,你仍然可以“借用”它通过实现一个版本的方法,简单地把消息传递给另一个类的一个实例:

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

这样做可能会有点麻烦,特别是如果你希望将大量消息传递给另一个对象时。你必须实现一个方法来覆盖希望从其他类借用的每个方法。此外,在编写代码时,不可能处理你可能想要转发的完整消息集。该集合可能依赖于运行时的事件,并且可能随着将来实现的新方法和类而改变。

转发调用提供的第二次机会:消息为这个问题提供了一个不太特别的解决方案,而且是动态的而不是静态的。它的工作原理是这样的:当一个对象由于没有与消息中的选择器匹配的方法而不能响应消息时,运行时系统通过发送一个forwardInvocation: message来通知该对象。每个对象都继承一个来自NSObject类的forwardInvocation:方法。然而,NSObject版本的方法只是调用了doesNotRecognizeSelector:。通过覆盖NSObject的版本并实现自己的版本,你可以利用forwardInvocation: message提供的机会将消息转发给其他对象。

要转发消息,所有的forwardInvocation:方法需要做的是:

  • 确定消息应该发送到哪里,以及
  • 将它与它的原始参数一起发送到那里。

消息可以通过invokeWithTarget:方法发送:

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

已转发消息的返回值将返回给原始发送者。所有类型的返回值都可以传递给发送方,包括id、结构和双精度浮点数。

forwardInvocation:方法可以充当未识别消息的分发中心,将它们分发给不同的接收者。或者它可以是一个中转站,将所有消息发送到同一个目的地。它可以将一个消息转换成另一个消息,或者简单地“吞下”一些消息,这样就没有响应和错误。forwardInvocation:方法还可以将多个消息合并到一个响应中。什么是forwardInvocation:由实现者决定。然而,它为在转发链中链接对象提供的机会为程序设计打开了可能性。

注意:forwardInvocation:方法只有在消息没有调用名义接收方中的现有方法时才处理消息。例如,如果你希望你的对象将negotiate消息转发给另一个对象,那么它就不能有自己的negotiate方法。如果它这样做,消息将永远不会到达forwardInvocation:。

有关转发和调用的更多信息,请参见基础框架参考中的NSInvocation类规范。

转发和多重继承

转发模拟继承,可以将多重继承的一些效果借给Objective-C程序。如图5-1所示,通过转发消息来响应消息的对象似乎借用或“继承”另一个类中定义的方法实现。

转发和多重继承

在本例中,Warrior类的实例将negotiate消息转发给Diplomat类的实例。Warrior看起来会像Diplomat 一样谈判。它似乎会对negotiate的信息做出回应,而且出于所有实际目的,它确实会做出回应(尽管它实际上是一位Diplomat在做这项工作)。

因此,转发消息的对象从继承层次结构的两个分支“继承”方法——它自己的分支和响应消息的对象的分支。在上面的例子中,Warrior类似乎继承了Diplomat和它自己的超类。

转发提供了你通常希望从多重继承中获得的大多数特性。然而,这两者之间有一个重要的区别:多重继承在一个对象中组合了不同的功能。它倾向于大型、多面对象。另一方面,转发将不同的职责分配给不同的对象。它将问题分解为更小的对象,但以对消息发送者透明的方式关联这些对象。

代理对象

转发不仅模仿了多重继承,它还使开发表示或“覆盖”更多实体对象的轻量级对象成为可能。代理程序代替另一个对象并将消息发送给它。

在Objective-C编程语言的“远程消息传递”中讨论的代理就是这样一个代理。代理负责将消息转发到远程接收器的管理细节,确保跨连接复制和检索参数值,等等。但它并不想做太多其他的事情;它不复制远程对象的功能,而只是给远程对象一个本地地址,一个它可以在另一个应用程序中接收消息的地方。

还可以使用其他类型的代理对象。例如,假设你有一个操作大量数据的对象—它可能创建复杂的图像或读取磁盘上的文件内容。设置这个对象可能很耗时,所以你更喜欢在真正需要它或系统资源暂时空闲的时候惰性地设置它。同时,为了让应用程序中的其他对象正常工作,你至少需要为这个对象预留一个占位符。

在这种情况下,你最初可以创建的不是完全成熟的对象,而是它的轻量级代理。这个对象可以自己做一些事情,比如回答关于数据的问题,但大多数情况下,它只是为较大的对象保留一个位置,当时间到了时,将消息转发给它。当代理的forwardInvocation:方法首先接收到一条发送给另一个对象的消息时,它将确保该对象存在,如果不存在就创建它。较大对象的所有消息都经过代理,因此,就程序的其余部分而言,代理和较大对象是相同的。

转发和继承

虽然转发类似于继承,但NSObject类从不混淆这两者。像respondsToSelector:和isKindOfClass:只看继承层次结构,从不看转发链。例如,如果一个Warrior对象被问到它是否响应一个negotiate消息,

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...

答案是否定的,尽管它可以毫无差错地接收到negotiate信息,并在某种意义上通过将信息转发给Diplomat来回应这些信息。(见图5 – 1)。

在很多情况下,NO是正确答案。但事实可能并非如此。如果你使用转发来设置代理对象或扩展类的功能,则转发机制应该与继承一样透明。如果你想让你的对象表现得好像它们真的继承了它们转发消息的对象的行为,你需要重新实现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:, instancesRespondToSelector:方法也应该镜像转发算法。如果使用协议,同样应该将conformsToProtocol:方法添加到列表中。类似地,如果一个对象转发它接收到的任何远程消息,它应该有一个methodSignatureForSelector:的版本,它可以返回最终响应转发消息的方法的准确描述;例如,如果一个对象能够将消息转发给它的代理,你将实现methodSignatureForSelector:如下所示:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

你可以考虑将转发算法放在私有代码的某个地方,并让所有这些方法(包括forwardInvocation: )调用它。

注意:这是一种高级技术,只适用于没有其他解决方案的情况。它不打算代替继承。如果必须使用此技术,请确保完全理解执行转发的类和要转发的类的行为。

本节中提到的方法在基础框架参考中的NSObject类规范中进行了描述。有关invokeWithTarget:的信息,请参阅基础框架参考中的NSInvocation类规范。

类型编码

为了辅助运行时系统,编译器对字符串中每个方法的返回和参数类型进行编码,并将该字符串与方法选择器关联起来。它使用的编码方案在其他上下文中也很有用,因此可以通过@encode()编译器指令公开使用。当给定类型规范时,@encode()返回该类型的字符串编码。类型可以是基本类型,如int、指针、带标记的结构或union或类名——实际上,任何类型都可以用作C sizeof()操作符的参数。

char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);

下表列出了类型代码。请注意,其中许多代码与你在为存档或分发而对对象进行编码时使用的代码重叠。但是,这里列出的代码在编写编码器时不能使用,而在编写非由@encode()生成的编码器时可能需要使用。(请参阅Foundation Framework参考资料中的NSCoder类规范,以获得关于编码对象以进行归档或分发的更多信息。)

表6-1 Objective-C类型编码

CodeMeaning
cchar
iAn int
sshort
llongl is treated as a 32-bit quantity on 64-bit programs.
qlong long
CAn unsigned char
IAn unsigned int
SAn unsigned short
LAn unsigned long
QAn unsigned long long
ffloat
ddouble
BA C++ bool or a C99 _Bool
vvoid
*A character string (char *)
@对象(不管是静态类型的还是类型化的id)
#A class object (Class)
:方法选择器(SEL)
[array type]An array
{name=type…}A structure
(name=type…)A union
bnumA bit field of num bits
^typeA pointer to type
?An unknown type (among other things, this code is used for function pointers)

重要提示:Objective-C不支持long double类型。@encode(long double)返回d,与double的编码相同。

数组的类型代码括在方括号内;数组中的元素数是在右括号之后、数组类型之前立即指定的。例如,一个包含12个指向浮点数的指针的数组将被编码为:

[12^f]

结构在大括号中指定,联合在圆括号中指定。首先列出结构标签,然后依次列出结构字段的等号和代码。例如,结构

typedef struct example {
    id   anObject;
    char *aString;
    int  anInt;
} Example;

会被编码成这样:

{example=@*i}

不管将定义的类型名(Example)还是结构标记(Example)传递给@encode(),结果都是相同的。结构指针的编码携带关于结构字段的相同数量的信息:

^{example=@*i}

但是,另一个间接层删除了内部类型规范

^^{example}

对象被视为结构。例如,将NSObject类名传递给@encode()将产生此编码

{NSObject=#}

NSObject类只声明了一个类类型的实例变量isa。

注意,虽然@encode()指令不返回它们,但是当在协议中声明方法时,运行时系统使用表6-2中列出的用于类型限定符的附加编码。

表6-2 Objective-C方法编码

CodeMeaning
rconst
nin
Ninout
oout
Obycopy
Rbyref
Voneway

声明属性

当编译器遇到属性声明,它生成描述性元数据与封闭类相关联,类别或协议。你可以访问此元数据使用功能,支持查找一个类的一个属性的名字或协议,获取一个属性的类型作为@encode字符串,并复制一个列表属性的属性作为C字符串数组。声明属性的列表是用于每个类和协议。

属性类型和函数

属性Property结构定义了一个不透明的句柄属性描述符。

typedef struct objc_property *Property;

你可以使用函数class_copyPropertyList和protocol_copyPropertyList来分别检索与类(包括已加载的类别)和协议相关联的属性数组:

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函数来发现属性的名称

const char *property_getName(objc_property_t property)

你可以使用函数class_getProperty和protocol_getProperty来分别获得类和协议中具有给定名称的属性的引用

objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你可以使用property_getAttributes函数来发现属性的名称和@encode类型字符串。有关编码类型字符串的详细信息,请参阅类型编码;有关此字符串的详细信息,请参见属性类型string和属性属性描述示例。

const char *property_getAttributes(objc_property_t property)

将这些属性放在一起,你可以使用以下代码打印与类关联的所有属性的列表

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));
}

属性类型字符串

可以使用property_getAttributes函数来发现属性的名称、@encode类型字符串和其他属性。

字符串以T开头,后跟@encode类型和逗号,以V结尾,后跟备份实例变量的名称。这些属性由下列描述符指定,以逗号分隔:

表7-1声明的属性类型编码

CodeMeaning
R属性是只读的 (readonly).
C属性是最后分配的值的副本(copy).
&属性是对最后赋值的引用 (retain).
N属性是非原子的(nonatomic).
G<name>该属性定义自定义getter选择器名称。名称跟在G后面(例如, GcustomGetter,).
S<name>属性定义自定义setter选择器名称。名称跟在S后面(例如, ScustomSetter:,).
D属性是动态的 (@dynamic).
W属性为弱引用 (__weak).
P该属性可用于垃圾收集。
t<encoding>指定使用旧式编码的类型。

属性的特性描述示例

鉴于这些定义:

enum FooManChu { FOO, MAN, CHU };
struct YorkshireTeaStruct { int pot; char lady; };
typedef struct YorkshireTeaStruct YorkshireTeaStructType;
union MoneyUnion { float alone; double down; };

下表显示了示例属性声明和property_getAttributes返回的相应字符串:

Property declarationProperty description
@property char charDefault;Tc,VcharDefault
@property double doubleDefault;Td,VdoubleDefault
@property enum FooManChu enumDefault;Ti,VenumDefault
@property float floatDefault;Tf,VfloatDefault
@property int intDefault;Ti,VintDefault
@property long longDefault;Tl,VlongDefault
@property short shortDefault;Ts,VshortDefault
@property signed signedDefault;Ti,VsignedDefault
@property struct YorkshireTeaStruct structDefault;T{YorkshireTeaStruct="pot"i"lady"c},VstructDefault
@property YorkshireTeaStructType typedefDefault;T{YorkshireTeaStruct="pot"i"lady"c},VtypedefDefault
@property union MoneyUnion unionDefault;T(MoneyUnion="alone"f"down"d),VunionDefault
@property unsigned unsignedDefault;TI,VunsignedDefault
@property int (*functionPointerDefault)(char *);T^?,VfunctionPointerDefault
@property id idDefault;Note: the compiler warns: "no 'assign', 'retain', or 'copy' attribute is specified - 'assign' is assumed"T@,VidDefault
@property int *intPointer;T^i,VintPointer
@property void *voidPointerDefault;T^v,VvoidPointerDefault
@property int intSynthEquals;In the implementation block:@synthesize intSynthEquals=_intSynthEquals;Ti,V_intSynthEquals
@property(getter=intGetFoo, setter=intSetFoo:) int intSetterGetter;Ti,GintGetFoo,SintSetFoo:,VintSetterGetter
@property(readonly) int intReadonly;Ti,R,VintReadonly
@property(getter=isIntReadOnlyGetter, readonly) int intReadonlyGetter;Ti,R,GisIntReadOnlyGetter
@property(readwrite) int intReadwrite;Ti,VintReadwrite
@property(assign) int intAssign;Ti,VintAssign
@property(retain) id idRetain;T@,&,VidRetain
@property(copy) id idCopy;T@,C,VidCopy
@property(nonatomic) int intNonatomic;Ti,VintNonatomic
@property(nonatomic, readonly, copy) id idReadonlyCopyNonatomic;T@,R,C,VidReadonlyCopyNonatomic
@property(nonatomic, readonly, retain) id idReadonlyRetainNonatomic;T@,R,&,VidReadonlyRetainNonatomic
赞(1)
未经允许不得转载:srcmini » Objective-C运行时Runtime完全解读

评论 抢沙发

评论前必须登录!