运行期(runtime)

Tuesday, January 30, 2018

理解objc_msgSend 的作用

如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变。

   id returnValue = [someObject messageName:parameter];

someObject 叫做接收者(receiver),messageName 叫做"选择子”,选择子与参数合起来称为"消息”,编译器看到此消息后,将其转换为一条标准的 C 语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做 objc msgSend,原型如下

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

这是个参数个数可变的函数,能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的那些参数,顺序不变。 编译器会把上述消息转为如下函数

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法。该方法需要在接收者所属的类中搜寻其方法列表(list of methods),如果能找到与选择子名称相符的方法,就跳转到实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终找不到,则执行消息转发。

为了提高匹配速度,objc_msgSend 会将匹配结果缓存到"快速映射表”

理解消息转发

在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确定类中到底会不会有某个方法实现。

消息转发分为两大阶段,第一阶段是先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个未知的选择子。这叫做动态方法解析。

第二阶段涉及完整的消息转发机制,如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含选择子的消息了。运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。分为两小步,首先请接收者看看有没有其它对象能够处理这条消息,若有,则运行期系统会把消息转发给那个对象。若没有,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到 NSInvocation 中,再给接收者一次机会解决未处理的这条消息。

动态方法解析

对象收到无法解读的消息,首先调用下列方法

+ (BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,表示这个类是否能新增一个实例方法用以处理此选择子。如果是类方法,则调用 resolveClassMethod。 在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。

使用这种办法的前提是,相关的方法实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方法常用来实现@dynamic 属性,比如说,要访问CoreData 框架中NSManagedObject对象的属性时就可以这么做。

下面展示如何用 resolveInstanceMethod 来实现 @dynamic 属性

id autoDictionaryGetter(id self, SEL  _cmd);
id autoDictionarySetter(id self, SEL _cmd, id value);
    
+ (BOOL)resolveInstanceMethod(SEL)selector {
        NSString *selectorString = NSStringFromSelector(selector);
        if ([selecotrString hasPrefix:@"set"]) {
            class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
        } else {
            class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
}

备援接收者

当前接收者还有第二次寄回能处理未知的选择子,在这一步中,运行期系统会问它,能不能把这条消息转给其他接收者来处理。

- (id)forwardingTargetForSelector:(SEL)selector

参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到则返回 nil。通过此方案,我们可以用组合来模拟出多重继承的某些特性。 注意,我们无法操作经由这一步所转发的消息。若是想在发送给备援对象接收者之前先修改消息内容,则要通过完整的消息转发机制来做了。

完整的消息转发

首先创建 NSInvocation 对象,把与尚未处理的那条消息的相关细节都封于其中。此对象包含选择子,目标及参数。在触发 NSInvocation 对象时,“消息派发系统"将亲自出马,把消息指派给目标对象。 会调用以下方法来转发消息:

- (void)forwardInvocation:(NSInvocation*)invocation

比较有用的实现方式,在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或者是改变选择子,等等。

消息转发全流程

runtime1

完整例子

编写一个类似『字典』 的对象,它里面可以容纳其他对象,只不过开发者要直接通过属性来存取其中的数据。 设计思路:由开发者来添加属性定义,并将其声明为@dynamic,而类则会自动处理相关属性值的存放与获取操作。

#import<Foundation/Foundation.h>
    
    @interface EOCAutoDictionary: NSobject
    @property (nonatomic, strong) NSString *string;
    @property (nonatomic, strong) NSNumber *number;
    @property (nonatomic, strong) NSDate * date;
    @property (nonatomic, strong) id opaqueObject;
    @end
    
    
    #import "EOCAutoDictionary.h"
    #import <objc/runtime.h>
    
    @interface EOCAutoDictionary ()
    @property (nonatomic, strong) NSMutableDictionary *backingStore;
    @end
    
    @implementation EOCAutoDictionary
    
    @dynamic string, number, date, opaqueObject;
    
    - (id)init {
           if ((self = [super init])) {
                    _backingStore = [NSMutableDictionary new];
               }
           return self;  
    }
    
    + (BOOL)resolveInstanceMethod(SEL)selector {
          NSString *selectorString = NSStringFromSelector(selector);
          if ([selecotrString hasPrefix:@"set"]) {
                 class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
          } else {
                class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
        return YES;
    }
    @end
    
    //getter 函数
    
    id autoDictionaryGetter(id self, SEL _cmd) {
        EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
        NSMutableDictionary * backingStore = typedSelf.backingStore;
    
      NSString *key = NSStringFromSelector(_cmd);
    
    return [backingStore objectForKey: key];
    }
    
    
    //setter 函数
    void autoDictionarySetter(id self, SEL _cmd, id value) {
    
            EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
            NSMutableDictionary *backingStore = typedSelf.backingStore;
    
            NSString *selectorString = NSStringFromSelector(_cmd);
            NSMutableString *key = [selectorString mutableCopy];
            [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
            [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
            NSString *lowercaseFirstChar = [[key substringToIndex: 1]                  lowercaseString];
            [key replaceCharactersInRange:NSMakeRange(0, 1) withString: lowercaseFirstChar];
    
          if (value) {
               [backingStore setObject:value forKey: key];
          } else {
              [backingStore removeObjectForKey:key];
          }
    } 

用"方法调配技术"调试"黑盒方法”

给定的选择子名称相对应的方法也可以在运行期改变,既不需要源代码,也不需要通过继承子类来覆写就能改变这个类本身的的功能,新功能将在本类的所有实例中生效,而不仅限于覆写了相关方法的那些子类实例。此方法称为 “method swizzling”

类的方法会把选择子的名称映射到相关的方法实现之上,使得动态派发系统能够找到调用的方法,这些方法均以指针的形式来表示,这种指针叫做 IMP。

id(*IMP)(id, SEL, ...)

runtime2

开发者可以向其中新增选择子,也可以改变某些选择子所对应的方法实现,还可以交换选择子映射到的指针。

交换方法的函数

void method_exchangeImplementations(Method m1, Method m2)

此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

Method class_getInstanceMethod(Class aClass, SEL aSelector)

此函数根据给定的类中取出相应的方法,再用 exchangeImplementations 方法即可交换。

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    
Method swappedMethod = class_getInstanceMethod([Nsstring class], @selector(uppercaseString));
    
method_exchangeImplementations(originalMethod, swappedMethod);

从现在开始,如果在 NSString 实例上调用 lowercaseString,那么执行的将是 uppercaseString 的原有实现,反之亦然。

在实际应用中,像这样交换两个方法实现意义不大。但是,可以通过这一手段为既有的方法实现增添方新功能。

@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end
    
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString {
        NSString *lowercase = [self eoc_myLowercaseString];
        NSLog(@"%@ => %@", self, lowercase);
        return lowercase;
}
@end

代码看上去会陷入递归调用的死循环,但是此方法是准备和 lowercaseString 方法互换的。所以,在运行期 eoc_myLowercaseString 选择子实际上对应于原有的 lowercaseString 方法实现。通过下列代码实现:

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));    
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

执行完上述代码之后,只要在 NSString 实例上调用 lowercaseString 方法,就会输出一行打印消息。

NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
//Output: ThIs iS tHe StRiNg => this is the string

runtime可以为那些"完全不知道其具体实现的"黑盒方法增加日志记录功能,非常有助于程序调试。但不宜滥用。

Objective-C

理解"类对象"的用意

对象,消息,运行期