三、Objective-C之Runtime的深入

接上一篇,说到了objc_class里面的method_list,以及SEL与IMP的一一对应关系,以及消息的发送处理过程,留下了动态方法解析和消息转发的迷点,这篇就继续学习这个留下的迷点。

一、动态方法解析

动态方法解析,顾名思义,就是在runtime时期动态的提供一个方法的实现。

举个栗子:@dynamic propertyName;。这表明我们自己会为这个属性动态提供存取方法,也就是告诉编译器不要再默认为我们生成setPropertyName:和propertyName方法,我们自己动态的提供。

温馨提示:@dynamic就是要告诉编译器,代码中用@dynamic修饰的属性,其getter和setter方法会在程序运行的时候或者用其他方式动态绑定,以便让编译器通过编译。

我们可以通过分别重载resolveInstanceMethod:resolveClassMethod:方法分别添加实例方法实现和类方法实现。(这个方法的调用时机为当被调用的方法实现部分没有找到,而消息转发机制启动之前的这个中间时刻。)

因为当Runtime系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:resolveClassMethod:来给程序员一次动态添加方法实现的机会。

我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作,参考代码如下:

@interface DynamicObject : NSObject

@property (assign, nonatomic) float height;
@end

@implementation DynamicObject

@dynamic height;  //声明为dynamic
//添加setter实现
void dynamicSetMethod(id self,SEL _cmd,float f) {
    NSLog(@"%s, %f\n", [NSStringFromSelector(_cmd) cStringUsingEncoding:NSUTF8StringEncoding], f);
    objc_setAssociatedObject(self, @"ivar_tag", [NSNumber numberWithFloat:f], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//添加getter实现
void dynamicGetMethod(id self,SEL _cmd) {
    NSLog(@"%s\n", [NSStringFromSelector(_cmd) cStringUsingEncoding:NSUTF8StringEncoding]);
    [objc_getAssociatedObject(self, @"ivar_tag") floatValue];
}

void dynamicGrowUp(id self,SEL _cmd) {
    NSLog(@"%s is 188.3", [NSStringFromSelector(_cmd) cStringUsingEncoding:NSUTF8StringEncoding]);

}

//解析selector方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    NSString *methodName=NSStringFromSelector(sel);
    BOOL result=NO;
    //动态的添加setter和getter方法
    if ([methodName isEqualToString:@"setHeight:"]) {
        //添加setter方法
        class_addMethod([self class], sel, (IMP) dynamicSetMethod, "v@:f");
        result=YES;
    }else if([methodName isEqualToString:@"height"]){
        //添加getter方法
        class_addMethod([self class], sel, (IMP) dynamicGetMethod, "v@:");
        result=YES;
    }else if([methodName isEqualToString:@"growUp"]){
        class_addMethod([self class], sel, (IMP) dynamicGrowUp, "v@:");
        result = YES;
    }
    return result;
}
//调用测试:
DynamicObject *dobj = [[DynamicObject alloc]init];

dobj.height = 3.14f;
NSLog(@"%f", dobj.height+0.1f);

[dobj growUp];

//输出结果:
setHeight:, 3.140000
height
3.240000
growUp is 188.3

在OSX 10.6之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);

这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联会涉及到一组枚举常量:

enum {
   OBJC_ASSOCIATION_ASSIGN  = 0,
   OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
   OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
   OBJC_ASSOCIATION_RETAIN  = 01401,
   OBJC_ASSOCIATION_COPY  = 01403
};

在消息转发机制开始之前,一个类在没有找到对应的selector时,会先动态解析该方法,如果你实现了resolveInstanceMethod:,但是想对一些特定的selectors启用消息转发机制,只需要过滤这些selector返回NO即可。

上面的例子的注意点:

1、在上个例子中,我们自己实现了setter方法,则在运行的时候,调用完我们实现的setter方法后不再执行resolveInstanceMethod方法,只有当找不到对象的selector的时候,才会走resolveInstanceMethod方法。

2、v@:属于Objective-C类型编码的内容。


二、消息转发

我们已然知道:对象会接收所有发送过来的消息,哪怕这些消息自己无法响应。那么问题来了:当对象无法响应这些消息时怎么办?runtime提供了消息转发机制来处理该问题。

消息转发开始前

在消息转发机制执行前,Runtime系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象。

forwardingTargetForSelector是NSObject的函数,用户可以在派生类中对其重载,从而将无法处理的selector转发给另一个对象。

