Objective-C Runtime(一): 初探

作者: 4d1487047cf6 | 来源:发表于2016-07-04 17:34 被阅读204次

    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研究与实践, 将在日后的博客中更新.

    参考资料

    相关文章

      网友评论

        本文标题: Objective-C Runtime(一): 初探

        本文链接:https://www.haomeiwen.com/subject/wjthjttx.html