现在的位置: 首页 > 综合 > 正文

OC运行时编程指南

2018年03月20日 ⁄ 综合 ⁄ 共 5711字 ⁄ 字号 评论关闭

本文转自原文地址

OC运行时编程指南

介绍

OC这个语言尽可能的将一些决定从编译和链接时推迟到运行时。它会尽可能的动态的处理事情。这意味这个语言 

不仅需要一个编译器,还需要一个运行时系统去执行编译过的代码。这个运行时系统扮演着对于OC这个语言操作系统的的角色,使得这个语言得以运行。

这个教程将探究NSObject这个类以及OC这个语言和运行时系统是如何进行交互的。特别是如何在运行时动态的根据类的范式加载类,并且传递给其他对象。同时这个教程还将告诉你如何在你的程序运行过程中找到你的对象的运行信息。

本教程的组织方式

本教程由如下几个部分组成:

更多

OC运行时编程指南描述了OC运行时库所支持的数据结构以及函数。你可以在自己的程序中使用这些接口和OC运行时系统进行交互。例如,你可以在运行时添加类和方法,或者取得已经加载的所有类的信息。

运行时系统的版本和平台

在不同的平台下存在着不同版本的OC运行时系统。

遗留版本和现在版本

有两个OC的运行时系统——遗留版本和现在版本 。现在版本是基于OC 2.0版本的语言,包含了一系列的心特征。遗留版本的运行时接口是在OC 1.0版本的语言中进行描述的。

比较两个版本的区别主要体现在如下方面:

  • 在遗留版本的运行时系统,如果你改变一个类的实例变量,你必须重新编译它的所有子类。
  • 在现在版本的鱼腥时系统,若果你改变一个类的实例变量,你无需重新编译他的所有子类。

平台

iPhone应用程序以及基于OS X 10.5+的64位应用程序是基于现在版本的运行时系统。

其他应用程序(基于OS X的32位应用程序)使用遗留版本的运行时系统。

和运行时系统进行交互

OC程序和运行时系统可以清晰的分为3个不同的级别: 

1.通过OC源代码。 

2.通过基础框架中NSObject类定义的方法。 

3.直接调用运行时方法。

OC源代码

大部分情况下,运行时系统的工作会在这个场景下自动完成。你只需要编写和编译OC代码就可以使用它。

当你编译的代码包含OC的类和方法时,编译器就会自动的实现语言动态特征的数据结构和函数调用。创建的数据结构回捕获类、类别定义以及协议声明的信息,同时还会捕获源码中的方法选择器,实例变量模板等信息。运行时函数最重要的函数就是负责消息传递的函数,详情参见消息传递这一章节。它是通过源码中的消息语句调用的。

NSObject方法

在Cocoa框架中的大部分对象都是NSObject的子类,所以大部分对象都会继承NSObject类定义的方法。(一个值得关注的例外就是NSProxy class,详情查看消息传递这一章节。)因此,NSObject的方法就“规范”了他所有子类的行为。然而,在少数情况下,NSObject并没有定义一个如何做某件事情的模板,它并没有提供所有必要的代码。

例如,NSObject类提供了一个description的实例方法用于返回这个类内容的一个字符串描述。这个方法主要用途是调试——GDB打印对象的命令(po指令)返回的内容就是这个方法的返回值。NSObject实现了这个方法,但是并不知道这个类到底包含了什么,因此它返回了包含这个类名称以及内存中地址的字符串。所有NSObject的子类都可以实现这个方法并返回类的详细描述,例如,基础框架里面的NSArray类的description就返回它包含所有对象的描述。

有些NSObject类的方法只是简单的查询运行时系统的信息,这些方法使得对象自身能够自己检查自己。例如,class这个方法,作用是查询一个对象所需的类;isKindOfClass:以及isMemberOfClass:是用来测试一个对象在继承链中的位置;conformsToProtocol:是用来检查一个对象是否实现某个协议,类似的这些方法赋予了一个对象可以在运行时检查自己的信息。

运行时函数

