Objective-C运行时编程指南

前言

Objective-C这门语言将尽可能多的决定从编译和链接阶段延迟到运行阶段决定。如果有可能,它就动态的处理一些事情。这意味着这门语言不止需要编译系统,也需要一个运行时系统来执行编译的代码。运行时系统就像Objective-C这门语言的一种操作系统;它是这门语言运行的决定性因素。

一:运行时的版本和平台

在不同的平台上有不同的运行时版本

1.遗留版本和现代版本

Objective-C的运行时有两个版本:legacymodern

  • legacy:参考Objective-C 1 Runtime Reference。
  • modern:Objective-C 2.0引入的,包含一些写的特性,参考Objective-C Runtime Reference。支持声明变量的synthesis

2.平台

iPhone应用和OSX10.5及以后版本的64位应用使用modern版本
其它程序(OSX平台的32位程序)使用legacy版本

二:与运行时的交互

Objective-C程序在以下三个层次上与运行时进行交互:

1.Objective-C源码

大多数情况下,运行时系统是在幕后自动运行的。当你编写和运行Objective-C源码的时候其实你就已经在使用它了。
当编译包含Objective-C类和方法的代码时,编译器就创建了实现这门语言动态特性的数据结构和函数调用。数据结构会捕获类和分类定义及协议声明中的信息;这些信息包括类和协议对象、方法选择子、实例变量模板和一些其它从源码中提取的信息。最重要的运行时函数是消息发送函数,这会在下文介绍。这个函数会被源码的消息表达式唤起。

2.NSObject定义的方法

Cocoa中的大部分对象是NSObject的子类,所以大部分对象都继承了NSObject定义的方法(NSProxy是个例外)。它的方法因此建立了适用于每个实例对象和类对象的行为。但是在少数情况下,NSObject类仅仅是定义了一个事情如何做的模板;它本身并不提供所有的必要代码。
例如,NSObject类定义了一个description实例方法来返回一个字符串来描述这个类的内容。它主要用于debugging。NSObject的这个方法的实现并不知道这个类包含的内容,所以它只返回类名和类的地址。NSObject的子类可以重写这个方法来返回更多详细的信息。例如,NSArray返回一组它包含对象的描述。
一些NSObject方法仅仅是向运行时系统询问一些信息。这些方法允许对象执行自省(introspection)。例如:class会请求一个对象来鉴别它的类;isKindOfClass:isMemberOfClass:检测一个对象在继承体系中的位置;respondsToSelector:指明一个对象是否可以接收一条指定消息;conformsToProtocol:指明一个对象是否遵循了指定的协议;methodForSelector:提供了一个方法实现的地址。像以上的这些方法让一个对象有能力进行自省。

3.对运行时函数的直接调用

运行时系统是一个动态的共享库,它包含由一系列函数和数据结构组成的公共接口,见图1。许多函数允许你使用简单的C语言复制编译器在你书写Objective-C代码所做的事情。另外一些则构成了NSObject这个类的功能的基础。这些函数使得开发一些其它的运行时接口变得可能,并能生产一些能增强开发环境的工具。

《Objective-C运行时编程指南》 图1:运行时的头文件

三:消息传递(Messaging)

消息表达式是如何转换成objc_msgSend函数的?如何通过名字指向方法?如何利用objc_msgSend?如何跳过动态绑定?请看下文。

1.objc_msgSend函数

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

[receiver message]

转换成一个对消息函数objc_msgSend的调用。这个消息函数将消息的接收者和消息的方法名(也就是函数的选择子:selector)作为它的两个主要参数:

objc_msgSend(receiver, selector)

消息中的任何参数同样也会传递给objc_msgSend函数:

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

消息函数会为动态绑定做所有必要的事情:

  • 首先,消息函数寻找选择子所指向的procedure(方法实现:method implementation)。因为同样的方法可能会被不同的类实现,所以消息函数寻找的精确的procedure取决于消息接收者这个类。
  • 然后,消息函数调用这个procedure,传入接收对象(一个指向接收对象数据的指针)和方法指定的任何参数。
  • 最后消息函数将这个procedure的返回值作为自己的返回值传入。

注意:编译器会生成消息函数的调用,作为开发者永远不要直接调用这个函数。

消息传递的关键取决于编译器为每个类和对象所构建的结构体。每个类的结构体包括以下两个必要元素:

  • 一个指向父类的指针
  • 一个类派发表。这个表拥有一些条目,这些条目将方法选择子和他们识别的指定类的方法的地址绑定起来。例如:setOrigin::方法的选择子和setOrigin::的地址绑定起来,display方法的选择子和display的地址绑定起来。

当一个新的对象被创建的时候,它的内存空间随之被开辟,它的实例变量也被初始化。在这些变量中,第一个是一个指向它的类结构的指针。这个指针,称作isa,这个isa指针使这个对象可以访问它的类,通过这个类,进而访问它继承的所有类。

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

