Objective-C Runtime(一): 初探

Objective-C 编译器与运行时系统支撑着OC程序的运行。

Objective-C程序在三个层面上与runtime系统交互:

  • Objective-C源代码:编译器把OC代码类、方法、成员变量等信息转化为支持语言动态特性的数据结构与函数。比如消息传递机制中的核心函数objc_msgSend,即由OC代码的消息传递语句转换而来。

  • NSObject提供了一系列的自省(Introspection)方法,也是运行时的一部分。

  • Runtime函数。

消息传递机制

在Objective-C里,消息(message)是到运行时才绑定到方法实现的.
意思就是说, 像

[receiver message];

这样一条语句, 编译器会把他转换为

objc_msgSend(receiver, selector);

这样第一个C语言的函数调用, 参数分别是消息接受者(对象), 消息对应的方法名称(选择子), 若改方法带参数, 则为

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

该函数的动态绑定过程是这样的:

  • 它首先沿着类的继承体系去寻找选择子对应的方法实现.
  • 找到后调用具体的方法实现, 并把对象指针以及各参数传递给该方法, 随后调用它.
  • 最后返回该方法的返回值.

它的函数原型:

id objc_msgSend(id self, SEL cmd, ...)

回头看动态绑定过程的第一步.
Objective-C里, 每个类里都维护着一张表格(dispatch table), 其中的指针正是指向该类下所定义的方法实现, 而方法的选择子(selector)作为查表用的"键".
每个类里除了该表之外, 还拥有一个指向其父类的指针.

这些类与对象的结构是这样的:

对象实例里有一个isa指针, 指向它的类对象.

objc_msgSend函数依赖着上述的继承体系去查找并调用恰当的方法.

为了加速方法的查找, 每个类里除了自身定义的方法列表外, 还维护这一张快速映射表作为缓存. 多次对它查找同一selector将不再向上追溯查找, 而直接查找本身的缓存并返回对应的方法实现.

刚才提到要调用的方法实现, 每个OC对象的方法都可视为一个C函数, 其原型如下:

<return_type> Class_selector(id self, SEL _cmd, ...)

实际函数名可能跟上面的不一样. 但注意的是该函数里是包括了self_cmd两个隐含参数的. 所谓"隐含", 是指在开发人员编写的方法代码里, 是不存在这两个参数, 但我们都可以通过这两个变量名去访问.

消息转发机制

在上一节消息传递机制中, 对象接收到一个消息后, 去搜寻其对应方法实现的函数地址. 若搜寻不到, 并不马上抛出异常, 而是再给接受者一次机会, 进入消息转发机制.

消息转发分为两大阶段. 第一阶段先征询接受者所属的类, 看其是否能动态添加方法, 以处理当前这个未知的选择子(unknown selector), 这叫做动态方法解析(Dynamic Method Resolution); 第二阶段则为"完整的消息转发机制"(full forwarding mechanism).

动态方法解析

对象在收到无法解读的消息后, 首先将调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

顾名思义, 该方法作用为解析实例方法, 相应地也有个类似的方法, 为解析类方法所用: resolveClassMethod.
此方法在respondsToSelector:instancesRespondToSelector:被调用后返回前, 也有一次机会为自己动态添加一个方法的实现.

动态方法解析常用来实现 @dynamic 属性.

下面看一个完整的例子演示动态方法解析.

假设要编写一个类似"字典"的对象, 它里面可以容纳其它对象, 只不过开发者要直接通过属性来存取其中的数据. 这个类的设计思路是: 有开发者来添加属性定义, 并将其声明为 @dynamic, 类则会自动处理相关属性值的存放与获取操作. 听起来不错吧? >_<

类的接口定义如下:

#import <Foundation/Foundation.h>

@interface AutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end

这个类将装载各种不同类型的对象, 看起来与平时普通的类没啥区别啊? 我们看类的实现.

#import "AutoDictionary.h"
#import <objc/runtime.h>

@interface AutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation AutoDictionary