运行时系统是一个提供一系列公开函数接口以及数据结构的动态链接库,这些头文件位于/usr/include/objc。许多这些函数允许你使用纯C语言复写当你写OC代码时编译器做的事情。其他形式的接口则是通过NSObject类中定义的一些方法。这些方法是的可以用来实现其他的运行时接口以提高生产效率。但是这些对于OC语言进行编程并非是必须的,但是,少数的运行时函数在一些特殊情况下,对于OC程序还是很有用途的。接下来将介绍这些函数。

消息传递机制

本章描述消息表达式是如何被转换为objc_msgSend方法调用的,以及你如何让通过消息名称调用到方法。然后解释如何利用objc_msgSend,以及在需要的情况下避开动态绑定。

objc_msgSend函数

在OC语言里面,消息直到运行时才呗绑定为(C语言)方法。编译器会转化一个消息表达式,

?
1
[receiver
message];

成为一次消息传递函数的调用,objc_msgSend。这个函数使用消息中的接收者以及方法对应的选择器作为他的最重要的参数:

?
1
objc_sendMsg(receiver,
selector)

任何在消息中传递的阐述也会被objc_msgSend函数处理:

?
1
objc_sendMsg(receiver,
selector, arg1, arg2, ...)

消息传递函数为动态绑定提供所有必要的内容:

  • 首先,它找到选择器调用的过程(方法实现)。由于同一个方法在不同的类中可能有不同的实现,这个精确的调用过程依赖于接收者所属于的类。
  • 然后,它会调用这个过程,传递接收者对象(一个指向其数据的指针),以及消息中定义的那些参数。
  • 最后,它传递过程调用的返回值作为它自身的返回值。

注意:编译器会自动调用消息传递函数。你不应该在自己的代码中直接调用该方法。


消息传递函数所使用的关键字隐藏在编译器编译生成的每个OC类和对象的结构体中。每一个类结构都包含如下两个本质的元素:

  • 一个指向父对象的指针。
  • 一个类消息分发表。这个表拥有把方法选择器和类中方法的内存地址联系起来的入口。例如,setOrigin::方法的的选择器是和该方法setOrigin::实现的入口地址进行关联的,display方法的选择器是和display方法的地址关联的,等等。

一个新对象一旦被创建,就会被分配内存,并且他的实例变量会被初始化。这些对象变量中的第一个就是指向该对象对应的类结构的指针。这个指针,叫做isa,给予这个对象访问其类信息,然后通过这个类,访问所有它继承自类的类信息。


注意:严格意义上来说,OC运行时系统依赖的isa指针并不是语言的一部分。在OC当中,一个对象需要“等于”一个以任何形式定义的objc_object结构体(在objc/objc.h中定义)。但是,你几乎不需要自己创建你自己的根对象,继承自NSObject或者NSProxy的对象回自动包含isa指针。


这些类元素以及对象结构通过一下的图片进行说明。

消息传递框架

当一个消息被传递到一个对象,消息传递函数会沿着对象的isa指针找到其类结构以查找方法选择器以及消息分发表。如果不能找到方法选择器,消息传递函数回继续查找其父类的指针查找方法选择器以及消息分发表,这个过程知道查找到继承树的根为止。一旦查找到对应的选择器,这个函数就会调用分发表里对应的方法地址,传递飞接收者对象的数据结构。

这就是运行时选择方法调用的方式,或者装逼点来说,在面向对象程序设计中,消息动态绑定到方法的方式。

为了加速这个消息调用的过程。运行时系统会缓存它们使用过的选择器以及起对应的方法地址。这些缓存是针对每个类单独缓存的,当然,这其中包含它自己定义的方法以及继承自父类的方法。在搜索分发表之前,消息传递路由会先检查接收消息接收类的缓存(基于如果干过一次就很可能还会干下一次的理论)。如果消息选择器存在在缓存中,消息传递只比直接的函数调用慢那么一点点。一旦一个程序已经跑了足够长的时间以“预热”他的缓存,几乎所有发送的消息就都可能被缓存了。缓存是根据程序运行时新消息的调用动态增长的。

使用隐藏参数

一旦objc_msgSend方法发现一个方法实现的调用过程,它就会调用改过程,并且传递消息中定义的所有变量。而且它还会传递两个隐藏变量:

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