注意:尽管严格来说isa指针不是语言的一部分,但是一个对象想与Objective-C的运行时系统协作,它却是必须的。但是你几乎不需要创建你自己的根类,继承自NSObjectNSProxy的对象自动拥有这个isa指针。
类和对象的元素如图2所示。

《Objective-C运行时编程指南》 图2: Messaging Framework

当一条消息被发送到一个对象后,消息函数沿着这个对象的
isa指针到达这个对象所属类的结构体,在结构体的派发表中消息函数查找这个方法选择子。如果查阅不到,
objc_msgSend就会沿着
isa指针在父类的派发表中查找这个选择子。持续的失败会导致
objc_msgSend沿着继承体系持续查找父类的派发表直到
NSObject根类。一旦消息函数定位到了这个选择子,它就会调用派发表中的方法并传入接收对象的数据结构。

以上就是方法实现在运行时被选择的方式。以面向对象编程的术语来说就是:方法被动态地绑定到消息。

为了加快消息传递进程,运行时系统在使用选择子和函数的地址的时候会缓存它们。每个类有一个单独的缓存,这个缓存可以包含继承方法的选择子和这个类本身定义的方法的选择子。在搜索派发表之前,消息函数按惯例会首先检查接收对象的类的缓存(假设一个方法被使用后可能会被再次使用)。如果方法选择子在缓存中,那么消息传递只是稍微比一个函数调用慢些(C语言的称作函数调用(function),OC称作方法调用(method),这句话的意思应该就是仅仅比C语言慢些,但是比OC快很多)。一旦一个程序运行的时间足够长到”热身”它的缓存,那么几乎所有它发送的消息都可以找到一个缓存方法。程序运行时缓存是动态增长的以便容纳新的消息。

2.使用隐藏的参数

objc_msgSend找到了这个实现了一个方法的procedure,它调用这个procedure并传入消息中的所有参数。它同样也传入了两个隐藏的参数:

  • 接收对象
  • 方法选择子

这些参数给每个方法实现关于唤起这个方法实现的消息表达式的明确的信息。之所以说它们是“隐藏”的,是因为它们并没有在定义这个方法的源码中声明。在代码编译后它们会被插入到实现中。
尽管这些参数没有明确的声明,源码仍然可以指向它们(就像它可以指向接收对象的实例变量一样)。一个方法将这个消息的接收对象称作self,将自己的选择子称作_cmd。下面的例子中,_cmd指向strange方法的选择子,self指向接收strange消息的对象。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self strange];
}

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

self是两个参数中比较有用的一个。

3.获取一个方法的地址

跳过动态绑定的唯一方法就是获取一个方法的地址然后直接调用它就像它是一个函数一样。这种技术适用于极其罕见的情况:当一个特殊的函数将会被多次成功执行,而你想要避免方法每次执行时消息传递的开销。
使用NSObjectmethodForSelector:方法,你可以请求一个指向实现了一个方法的procedure,然后使用这个指针来调用proceduremethodForSelector:返回的指针必须被小心的转换为合适的函数类型。返回值和参数类型都应该被包含在转换中。
下面的例子展示了实现了setFilled:方法的procedure是如何被调用的:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self circumvent];
}

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

- (void)circumvent{
    setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];
    for (int i = 0 ; i < 1000 ; i++ ){
        setter(self, @selector(setFilled:), YES);
    }
}

- (void)setFilled:(BOOL)filled{
    NSLog(@"%@",NSStringFromSelector(_cmd));
}

传入procedure的前两个参数是接收对象(self)和方法选择子(_cmd)。在语法上这些参数是隐藏的但是当方法被当做函数调用时必须明确显示呈现。
使用methodForSelector:来跳过动态绑定节省了消息传递的大部分时间。但是这种节省只在一个特殊的消息被重复请求多次的情况下才会有显著的效果,例如在for循环中。
注意,methodForSelector:是由Cocoa的运行时系统提供的,它不是Objective-C语言本身的一种特性。

四:动态方法解析

如何动态的提供一个方法的实现呢?请看下文

1.动态方法解析

有些情况下你也许想动态的提供一个方法的一个实现。例如,Objective-C声明的属性中包括@dynamic指令:

@dynamic propertyName;

这个指令会告诉编译器和这个属性相关的方法将会被动态提供。
可以通过实现resolveInstanceMethod:resolveClassMethod:方法来动态的分别为某个实例或者类的某个指定选择子提供一个实现。
一个Objective-C方法就是一个简单的包含至少两个参数(self_cmd)的C函数。可以使用class_addMethod函数为一个类添加一个函数作为一个方法。因此,考虑以下函数:

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

