Objective-C Runtime 指南(1)

来源:互联网 发布:中国电商发展现状知乎 编辑:程序博客网 时间:2024/05/19 12:15

Introduction

    Objective-C语言为动态特性做了很多努力,它尽量将决定从编译和链接推迟到运行时刻。这意味着它不仅需要一个编译器,还需要一个运行时。运行时就像是一个保证Objective-C正常运行的小操作系统。

    这篇文档介绍了NSObject以及Objective-C 程序如何同runtime交互。举例说明了如何动态加载一个类,如何将message发送到另外一个对象以及如何在运行时获取一个类的元信息。

    阅读本文你将了解runtime是如何工作的,还有如何利用runtime所提供的功能。

本文的组织
  • Runtime 的版本和平台
  • 与Runtime交互
  • 消息(Messaging)
  • 动态方法决议(Dynamic Method Resolution)
  • 消息路由(Message Forwarding)
  • 类型编码(Type Encodings)
  • 声明属性(Declared Properties)

Runtime 的版本和平台

与Runtime交互

    Objective-C程序与Runtime的交互有三种方式:通过Objective-C源代码;通过Foundation framework 中NSObject的方法;直接调用Runtime方法


1. 通过Objective-C源代码方式与Runtime交互:

大部分时间,Runtime自动的在后台工作。你只需要写下Objective-C的源代码并编译即可。

    当你编译Objective-C类和方法的时候,编译器自动生成实现语言动态特性的结构体和方法调用。编译器生成的结构体中提取了Objective-C类的类别(category)和协议(protocol)信息,方法选择器(method selectors),实例变量模版(instance variable templates)等等。Runtime的一个主要功能是发送消息,源代码中的消息表达式的调用会激发一次消息发送,在后边的Messaging章节中会介绍。

2. 通过NSObject方法的方式与Rumtime交互:

    Cocoa的大部分类都是NSObject的子类,因此大部分Cocoa类都继承了NSObject的方法(一个著名的例外是NSProxy,在Message Forwarding章节会介绍)。都可以使用NSObject的方法与Runtime交互。其中有一些方法NSObject只提供了模板,具体内容需要子类自己实现。

    例如,NSObject定义了一个description的方法,返回一个描述类内容的NSString字符串,主要用于调试功能。但是NSObject类并不知道用户定义的类中会有什么内容,所以在NSObject的实现中仅仅返回类的名字和地址。子类需要根据自己的情况重写这个方法以提供更详细的信息。例如,NSArray类返回数组中每一个对象的description。

    一些NSObject方法只是简单向Runtime查询信息,这些方法与允许对象自省(introspection)。这方面的例子有:class(获取对象的类信息);isKindOfClass 和 isMemberOfClass(判断对象在继承层次中的位置);respondToSelector(判断一个对象是否能相应某个消息);conformsToProtocol(判断一个对象是否遵守某一个协议);methodForSelector(返回方法的实现的地址)。

3. 通过Rumtime方法交互:

    Runtime System是一个动态库,提供了一系列的接口和结构,它的头文件在/usr/include/objc。很多函数允许用户重新定制编译器的行为。另外函数是NSObject各种功能的基础。通过使用这些函数,你可以定义自己的NSObjet或者开发IDE工具。如果仅仅是使用Objective-C编写程序,大部分情况下你不需要使用它们。


Messaging

    这一节主要介绍一个Objective-Cmessage表达式是如何转换为obje_msgSend函数调用的,以及用户如何通过名字找到函数。之后介绍了如何利用obje_msgSend绕过动态绑定。

1. objc_msgSend 函数

在Objective-C中,消息是在运行时候与方法绑定的。编译器将消息表达式:

                [receiver message]

转换为一个消息方法的调用:

                objc_msgSend(receiver, selector)

并传递所有的参数:

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

在objc_msgSend函数中,做了动态绑定所需要的所有工作。

  • 首先,找到方法实现过程。由于同一方法会被继承体系中不同的类分别实现,因此要找到真正要调用的函数的地址。
  • 调用该函数,并传递接收对象指针(self)以及所有参数。
  • 最后将函数的返回值返回给调用者

注意: objc_msgSend函数的调用是编译的转换出来的,用户在任何时候都不应该在代码中调用这个函数。

    能够找到要调用的函数的地址的关键在于编译器为每个类生成的一个类结构体。这个类结构体中包含了两个关键的内容:

  • 一个指向 基类 的指针
  • 一个类分发列表(dispatch table)。这个列表保存了方法选择器和函数地址的关联。如。setOrigin::方法选择器和setOrigin方法的地址,display选择器和display函数地址等等。

当创建一个新的对象的时候,会分配内存,初始化成员。对象的第一个成员变量是一个指针,指向它的类结构体,这个指针就是isa。对象可以通过isa指针访问到它的类结构体,进而访问它的基类

注意:在Objective-C语言中,一个对象要和Objective-C Runtime交互,isa指针不是必须的。但是,字段中必须有一个和struct objc_object“等价”的结构体(因此你可以定义自己的“NSObject”)。很少情况下需要创建自己的root object,所有继承自NSObject和NSProxy的类自动含有isa变量。原文如下:



类和对象的结构元素:


当消息被发送到对象的时候,消息函数通过isa指针找到对象对应的类结构体,然后在分发列表中(dispatch table)搜寻函数选择器(method selector)。如果没有找到选择器,objc_msgSend通过类结构中指向基类的指针找到基类,然后在基类的分发列表中搜寻函数选择器。如此下去,一直上溯到NSObject类中。一旦找到方法选择器,objc_msgSend就调用方法,并将接收消息的对象作为参数传递进去。


上边说的就是runtime如何根据对象的实际类型选择方法的,——或者,用面向对象的术语讲就是方法被动态的绑定到消息上。


为了加快消息的处理过程,runtime system一旦调用过某个方法就缓存方法的地址。每一个类都有不同的缓存,包含了继承而来的方法和自己定义的方法。在搜寻分发列表之前,消息路由先检查缓存,如果缓存中已经存在消息选择器,就直接调用,这样消息的处理就仅仅比函数的调用多一点点开销。因此,一个程序运行一段时间以后(warm up),几乎所有的方法都已经在缓存中了。性能就会大大提升.


2. 使用隐藏参数

当objc_msgSend 找到实现方法的过程(地址),将调用方法过程,并将所有的参数传递过去。其中有两个隐藏参数:

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

这两个参数并没有直接在源代码中定义,但是源代码却可以直接使用它们。使用self引用接收消息的对象自身,使用_cmd引用方法选择器。下边这个例子在Strange方法中引用了self和_cmd:

- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}


3.获取一个方法的地址:

绕过动态绑定的唯一方式是获取方法的地址并直接调用,这种做法比较少见,然而,如果需要在循环中反复发送一个消息,可以通过这个方法提高性能。

在NSObject中有一个methodForSelector: 方法,通过这个方法可以得到一个函数指针,通过函数指针来直接调用函数。但是,你必须小心的进行类型转换,使函数指针的类型和实际函数的类型一致。下边是一个使用函数指针直接调用函数的例子:


在发送消息时的隐藏参数:self和_cmd此时必须显示的传递给函数调用。
使用methodForSelector:绕过动态绑定的方法能够节省发送消息的是时间开销,但是也只有在大量调用的时候才会有显著的性能提高,因此上边的例子是一个正确的做法。
注意:methodForSelector是Cocoa runtime system提供的方法,不是Objective-C语言的特性。




原创粉丝点击