0%

【翻译】Objective-C Runtime Programming Guide

原文地址:Objective-C Runtime Programming Guide

简介

Objective-C 语言将尽可能多的决定从编译期和链接期推迟到运行时。它尽可能的动态化。这就意味着这门语言不仅需要编译器还需要运行时系统来执行它编译好的代码。运行时系统对于 Objective-C 语言来说就像操作系统一样来使它运行起来。

这篇文档着眼于NSObject类和 Objective-C 程序如何与运行时系统交互。重点探讨了在运行时动态加载新类的范例,将消息转发给其他对象。还有对在程序运行时如何查找对象的有关信息提供了相关资料。

你可以通过阅读此文档来了解 Objective-C 运行时系统是如何工作的以及如何利用它。同时,你几乎不需要了解这篇文档就可以写 cocoa 应用了。

不同平台与版本的运行时系统

不同的平台上使用着不同版本的运行时系统。

早期版本与现代版本

现在同时存在着两个版本的运行时系统:“现代版本”和“早期版本”,现代版本是与 Objective-C 2.0以及很多其他特性一起引入的。早期版本的编程接口可以在 Objective-C 1 Runtime Reference 中查看。现代版本的编程接口可以在 Objective-C Runtime这里查看。

现代版本的运行时系统最值得注意的地方在于实例变量是“稳固的”:

  • 在早期版本的运行时系统中,如果你改变了一个类的实例变量的布局,你就得重新编译所有继承于此类的类。
  • 在现在版本中则不需要。

除此之外,现代版本的运行时系统支持已声明属性的实例变量的自动合成。

不同平台的版本

iphone 应用程序和 OS X 10.5 之后的 64 位程序都使用现代版本的运行时系统。

其他程序(OS X 上的 32 位应用)使用了早期版本的运行时系统。

与运行时系统交互

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

Objective-C 源代码

大多数情况下,运行时系统都是在幕后自动工作的,你仅仅需要编写和编译 Objective-C 源代码。

当你编译包含 Objective-C 类和方法的代码时,编译器会创建实现该语言动态特征的数据结构和函数调用。数据结构中会包含类和分类的定义以及协议声明中的信息。其中有《The Objective-C Programming Language》中Defining a Class and Protocols章节中讨论过的类和协议对象,以及方法选择器,实例变量模板及其他从源代码中提取出的信息。最基本的运行时方法是在Messaging中描述的发送消息的方法。它通过源代码中的消息表达式调用。

NSObject 方法

Cocoa 中的大多数对象都是NSObject类的子类,因此他们都继承了NSObject类定义的方法。(一个例外是NSProxy类)这些方法使得每一个实例对象和类对象都有与生俱来的一些能力。然而极少数情况下,NSObject 类只提供了相关方法的模板,并不提供所有的实现代码。

举例来说,NSObject类定义了一个返回类的相关内容的字符串的description实例方法,主要用于调试-GDB的print-object命令就打印这个方法返回的字符串。由于并不知道类中包含什么,所以NSObjectdescription方法的实现只是返回包含对象的名字以及内存地址的字符串。NSObject的子类可以实现这个方法来返回更多信息。比如基础类NSArray返回了它包含对象的列表。

有些NSObject类的方法只是向运行时系统查询相关信息。这些方法允许对象自省。例如,class方法是对象用来确定自己的类的方法;isKindOfClass:isMemberOfClass:是用来检查对象在继承等级中的位置的;respondsToSelector:方法是用来指明一个对象是否可以接受特定消息的;conformsToProtocol:方法用于指明一个对象是否声明要实现相应协议中定义的方法;methodForSelector:方法提供了方法实现的内存地址。这些方法为对象提供了自省的相关能力。

运行时函数

运行时系统是一个有公用接口的动态共享库,接口包含了存储在/usr/include/objc目录下的头文件中的一系列函数和数据结构。它们中的大多数函数允许你在写 Objective-C 代码时使用直白的 C 代码来复制编译器的行为。其他的组成了NSObject类中方法相关功能的基础。这些函数使开发其他运行时系统接口成为可能并生产了提高开发环境能力的工具。这些函数并不是编写 Objective-C 程序是必须的,但是有些函数偶尔会非常有用。这些函数都在Objective-C Runtime Reference中列出了。

发送消息

本章主要描述消息表达式如何转换成对objc_msgSend的函数调用,以及如何通过名字查找方法。然后解释了如何使用objc_msgSend函数,如果需要的话怎么绕过消息的动态绑定。

objc_msgSend 函数

在 Objective-C 中,直到运行时消息才与方法实现绑定在一起。编译器将如下消息表达式:

1
[receiver message]

转换为objc_msgSend消息函数的调用。这个函数将接收者及消息中提及的方法名字也就是方法选择器作为它的两个基本参数:

1
objc_msgSend(receiver, selector)

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

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

这个发送消息函数做了动态绑定的所有工作:

  • 它首先查找到方法选择器所指向的程序(方法实现)。由于不同的类可以对相同的方法提供不同的实现,所以这个准确的方法实现就取决于接收者所属的类。
  • 然后调用这个程序,并传递消息接收者对象(指向其数据的指针)和其他指定的参数。
  • 最后,将程序的返回值作为自己的返回值返回。

注意:编译器会自动调用objc_msgSend这个发送消息函数,你不应该在自己的代码中直接调用它。

发送消息的关键在于编译器为每个类和对象所创建的结构体,每个类的结构体包含了如下两个重要元素:

  • 一个指向其父类的指针。
  • 一个类的分发表。这个表记录了方法选择器和它对应的特属于该类的方法实现的指针之间的关联。如方法选择器setOrigin::setOrgin::方法的实现程序的地址间的相互关联,方法选择器displaydisplay方法的实现程序的地址之间相互关联,等等…

当一个新的对象被创建成功后,内存空间已经开辟好了,它的实例变量也已经初始化完成了,对象的变量中的第一个就是指向其类结构体的命名为isa的指针。通过这个指针,对象可以访问其所属类,通过所属类可以访问它的所有父类。

严格来说isa指针并不是 Objective-C 语言的一部分,但是它确实是运行时系统所必需的。一个对象必须与定义在objc/objc.h中的struct objc_object结构体所定义的等同。任何继承自NSObjectNSProxy的对象都自动包含了这个isa变量,你几乎不需要创建自己的根对象。

类与对象的结构体的元素如下图所示:

Figuire 3-1

当一个消息发送给对象时,objc_msgSend函数顺着对象的isa指针找到对象所属类的结构体,从而在类的分发表中查找方法选择器。如果找不到就顺着指向父类的指针找到父类的结构体并在其中的分发表中继续查找。连续的失败会使objc_msgSend函数顺着类的继承层级一路找到NSObject类。一旦定位到方法选择器之后,这个函数就会调用与之关联的方法实现,并将消息接受对象的数据结构作为参数传递过去。

这就是在运行时决定最终的方法调用的过程,或者用行话来说就是面向对象编程,即方法是动态绑定到消息上的。

为加速发送消息的过程,运行时系统会随着方法的调用建立起方法选择器与方法的实现地址之间的缓存。每一个类都会都单独的缓存,其中不仅包含了类自己定义的方法还有继承的方法。在objc_msgSend函数查找分发表之前,它会例行先查找一下接收者对象所属类的缓存(有一种理论是一个方法在使用之后很快又会被再次使用)。如果在缓存中找到这个方法选择器,那么发送消息过程只会比一般的函数调用慢一丢丢。因此一旦程序运行时间足够长致使缓存系统“热身”后,几乎所有的消息都可以在缓存中找到对应的方法。程序运行时,缓存会自动扩充来容纳新的消息。

使用隐藏参数

objc_msgSend函数查找到实现方法的程序时,它就会调用这个程序并将消息中的参数传递过去。此外还有两个隐藏的参数被传递过去:

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

这些参数给方法表达式调用起的方法实现提供了足够准确的信息。说它们是“隐藏的”是因为在定义方法的源代码中并没有声明它们,它们是在编译时被插入实现代码中的。

尽管这些参数并没有被显示的声明,在源代码中依然可以引用它们(跟引用其他接收者对象的实例变量一样),方法中使用self来引用接收者对象,使用_cmd来引用方法选择器。在下面的例子中,_cmd引用strange方法的选择器,self用来引用接收strange消息的对象。

1
2
3
4
5
6
7
8
9
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();

if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}

这两个参数中self更有用一些,通过它方法中才可以访问接收者对象的实例变量。

获取一个方法的地址

绕过动态绑定的唯一办法是获取到方法的地址,然后像函数一样直接调用它。当你需要连续多次执行某一方法又想要避免每一次都需要发送消息的过程时,绕过动态绑定才可能会稍微合理。

你可以通过定义在NSObject类中的方法:methodForSelector:来获取指向方法实现的程序的指针,然后通过该指针来调用程序。methodForSelector:返回的指针必须被小心的转换成合适的函数类型,参数类型与返回值类型都必须被转换。

下面的例子中展示了setFill:方法的实现程序是如何被调用的:

1
2
3
4
5
6
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 中的已声明的属性使用了@dynamic指令时:

1
@dynamic propertyName;

这个指令告诉编译器与属性相关方法都会被动态的在运行时提供。

你可以通过实现resolveInstanceMethod:为实例方法动态的提供实现程序,实现resolveClassMethod:来为类方法动态的提供实现程序。

一个 Objective-C 方法就是最少有两个参数-self_cmd的简单 C 函数。你可以使用class_addMethod函数来给一个类添加一个函数作为方法。例如对于如下的函数:

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

你可以像下面这样使用resolveInstanceMethod:将它作为一个方法(resolveThisMethodDynamically方法)动态的添加进类中。

1
2
3
4
5
6
7
8
9
10
@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:instancesResponsToSelector:方法被调用时,动态方法解析机制首先会给你提供方法实现IMP的机会。如果你实现了resolveInstanceMethod方法,但是还想要特定的选择器通过消息转发机制被转发,只需要对这些选择器返回NO

动态加载

Objective-C 程序可以在运行时加载和链接新的类和分类,这些代码被合并入程序中并且和启动时加载进的类和分类被同等对待。

动态加载可以做很多事情,举个例子,系统偏好设置里面的各种模块都是被动态加载的。

在 Cocoa 环境中,动态加载普遍被用于应用程序的自定义化。运行时加载其他人编写的模块-诸如 Interface Builer 加载自定义的选项板, OS X 系统偏好设置加载自定义的偏好模块。这些可加载模块可以通过一种你允许的但却不能参与或者定义它的方式来扩展你应用的能力。你只需要提供框架,由其他人来实现代码。(注:本段原文中似乎是以系统为第一人称来表述动态加载的功能,不清楚时可翻看原文)。

除此之外还有运行时函数(定义在objc/objc-load.h中的objc_loadModules)用来动态加载 Mach-O 文件类型的 Objective-C 模块,Cocoa 的 NSBundle 类也提供了更方便的面向对象的集成了相关服务的动态加载方法。查看NSBundle类来获取更多信息。

消息转发

给一个对象发送它不能处理的消息是一个错误。然而在提示错误之前,运行时系统给接收者对象第二次机会来处理这条消息。

转发

给一个对象发送它不能处理的消息时,在提示错误之前,运行时系统会给对象发送带有一个NSInvocation对象(其中封装了原始消息和传入的参数)作为唯一参数的消息forwardInvocation:

你可以通过实现forwardInvocation:方法给消息提供一个默认的响应或者其他操作来避免错误。就像forwardInvocation:这个方法名所暗示的它主要用于将消息转发给其他对象。

为了解转发的意图,想象这样一个场景:假设你在设计一个可以响应negotiate消息的对象,你想在这个响应中包含其他不同对象的响应,你可以简单的通过在negotiate消息的实现代码中把negotiate消息传递给其他对象。

更进一步,假设你想让对象对negotiate消息的响应完全是其他某个类所实现的响应,一种实现办法是使你的类继承于那个类。然而这种方法或许不可能实现,因为你的类与实现negotiate消息的类很大可能属于不同分支的继承链中。

即使你的类不能继承negotiate方法,你仍然可以通过传递消息给其他类的实例来简单的借用它的实现:

1
2
3
4
5
6
- (id)negotiate
{
if ( [someOtherObject respondsTo:@selector(negotiate)] )
return [someOtherObject negotiate];
return self;
}

这种方式比较麻烦,尤其是当你想把一大堆消息传递给其他对象时。你必须为每一个想要从其他类借用的方法实现提供一个方法实现。更甚者,当你编写代码时,你可能并不知道所有想要转发的消息和目标对象,这种情况就难以处理了。这些消息或许是运行时触发的,或许变成未来实现的新的方法和类。

forwardInvocation:消息提供了一种更具有普适性的动态而非静态的方式来为处理这种问题。它是这样工作的:当一个对象因为没有与消息的选择器相匹配的方法而不能响应这条消息时,运行时系统会通过发送一条forwardInvocation:消息来通知该对象。每个对象都会从NSObject类中继承这个forwardInvocation:方法,然而NSObject对这个方法的实现只是简单的调用了doesNotRecognizeSelector:。通过重写这个方法来实现自己的逻辑,你就可以利用forwardInvocation:提供的机会将消息转发给其他对象。

为了转发消息,forwardInvocation:只需要:

  • 决定消息转发给谁。
  • 然后将带有原始参数的消息发送给它。

发送消息可以使用invokeWithTarget:方法:

1
2
3
4
5
6
7
8
- (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 程序中实现一些多重继承的效果。如图 Figure 5-1 所示,对象通过消息转发来实现借用或”继承”其他类中的实现方法来响应消息。

上图中,Warrior类的实例对象将negotiate消息转发给Diplomat类的实例对象。这个勇士就会像外交官一样来谈判,勇士对象似乎可以响应negotiate消息,而且实际上也确实响应了,即使大部分工作是由外交官对象来完成的。

对象通过转发消息来实现从两个不同的继承链分支(自己的继承链和响应消息的对象的继承链)继承,在上面的例子中,Warrior类似乎同时继承了它的父类和Diplomat类。

消息转发提供了你想从多重继承获取的大部分特性,但是两者还有很大区别:多重继承将不同的能力集成与同一对象上,它倾向于实现庞大的多功能的对象。而消息转发会把不同的任务分派给不同的对象,会将问题分拆给小的对象,但是又将这些对象连接起来,使得从消息发送者的角度来看这一切都是透明的。

代理对象

消息转发不仅模仿多重继承,还使得开发轻量级的对象来代表或者“隐藏”其他背后的对象,代理对象作为其他对象的替身将消息过滤给他们。

在《The Objective-C Programming Language》中“Remote Messaging”中讨论的代理就是这样一种代理方式。代理负责将消息转发给一个远程对象(译注:位于不同的地址空间)的各种事务,保证通过这种连接,参数可以被拷贝和取回,它不会做过多的事情,它不会复制远程对象的功能,而是仅仅为它提供一个本地地址,使得存在于另一个应用中的远程对象可以接收到消息。(译注:这一部分不熟悉的话可以去The Objective-C 2.0 Programming Language看一下)

也可以实现其他类型的代理对象,举个例子,假设你有一个要操控很多数据的对象-或许它创建了一个复杂的图片或者读取了磁盘中的文件,创建这个对象会很耗时,所以你倾向于惰性的设置它-即当你真正需要时或者系统资源空闲时。同时你需要一个占位的对象来使得应用中的其他对象正常运行。

在这种情况下,你可以在最初创建一个轻量级的代理对象而不是成熟的对象。这个对象可以完成一些简单的功能,如回答关于数据的问题等,但是主要还是用于为大对象占位置,当时机来临时将消息转发给它。当代理对象的forwardInvocation:消息收到发给其他对象的消息时,它要保证那个对象存在,如果不存在的话就创建一个。大对象的所有消息都通过代理对象。所以对于程序的其他部分来说,代理对象和大对象是相同的。

转发和继承

尽管转发可以模仿继承,但是NSObject类并不会混淆他们。像respondToSelector:isKindOfClass:这样的消息就只会在继承链中查找,绝不会去转发链中查找。举个例子:如果勇士对象被问到是否会响应negotiate消息时:

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

返回值永远是 NO,即使在某种意义上它可以通过转发给外交官对象来无错误的接收并响应negotiate消息。

大多数情况下 NO都是正确的答案,但不总是。如果你使用消息转发来创建一个代理对象或者扩展你类的能力。转发机制应该像继承一样是透明的。如果想要你的对象真的像继承了要转发到的对象的行为时,你需要重写一下respondToSelector:isKindOfClass:方法将你的转发机制考虑在内。

1
2
3
4
5
6
7
8
9
10
11
- (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;
}

除了respondToSelector:isKindOfClass:之外,instancesRespondToSelector:方法也需要反映转发机制。如果使用了协议,conformToProtocol:方法也需要添加进需要重写方法的列表中。相似的,如果对象转发了它收到的远程消息,它应该实现能返回最终响应转发来的消息的方法的确切描述的methodSignatureForSelector:方法。举个例子:如果对象能转发消息给它的代理对象,你要像下面这样实现methodSignatureForSelector:方法:

1
2
3
4
5
6
7
8
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}

你可以将这些有转发机制有关的方法都整理在一起,当有转发消息的时候就调用它。

注:这是个用于其他方法都不能解决的问题的高级技巧,它并不打算替代继承,如果要使用这个技巧,确保你完全了解转发消息的类和接收转发消息的类的行为。

这一部分提到的方法在NSObject文档中可以查阅,可以查询NSInvocation文档来获取invokeWithTarget:方法的相关信息。

类型编码

为协助运行时系统,编译器会将每个方法中的返回值和参数的类型编码成字符串并且和方法选择器联系起来。这种编码方案在其他上下文中也很有用所以通过@encode()编译器指令将其公开化了。给一个特定类型,@encode()会返回一个编码了这个类型的字符串。类型可以是int,指针,有标签的结构体或联合这样的基础类型,或者一个类名,或者任何类型。这个指令可以像 C 中的sizeof()操作符一样用。

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