你可以使用resolveInstanceMethod:动态地将这个函数添加到一个类中作为一个方法(称作resolveThisMethodDynamically),像下面这样:

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

转发方法(下面会在消息转发中介绍)和动态方法解析很大程度上是正交的。一个类有机会在触发转发机制前动态解析一个方法。如果respondsToSelector:或者instancesRespondToSelector:被唤起,动态方法解析有机会首先为这个选择子提供一个IMP。如果你实现了resolveInstanceMethod:方法,但是却想通过转发机制将某些选择子进行转发,那么需要为这些选择子返回NO

2.动态加载

一个Objective-C程序在运行时可以加载和链接新的类和分类(category)。这些新的代码被编入到程序中,它们被当做初次加载的类和分类一样。
动态加载可以用来做许多不同的事情。例如,系统偏好设置应用中的各种各样的模块就是动态加载的。
在Cocoa环境下,动态加载通常用来让应用可以自定义。其他人可以编写一些你程序在运行时可以加载的模块–就像界面生成器(Interface Builder)加载自定义的调色板、OS X系统偏好应用加载自定义的偏好设置。这种可加载的模块扩展了应用可以做的事情。你提供了框架,但是其他人提供了代码。
尽管有一个执行Objective-C模块动态加载的运行时函数(objc_loadModules,位于objc/objc-load.h),Cocoa的NSBundle类提供了一个更加便利的接口用来动态加载–这个类是面向对象的,并集成了相关的服务。详见NSBundle

五:消息转发

向一个对象发送一个这个对象无法处理的消息会导致crash。但是在宣布错误前,运行时系统给接收者第二次机会来处理这条消息。

1.转发

如果你向一个对象发送一个这个对象无法处理的消息,在宣布错误前,运行时系统向这个对象发送一条包含一个唯一参数(NSInvocation对象)的forwardInvocation:消息–NSInvocation对象封装了最初的消息和传递给它的参数。
可以实现forwardInvocation:来给消息一个默认的响应,或者以其它方式避免这个无法识别消息导致的错误。就像这个函数名字所指示的那样,forwardInvocation:通常用来将这条消息转发给另一个对象。
为了了解转发的范围和目的,想象以下场景:首先假设你在设计一个可以响应negotiate这条消息的对象,你期望它的响应包含另一个对象的响应。想要完成这个操作很简单,你只需要在你设计的对象的negotiate方法实现里向另一个对象传入一条negotiate消息即可。
再进一步,假设你期望你设计的对象对negotiate消息的响应就是另一个对象实现的响应。一种解决方式是让你设计的类继承另一个类的negotiate方法。但是这样安排可能是行不通的。有很多理由导致你的类和实现negotiate的类在不同的继承体系分支。
即使你的类不能继承negotiate方法,你也可以“借来”这个方法:实现这个方法并在这个方法中向另一个类的实例发送一条negotiate消息。

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

这种处理事情的方式略显笨重,尤其是当有许多消息你想通过你的对象传到另外一个对象。你不得不实现一个方法来覆盖每个你想从其它类”借来”的方法。此外,这种方法不可能处理这样一种情况–当你编写这种方法的代码时,你并不知道整个你想要转发的消息集合。这个集合也许取决于运行时的一些事件,当一些新的方法和类将来实现的时候也可能改变这个集合。
对于这个问题,forwardInvocation:消息提供的第二个机会提供了一个需要更少临时性修复(less ad hoc)的解决方式,这种解决方式是动态的而不是静态的。它的工作原理是这样的:当一个对象因为没有方法匹配消息的选择子而无法响应一条消息时,运行时系统通过向这个对象发送一条forwardInvocation:消息来通知这个对象。每一个继承自NSObject的类都继承了forwardInvocation:方法。但是这个方法的NSObject版本只是唤起了doesNotRecognizeSelector:方法。通过重写NSObjectforwardInvocation:方法,你可以充分利用forwardInvocation:消息提供的机会来将这条消息转发给其它的对象。
为了转发一条消息,一个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:
更多关于转发和调用(invocations)的消息,参考invocations

2.转发和多继承

转发模仿(mimics)继承,它可以向Objective-C程序添加一些多继承的效果。如图3所示,一个通过转发来响应一条消息的对象看起来就像”借来”或者”继承”了一个定义在另一个类里方法实现。

《Objective-C运行时编程指南》 图3:Forwarding

示例中,一个战士类的实例将一条
negotiate消息转发给了一个外交家类的实例。这个战士会像外交家一样来谈判。这个战士看起来就像响应了这条
negotiate消息(尽管实际上是一个外交家在做这项工作)。

转发了一条消息的这个对象于是乎从继承体系的两个分支”继承”方法–它自己的分支和响应这条消息的对象。上面的例子中,这个战士类看起来像是把外交家类当做父类一样来继承。