@dynamic string, number, date, opaqueObject;

- (instancetype)init {
    if (self = [super init]) {
        _backingStore = [[NSMutableDictionary alloc] init];
    }
    return self;
}

声明各属性为 @dynamic, 编译器不会自动为property生成存取方法和实例变量. 由我们自行实现.

关键在于resolveInstanceMethod:方法的实现.

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
    } else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    
    return [super resolveInstanceMethod:sel];
}

众所周知, 任何的点语法访问都会转化为名为<name>, set<Name>形式的存取方法来访问, 以上使用class_addMethod在运行时添加存取方法, 所有属性将共用这一对getter与setter.

class_addMethod函数第一和第二参数分别为类对象自身与选择子, 第三个参数为待添加方法实现对应的函数指针, 第四为待添加方法的"类型编码", 指定该添加方法的参数与返回值等.

使用class_addMethod动态添加方法后, 所添加的方法将一直在运行时存在, 下一次的调用该方法将不再进行动态方法解析.

下面实现getter与setter:

// getter
id autoDictionaryGetter(id self, SEL _cmd) {
    // Get the backing store from the object
    AutoDictionary *typedSelf = (AutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    // The key is simply the selector name
    NSString *key = NSStringFromSelector(_cmd);
    
    // Return the value
    return [backingStore objectForKey:key];
    
}

//setter
void autoDictionarySetter(id self, SEL _cmd, id value) {
    // Get the backing store from the object
    AutoDictionary *typedSelf = (AutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    /** The selector will be for example, "setOpaqueObject:".
     *  We need to remove the "set", ":" and lowercase the first
     *  letter of the remainder.
     */
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    // Remove the `:' at the end
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
    // Remove the `set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    // Lowercase the first character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }
}

使用它们的方式很简单:

AutoDictionary *dict = [[AutoDictionary alloc] init];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);
//Output: dict.date = 1985-01-24 00:00:00 +0000

而且它还是KVC兼容的哦! *(关于KVC与KVO, 可参考我之前的博客

[dict setValue:@"I'm a string!" forKey:@"string"];
NSLog(@"dict.string = %@", dict.string);
//Output: dict.string = I'm a string!

备援接受者

在完整的消息转发来临之前, 当前接受者还有第二次机会处理未知的选择子. 处理方法如下:

- (id)forwardingTargetForSelector:(SEL)aSelector

运行时系统通过该方法询问能否把无法识别的选择子转给其它对象处理呢?
例如, 在一个对象内部, 可能还有其它一系列对象, 该对象可经由此方法将能够处理某选择子的相关内部对象返回. 这样看来, 就好像是该对象亲自处理了这些消息似的. 这样可以模拟出"多重继承"的特性.

完整的消息转发

终于来到了这一步. 首先创建NSInvocation对象, 把尚未处理的有关该消息的全部细节封装起来, 包括选择子, 目标(target), 参数与返回值等. 在触发NSInvocation对象时, 消息派发系统(message-dispatch system)将亲自出马, 把消息指派给目标对象.

消息转发方法:

- (void)forwardInvocation:(NSInvocation *)invocation

在此方法里需要做的事情是:

  • 决定消息发送的目标对象;
  • 随参数一起发送该消息.

消息通过invokeWithTarget:发送.

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

以上代码中最后调用超类处理该消息, 沿着继承体系向上, 每个类都有机会处理该请求, 直至NSObject, 它的该方法默认实现为抛出doesNotRecognizeSelector:异常.

相对于简单的消息发送语句 [receiver message];, forwardInvocation:提供了一种更加灵活的机制, 避免了冗余的方法重写或者破坏类继承体系, 而提供了一种类似"消息中转派发"的机制. 另外NSInvocation也提供了对待转发消息的修改机制, 甚至不做转发, 等等, 也提供了更多的操作性.

初探Objective-C Runtime System, 这篇博文对Runtime消息传递, 转发机制等做了一些探讨. 关于更多的Runtime研究与实践, 将在日后的博客中更新.

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容