下表列出了各种类型编码,注意它们中很多会与你编码对象来归档或者分发时的编码重复,但是还有一些编码是你在写一个编码器时不能使用的,还有一些当你为不使用@encode()指令编写一个编码器想使用的编码。(查看NSCoder类获取更多关于编码对象来归档和分发的信息。)

Table 6-1 Objective-C 类型编码

编码 语义
c A char
i An int
s A short
l A long ,l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type…} A structure
(name=type…) A union
bnum A bit field of num bits
^type A 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个指向floats的指针的数组会被编码成:

1
[12^f]

结构体的类型编码被包裹在花括号中,联合被包裹在括号中,结构体编码中先写出标签,然后按顺序列出结构体中的成员的类型,例如:

1
2
3
4
5
typedef struct example {
id anObject;
char *aString;
int anInt;
} Example;

会被编码成:

1
{example=@*i}

不论传入@encode()的是结构体定义的类型名(Example)还是结构体的标签名(example),编码都会返回同样的结果。对于指向结构体的指针来说编码也会携带同样的信息:

1
^{example=@*i}

然而多加一层间接寻址(译注:指针的指针)就会去掉类型的内部信息:

1
^^{example}

对象也会被当做结构体看待,例如将NSObject类名传给@encode()会产生如下编码:

1
{NSObject=#}

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

(译注:尝试如下所示)

1
2
3
4
5
6
7
NSLog(@"%s",@encode(NSObject));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(NSObject *));
//打印结果为:
//{NSObject=#}
//@
//@

注意当在协议中声明方法中有类型限定符时,@encode()指令并不会返回他们,但是运行时系统会使用下表中的额外的编码。

Table 6-1 Objective-C 方法编码

编码 语义
r const
n in
N inout
o out
O bycopy
R byref
V oneway

已声明的属性

当编译器碰到属性声明时(在 The Objective-C Programming Language 的 Declared Properties 章节描述),会生成与封装它的类、分类或者协议相关的描述性的元数据。你可以使用能够通过名字在类或协议中查询属性的函数,获取属性的类型作为一个@encode字符串的函数或者可以拷贝属性特征列表为一个 C 字符串数组的函数来访问这些元数据。每一个类和协议都有一串的已声明的属性可以使用。

属性类型和函数

Property结构体为 property 描述符提供了一个不透明的处理。

你可以使用函数class_copyPropertyListprotocol_copyPropertyList来分别获取类(包含以及加载的分类)和协议相关联的属性的数组。

1
2
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

例如给定如下的类的声明:

1
2
3
4
5
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end

你可以像这样获取属性列表:

1
2
3
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你可以使用property_getName函数来找到属性的名字。

1
const char *property_getName(objc_property_t property)

你可以使用class_getPropertyprotocol_getProperty函数用属性的名字分别从类和协议中获取相应的属性的引用。

1
2
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编码类型的字符串。查看类型编码一章和下面的内容来获取更多类型编码字符串相关的信息。

1
const char *property_getAttributes(objc_property_t property)

将这些代码组合在一起,你就可以打印和这个类相关的属性的列表了:

1
2
3
4
5
6
7
8
9
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));
}
//译注:打印结果:
//alone Tf,V_alone

属性类型字符串

你可以使用property_getAttributes函数来查找一个属性的名字和@encode编码类型的字符串以及其他特性。

这个字符串以T开头,紧跟着@encode编码类型的字符串和一个逗号,以V后面跟着属性背后的实例变量的名字结束,中间是以逗号隔开的各种特性的描述符,如下表所示:

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

编码 语义
R The property is read-only (readonly).
C The property is a copy of the value last assigned (copy).
& The property is a reference to the value last assigned (retain).
N The property is non-atomic (nonatomic).
G The property defines a custom getter selector name. The name follows the G (for example, GcustomGetter,).
S The property defines a custom setter selector name. The name follows the S (for example, ScustomSetter:,).
D The property is dynamic (@dynamic).
W The property is a weak reference (__weak).
P The property is eligible for garbage collection.
t Specifies the type using old-style encoding.

属性特性描述示例

给定下面的定义:

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

下表展示了属性声明及相应的由property_getAttributes:函数返回的字符串:

属性声明 属性描述
@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

修正历史

下表列出了Objective-C Runtime Programming Guide 的修改历史。

时间 记录
2009-10-19 Made minor editorial changes.
2009-07-14 Completed list of types described by property_getAttributes.
2009-02-04 Corrected typographical errors.
2008-11-19 New document that describes the Objective-C 2.0 runtime support library.