3.替代对象

转发不只是模仿多继承,它同样可以开发轻量级的对象来代表或者”覆盖”更加复杂的对象(substantial objects)。这个替代对象代替其它对象并向它发送消息。

4.转发和继承

尽管转发模拟继承,NSObject类永远也不会混淆这两个概念。像respondsToSelector:isKindOfClass:这样的方法只会考察继承体系,而不会考察转发链。例如,如果询问一个战士它是否响应negotiate消息,

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

答案是NO,即使它可以接收negotiate消息,不报错并能响应它们。
在一些情况下,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:方法同样需要添加到列表中。同样的,如果一个对象转发了任何远程消息(remote messages),它也应该实现methodSignatureForSelector:以便可以返回最终会响应转发消息的方法的准确描述;例如,如果一个对象可以转发一条消息到它的替代,你应该像下面这样实现methodSignatureForSelector:

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

注意:这是一门高级的技术,只适用于没有其它解决方案可选的情况。它并不是为了替换继承。如果你一定要使用这门技术,确保你完全理解了执行转发的类和转发到的类的行为。

六:类型编码(Type Encodings)

为了支持运行时系统,编译器将每个方法的返回值和参数类型编码到一个字符串,并将这个字符串和方法的选择子关联起来。这套编码方案因为同样适用于其它的上下文环境,所以设计成了一个公共的编译器指令:@encode()。当传入一个类型规格后,@encode()返回一个编码传入类型所对应的字符。传入的类型可以是基本的数据类型如int,可以是一个指针,一个标记的结构体或者集合,或者一个类名,实际上,任何可以作为sizeof()操作符参数的都可以传入。

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

图4列出了类型编码对照。

《Objective-C运行时编程指南》 图4:Objective-C type encodings

注意:Objective-C不支持
long double类型,
@encode(long double)返回
d,同编码
double类型一样。

一个数组的类型编码被包围在一对方括号中;数组元素的个数在左方括号后立刻被指定,位于数组类型之前。例如,一个包含12个指向浮点数的指针会被编码成:

[12^f]

结构体被大括号包围,集合被圆括号包围。首先列出的是结构体的tag,接下来是一个等号,最后是结构体各字段的编码类型。

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

会被编码成:

{example=@*i}

传入@encode()的无论是定义的类型名(Example)还是结构体的tag(example)结果都是一样的。如果是一个结构体的指针,编码结果会是:

^{example=@*i}

对象被当做结构体处理。例如,传入NSObject编码后结果为:

{NSObject=#}

NSObject类只声明了一个实例变量isa,它是一个Class
注意:对于图5的编码类型,@encode()指令并不返回它们,但是当它们被用来在协议中声明方法时,运行时系统会使用它们作为类型限定符。

《Objective-C运行时编程指南》 图5:Objective-C method encodings

七:声明属性

当编译器遇到属性声明时,它会生成描述性的元数据,这些元数据会和其所在的类,分类和协议关联起来。可以使用函数来访问元数据,这些函数必须支持通过名字查询某个类的某个属性,并能获取到这个属性的类型(@encode),而且可以将属性的属性列表复制为一个C字符串的数组。每个类和协议都有一组声明属性的清单。

1.属性类型和函数

objc_property_t结构体对一个属性描述符号定义了一个不透明的句柄。

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

可以使用class_copyPropertyListprotocol_copyPropertyList函数分别获取某个类(包括加载的类)或者某个协议所关联的属性数组。

OBJC_EXPORT objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
OBJC_EXPORT 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可以获取某个属性的名字。

OBJC_EXPORT const char *property_getName(objc_property_t property) 

使用class_getPropertyprotocol_getProperty函数可以分别获取某个类或者协议的某个属性的索引:

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

使用property_getAttributes函数可以获取一个属性的名字和编码类型(@encode)。

OBJC_EXPORT 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));
}
///打印结果
quartzView T@"TestView",W,N,V_quartzView
mStub T@"Stub",&,N,V_mStub

2.属性类型字符

使用property_getAttributes函数可以获取一个属性的名字、编码类型(@encode)和其它的特性(attributes)。
它返回字符串以T开头,接下来是@encode类型、逗号、V、实例变量名。在逗号和V中间是下图的描述符,以逗号分隔。

《Objective-C运行时编程指南》 图6:Declared property type encodings

3.属性特性描述符实例

考虑以下的定义:

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

下图展示了示例属性声明和对应property_getAttributes的返回值:

《Objective-C运行时编程指南》 图7

参考文献:Objective-C Runtime Programming Guide

提升代码质量最神圣的三部曲:模块设计(谋定而后动) –>无错编码(知止而有得) –>开发自测(防患于未然)

    原文作者:BoomLee
    原文地址: https://www.jianshu.com/p/0e623434efcd
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