这些参数使得每个方法的实现可以知道调用者和接受者的明确信息。之所以说他们是“隐藏”的,是因为他们并没有显式的声明在方法里面。它们是在代码编译时被安插到实现里面去的。

尽管这些参数并没有被明确的声明,源代码还可以引用到它们(就像它可以引用到接受者对象的实例变量一样)。一个方法引用接收者对象使用self变量,使用cmd变量引用其选择器。下面的例子中,cmd引用strange方法的选择器,self变量指向接收strange消息的对象。

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

( target == self || method == _cmd)
        return

nil;
    return

[target performSeletor:method];
}

self变量是两个变量中比较重要的一个。事实上,可以通过它访问到消息接收者的实例变量。

获取一个方法的地址

唯一一种避开动态绑定的方法是获取一个方法的地址然后直接按照函数的方式调用它。这种方式的一个使用场景是一个特殊的方法需要频繁被调用并且你希望避免每次的一连串的消息传递。。。

通过在NSObject中定义的methodForSelector方法,你可以获得一个指向方法实现入口的指针,然后通过这个指针调用该过程。methodForSelector返回的指针应该小心的被转化为合适的函数指针。返回值以及参数类型都要一一对应。

接下来的例子serFilled:消息对应的方法调用过程的实现:

?
1
2
3
4
5
6
7
void

(*setter)(id, SEL, BOOL);
int

i;
 
setter
= (
void

(*)(id, SEL, BOOL))[target methodForSelector:
@selector(setFilled:)];
for

(
int

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

该函数调用的前两个参数分别是接收者对象(self)以及消息选择器(_cmd)。这些参数被隐藏在方法语法中,但是必须明确的给出当方法通过函数的方式调用。

使用methodForSelector:可以绕过动态绑定以节约消息传递的时间。然而,这种节约只有在一些特殊的消息需要被频繁调用多次的情况,就像上面展示的for循环中。


注意methodForSelector是Cocoa运行时系统提供的,并不是OC这个语言的一部分。


动态加载机制

本章将介绍泽呢样动态的实现一个方法。

动态加载机制

编码过程中,你可能会遇到动态提供一个方法实现的时候。例如,OC语言中声明为@dynamic的属性:

?
1
@dynamic

propertyName;

告诉编译器和这个属性关联的方法会被动态的提供。

你可以通过实现resolveInstanceMethod:以及resolveClassMethod:方法以动态的实现一个指定实例的选择器或者类方法。

一个OC的方法简单意义上来说就是一个至少有两个参数的C函数——self以及_cmd。你可以使用class_addMethod方法给一个类添加一个方法。然后,通过下面的方式:

?
1
2
3
4
void

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

你可以动态的给一个类添加一个方法(称为resolveThisMethodDynamically)使用resolveInstanceMethod:像这样子:

?
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:或者instancesRespondToSelector:被调用,动态加载机制会有机会首先给选择器提供一个IMP。如果你实现了resolveInstanceMethod:,但是希望一些选择器实际上通过转发机制加载,你可以选择将这些选择器返回NO。

动态加载

一个OC应用程序可以动态的加载和链接新的类和类别。这些新加入的代码回像最初加入的类和类别一样平等的对待。

动态加载可以被用在许多不同的事情上面。例如,许多系统设置程序程序中的模块都是被动态加载的。

在Cocoa环境中,动态加载普遍被应用在个性化应用程序上面。别人可以通过编写你的应用程序在运行时加载的模块——就像IB(Interface Builder)加载自定义的组件和OS X系统设置程序加载自定义设置模块一样。可加载模块扩展了你的应用程序。他们通过你允许的方式但是你并未参与和定义的方式加载。你提供框架,别人提供代码。

尽管有一个OC运行时函数提供动态加载OC模块(objc_loadModules, 定义在objc/objc-load.h中),Cocoa的NSBundle类提供了更方便的动态加载的接口——一个面向对象的并且和相关服务整合的接口。相关内容可以相关参考相关文档。

消息转发机制

【上篇】
【下篇】

抱歉!评论已关闭.