还是以上面的uppercaseString为例,如果用户自己定义的CA类的对象a,没有uppercaseString这样一个实例 函数,那么在不调用respondSelector的情况下,直接执行[a performSelector:@selector"uppercaseString"],那么执行时一定会crash,此时,如果CA实现了forwardingTargetForSelector函数,并返回一个NSString对象,那么就相对于对该NSString对象执行了 uppercaseString函数,此时就不会crash了。当然实现这个函数的目的并不仅仅是为了程序不crash那么简单。

// ---------MsgObject---------
@interface MsgObject : NSObject

@end

@implementation MsgObject

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(uppercaseString)) {
        return @"hello world";
    }else if (aSelector == @selector(sayHello)) {
        SayObject *say = [[SayObject alloc]init];
        return say;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

// ---------SayObject---------
@interface SayObject : NSObject

- (void)sayHello;
@end

@implementation SayObject

- (void)sayHello {
    NSLog(@">> i say hello");
}
@end

//---------测试代码---------
MsgObject *msg = [[MsgObject alloc]init];
NSString *s = [msg performSelector:@selector(uppercaseString)];
NSLog(@"%@", s);

[msg performSelector:@selector(sayHello)];

//---------输出:---------
HELLO WORLD
>> i say hello

上面的例子,把自身不能处理的SEL方法转发给了NSString来处理(NSString对象有uppercaseString的方法)。避免了当对象无法响应某个selector时的崩溃问题,而hello world字符串可以响应uppercaseString,所以调用该方法,输出大写的HELLO WORLD,同理,当MsgObject处理不了sayHello时,找了个SayObject的对象来处理sayHello。

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回self,因为那样会死循环。

消息转发进行中

当外部调用的某个方法对象没有实现,而且resolveInstanceMethodforwardingTargetForSelector方法中也没有做重定向处理时,就会触发- (void)forwardInvocation:(NSInvocation *)anInvocation方法。在该方法中,可以实现对不能处理的消息做的一些默认处理,也可以以其它的某种方式来避免错误被抛出。像forwardInvocation:的名字一样,这个方法 通常用来将不能处理的消息转发给其它的对象 。

当消息转发机制被触发,这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
	SEL invSEL = invocation.selector;
	if ([someOtherObject respondsToSelector:invSEL])
		[anInvocation invokeWithTarget:someOtherObject];
	} else {
		[self doesNotRecognizeSelector:invSEL]; 
	}
}

forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。

在调用forwardInvocation:之前,需要先调用methodSignatureForSelector:获取指定selector的方法签名:

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

说的理论始终是抽象的东西,还是举个完整的例子:

// ---------SomeObject---------
@interface SomeObject : NSObject{
    id forwardObject;
}

- (void)doSomething;
@end

@implementation SomeObject

- (id)init {
    if (self = [super init]) {
        forwardObject = [[ForwardObject alloc]init];
    }
    return self;
}

- (void)doSomething {
    NSLog(@"doSomething:%@", [self class]);
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    if (!forwardObject) {
        [self doesNotRecognizeSelector: [invocation selector]];
    }
    [invocation invokeWithTarget:forwardObject];
}

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if (! signature) {
        NSLog(@"生成方法签名");
        signature = [forwardObject methodSignatureForSelector:selector];
    }
    return signature;
}

// ---------ForwardObject---------
@interface ForwardObject : NSObject
- (void)doSomethingElse;
@end

@implementation ForwardObject

- (void)doSomethingElse {
    NSLog(@"doSomethingElse:%@", [self class]);
}
@end

//---------测试代码---------
SomeObject *someClass = [[SomeObject alloc]init];
[someClass doSomething];
[someClass doSomethingElse];

//---------输出:---------
doSomething:SomeObject
生成方法签名
doSomethingElse:ForwardObject

从上面可以看出,消息转发类似于多重继承。然而,两者之间有很大的不同:

多重继承是将不同的行为封装到单个的对象中,有可能导致庞大的,复杂的对象。而消息转发是将问题分解到更小的对象中,但是又以一种对消息发送对象来说完全透明的方式将这些对象联系起来。

消息转发有很多的用途,比如:

  • 创建一个对象负责把消息转发给一个由其它对象组成的响应链,代理对象会在这个有其它对象组成的集合里寻找能够处理该消息的对象;

  • 把一个对象包在一个logger对象里,用来拦截或者纪录一些有趣的消息调用;

  • 比如声明类的属性为dynamic,使用自定义的方法来截取和取代由系统自动生成的getter和setter方法。

posted @ 2018-07-09 11:09  Mr轨迹  阅读(161)  评论(0编辑  收藏  举报