运行时(runtime)技术的几个要点总结 和 消息转发

运行时(runtime)技术的几个要点总结

Objective C的runtime技术功能非常强大,能够在运行时获取并修改类的各种信息,包括获取方法列表、属性列表、变量列表,修改方法、属性,增加方法,属性等等,本文对相关的几个要点做了一个小结。

目录:

(1)使用class_replaceMethod/class_addMethod函数在运行时对函数进行动态替换或增加新函数

(2)重载forwardingTargetForSelector,将无法处理的selector转发给其他对象

(3)重载resolveInstanceMethod,从而在无法处理某个selector时,动态添加一个selector

(4)使用class_copyPropertyListproperty_getName获取类的属性列表及每个属性的名称

  (5使用class_copyMethodList获取类的所有方法列表

  (6) 总结

 

(1)在运行时对函数进行动态替换  class_replaceMethod

      使用该函数可以在运行时动态替换某个类的函数实现,这样做有什么用呢?最起码,可以实现类似windowshook效果,即截获系统类的某个实例函数,然后塞一些自己的东西进去,比如打个log什么的。

示例代码:

复制代码
IMP orginIMP;
NSString * MyUppercaseString(id SELF, SEL _cmd)
{
    NSLog(@"begin uppercaseString");
    NSString *str = orginIMP (SELF, _cmd);(3)
    NSLog(@"end uppercaseString");
    return str;
}
-(void)testReplaceMethod
{
      Class strcls = [NSString class];
      SEL  oriUppercaseString = @selector(uppercaseString);
      orginIMP = [NSStringinstanceMethodForSelector:oriUppercaseString];  (1)  
      IMP imp2 = class_replaceMethod(strcls,oriUppercaseString,(IMP)MyUppercaseString,NULL);(2)
      NSString *s = "hello world";
      NSLog(@"%@",[s uppercaseString]];
}
复制代码

执行结果为:

begin uppercaseString

end uppercaseString

HELLO WORLD

这段代码的作用就是

(1)得到uppercaseString这个函数的函数指针存到变量orginIMP中

(2)将NSString类中的uppercaseString函数的实现替换为自己定义的MyUppercaseString

(3)在MyUppercaseString中,先执行了自己的log代码,然后再调用之前保存的uppercaseString的系统实现,这样就在系统函数执行之前加入自己的东西,后面每次对NSString调用uppercaseString的时候,都会打印出log来

 与class_replaceMethod相仿,class_addMethod可以在运行时为类增加一个函数。

(2)当某个对象不能接受某个selector时,将对该selector的调用转发给另一个对象- (id)forwardingTargetForSelector:(SEL)aSelector

     forwardingTargetForSelector是NSObject的函数,用户可以在派生类中对其重载,从而将无法处理的selector转发给另一个对象。还是以上面的uppercaseString为例,如果用户自己定义的CA类的对象a,没有uppercaseString这样一个实例函数,那么在不调用respondSelector的情况下,直接执行[a performSelector:@selector"uppercaseString"],那么执行时一定会crash,此时,如果CA实现了forwardingTargetForSelector函数,并返回一个NSString对象,那么就相对于对该NSString对象执行了uppercaseString函数,此时就不会crash了。当然实现这个函数的目的并不仅仅是为了程序不crash那么简单,在实现装饰者模式时,也可以使用该函数进行消息转发。

示例代码:

复制代码
 1 @interface CA : NSObject
 3 -(void)f;
 4 
 5 @end
 6 
 7 @implementation CA
 8 
 9 - (id)forwardingTargetForSelector:(SEL)aSelector
11 {
13     if (aSelector == @selector(uppercaseString))
15     {
17         return@"hello world";
19     }
21 }
复制代码

 

测试代码:

CA *a = [CA new];

 NSString * s = [a performSelector:@selector(uppercaseString)];

NSLog(@"%@",s);

 

测试代码的输出为:HELLO WORLD 

 ps:这里有个问题,CA类的对象不能接收@selector(uppercaseString),那么如果我在forwardingTargetForSelector函数中用class_addMethod给CA类增加一个uppercaseString函数,然后返回self,可行吗?经过试验,这样会crash,此时CA类其实已经有了uppercaseString函数,但是不知道为什么不能调用,如果此时new一个CA类的对象,并返回,是可以成功的。

(3)当某个对象不能接受某个selector时,向对象所属的类动态添加所需的selector

+ (BOOL) resolveInstanceMethod:(SEL)aSEL 

     这个函数与forwardingTargetForSelector类似,都会在对象不能接受某个selector时触发,执行起来略有差别。前者的目的主要在于给客户一个机会来向该对象添加所需的selector,后者的目的在于允许用户将selector转发给另一个对象。另外触发时机也不完全一样,该函数是个类函数,在程序刚启动,界面尚未显示出时,就会被调用。

     在类不能处理某个selector的情况下,如果类重载了该函数,并使用class_addMethod添加了相应的selector,并返回YES,那么后面forwardingTargetForSelector就不会被调用,如果在该函数中没有添加相应的selector,那么不管返回什么,后面都会继续调用forwardingTargetForSelector,如果在forwardingTargetForSelector并未返回能接受该selector的对象,那么resolveInstanceMethod会再次被触发,这一次,如果仍然不添加selector,程序就会报异常

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
代码示例一:
 
  1 @implementation  CA
  3 void  dynamicMethodIMP( id  self , SEL  _cmd)
  5 {   
  7      printf( "SEL %s did not exist\n" ,sel_getName(_cmd));
  9 }
10
11 + ( BOOL ) resolveInstanceMethod:( SEL )aSEL
13 {
15     if  (aSEL == @selector (t))
17     {
19         class_addMethod([selfclass], aSEL, (IMP) dynamicMethodIMP, "v@:" );
21         return  YES ;
23     }
25     return  [superresolveInstanceMethod:aSEL];
27 }
28
29 @end
  
 
测试代码:
 
   CA * ca = [CA new ]
 
   [ca performSelector: @selector (t)]; 
复制代码

  

 

执行结果

   SEL t did not exist

 

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
示例代码二:
 
@implementation  CA
 
void  dynamicMethodIMP( id  self , SEL  _cmd)
{
     printf( "SEL %s did not exist\n" ,sel_getName(_cmd));
}
 
+ ( BOOL ) resolveInstanceMethod:( SEL )aSEL
{
     return   YES ;
}
- ( id )forwardingTargetForSelector:( SEL )aSelector
{
     if  (aSelector == @selector (uppercaseString))
     {
         return  @ "hello world" ;
     }
}
测试代码 :
 
  a = [[CA alloc]init];
  NSLog (@ "%@" ,[a performSelector: @selector (uppercaseString)];
复制代码

  

该测试代码的输出为:HELLO WORLD 

对于该测试代码,由于a没有uppercaseString函数,因此会触发resolveInstanceMethod,但是由于该函数并没有添加selector,因此运行时发现找不到该函数,会触发

forwardingTargetForSelector函数,在forwardingTargetForSelector函数中,返回了一个NSString "hello world",因此会由该string来执行uppercaseString函数,最终返回大写的hello world。

 

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
示例代码三:
 
@implementation  CA
 
+ ( BOOL ) resolveInstanceMethod:( SEL )aSEL
{
     return   YES ;
}
- ( id )forwardingTargetForSelector:( SEL )aSelector
{
     return  nil ;
   }
 测试代码:
 
1  a = [[CA alloc]init];
2   NSLog (@ "%@" ,[a performSelector: @selector (uppercaseString)];

  

复制代码

  

这段代码的执行顺序为:

 (1):首先在程序刚执行,AppDelegate都还没有出来时,resolveInstanceMethod就被触发,

 

(2)等测试代码执行时,forwardingTargetForSelector被调用

(3)由于forwardingTargetForSelector返回了nil,因此运行时还是找不到uppercaseString selector,这时又会触发resolveInstanceMethod,由于还是没有加入selector,于是会crash。

 

(4) 使用class_copyPropertyList及property_getName获取类的属性列表及每个属性的名称

  

复制代码
u_int               count;
objc_property_t*    properties= class_copyPropertyList([UIView class ], &count);
for  ( int  i = 0; i < count ; i++)
{
     const  char * propertyName = property_getName(properties[i]);
     NSString  *strName = [ NSString   stringWithCString:propertyName encoding: NSUTF8StringEncoding ];
     NSLog (@ "%@" ,strName);
}
复制代码

 以上代码获取了UIView的所有属性并打印属性名称, 输出结果为:

复制代码
skipsSubviewEnumeration
viewTraversalMark
viewDelegate
monitorsSubtree
backgroundColorSystemColorName
gesturesEnabled
deliversTouchesForGesturesToSuperview
userInteractionEnabled
tag
layer
_boundsWidthVariable
_boundsHeightVariable
_minXVariable
_minYVariable
_internalConstraints
_dependentConstraints
_constraintsExceptingSubviewAutoresizingConstraints
_shouldArchiveUIAppearanceTags
复制代码

 

 

(5) 使用class_copyMethodList获取类的所有方法列表

    获取到的数据是一个Method数组,Method数据结构中包含了函数的名称、参数、返回值等信息,以下代码以获取名称为例:

复制代码
u_int               count;
Method*    methods= class_copyMethodList([UIView class ], &count);
for  ( int  i = 0; i < count ; i++)
{
     SEL  name = method_getName(methods[i]);
     NSString  *strName = [ NSString   stringWithCString:sel_getName(name) encoding: NSUTF8StringEncoding ];
     NSLog (@ "%@" ,strName);
}
复制代码

  代码执行后将输出UIView所有函数的名称,具体结果略。

     其他一些相关函数:

    

复制代码
1.SEL method_getName(Method m) 由Method得到SEL
2.IMP method_getImplementation(Method m)  由Method得到IMP函数指针
3.const char *method_getTypeEncoding(Method m)  由Method得到类型编码信息
4.unsigned int method_getNumberOfArguments(Method m)获取参数个数
5.char *method_copyReturnType(Method m)  得到返回值类型名称
6.IMP method_setImplementation(Method m, IMP imp)  为该方法设置一个新的实现
复制代码

 

(6)总结

       总而言之,使用runtime技术能做些什么事情呢?

       可以在运行时,在不继承也不category的情况下,为各种类(包括系统的类)做很多操作,具体包括:

  •    增加
增加函数:class_addMethod
增加实例变量:class_addIvar
增加属性: @dynamic 标签,或者class_addMethod,因为属性其实就是由getter和setter函数组成
增加Protocol:class_addProtocol (说实话我真不知道动态增加一个protocol有什么用,-_-!!)
  •    获取  
获取函数列表及每个函数的信息(函数指针、函数名等等):class_getClassMethod method_getName ...
获取属性列表及每个属性的信息:class_copyPropertyList property_getName
获取类本身的信息,如类名等:class_getName class_getInstanceSize
获取变量列表及变量信息:class_copyIvarList
获取变量的值
  •    替换
将实例替换成另一个类:object_setClass
将函数替换成一个函数实现:class_replaceMethod
直接通过 char  *格式的名称来修改变量的值,而不是通过变量

  

    

   参考资料:(1)Objective-C Runtime Reference

               (2)深入浅出Cocoa之消息   

               (3)objective-c 编程总结(第六篇)运行时操作 - 方法交换

                (4)Runtime of Objective-C



消息转发

一.消息转发流程

当向Objective-C对象发送一个消息,但runtime在当前类及父类中找不到此selector对应的方法时,消息转发(message forwarding)流程开始启动。

  1. 动态方法解析(Dynamic Method Resolution或Lazy method resolution)
    向当前类(Class)发送resolveInstanceMethod:(对于类方法则为resolveClassMethod:)消息,如果返回YES,则系统认为请求的方法已经加入到了,则会重新发送消息。
  2. 快速转发路径(Fast forwarding path)
    若果当前target实现了forwardingTargetForSelector:方法,则调用此方法。如果此方法返回除nil和self的其他对象,则向返回对象重新发送消息。
  3. 慢速转发路径(Normal forwarding path)
    首先runtime发送methodSignatureForSelector:消息查看Selector对应的方法签名,即参数与返回值的类型信息。如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation,向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数;若methodSignatureForSelector:无方法签名返回,则向当前对象发送doesNotRecognizeSelector:消息,程序抛出异常退出。

    Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MessageInterceptor test]: unrecognized selector sent to instance 0x9589830'

二.动态解析(Lazy Resolution)

runtime发送消息的流程即查找该消息对应的方法或IMP,然后跳转至对应的IMP。有时候我们不想事先在类中设置好方法,而想在运行时动态的在类中插入IMP。这种方法是真正的快速”转发”,因为一旦对应的方法被添加到类中,后续的方法调用就是正常的消息发送流程。此方法的缺点是不够灵活,你必须有此方法的实现(IMP),这意味这你必须事先预测此方法的参数和返回值类型。

@dynamic属性是使用动态解析的一个例子,@dynamic告诉编译器该属性对应的getter或setter方法会在运行时提供,所以编译器不会出现warning; 然后实现resolveInstanceMethod:方法在运行时将属性相关的方法加入到Class中。

respondsToSelector:instancesRespondToSelector:方法被调用时,若该方法在类中未实现,动态方法解析器也会被调用,这时可向类中增加IMP,并返回YES,则对应的respondsToSelector:的方法也返回YES。

三.快速转发(Fast Forwarding)

runtime然后会检查你是否想将此消息不做改动的转发给另外一个对象,这是比较常见的消息转发情形,可以用较小的消耗完成。
快速转发技术可以用来实现伪多继承,你只需编写如下代码

- (id)forwardingTargetForSelector:(SEL)sel { return _otherObject; }

这样做会将任何位置的消息都转发给_otherObject对象,尽管当前对象与_otherObject对象是包含关系,但从外界看来当前对象和_otherObject像是同一个对象。
伪多继承与真正的多继承的区别在于,真正的多继承是将多个类的功能组合到一个对象中,而消息转发实现的伪多继承,对应的功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。

四.慢速转发(Normal Forwarding)

以上两者方式是对消息转发的优化,如果你不使用上述两种方式,则会进入完整的消息转发流程。这会创建一个NSInvocation对象来完全包含发送的消息,其中包括target,selector,所有的参数,返回值。

在runtime构建NSInvocation之前首先需要一个NSMethodSignature,所以它通过-methodSignatureForSelector:方法请求。一旦NSInvocation创建完成,runtime就会调用forwardInvocation:方法,在此方法内你可以使用参数中的invocation做任何事情。无限可能…
举个例子,如果你想对一个NSArray中的所有对象调用同一个方法,而又不想一直写循环代码时,想直接操作NSArray时,可这样处理:

@implementation NSArray (ForwardingIteration)

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    {
        NSMethodSignature *sig = [super methodSignatureForSelector:sel];
        if(!sig)
        {
            for(id obj in self)
                if((sig = [obj methodSignatureForSelector:sel]))
                    break;
        }
        return sig;
    }

    - (void)forwardInvocation:(NSInvocation *)inv
    {
        for(id obj in self)
            [inv invokeWithTarget:obj];
    }

    @end

然后就可以这样使用

[(NSWindow *)windowsArray setHidesOnDeactivate:YES];

不过不建议这样使用,因为若NSArray实现了此方法,就不会进入转发流程。实现这种功能的一种比较好的方法是使用NSProxy。

五.方法声明

虽然上述机制可以转发当前类中没有实现的方法,但发送消息时仍然需要知道每个消息的方法签名,否则就会有编译器告警。可以通过category来声明转发消息的方法。

六.使用消息转发在子类中处理Delegate消息

当继承一个具有delgate的类,而又需要在子类中处理某些delegate消息,而又不影响对正常Delegate消息的调用时,需要如何处理呢?
一种方法是将子类对象设为自身的delegate,而将外部设置的delegate存储到另一个参数中。在子类中实现所有的delegate方法,处理子类中需要处理的delegate消息,而将子类中不处理的delegate消息再发送到外部delegate。这种方法的缺点在于实现繁琐,在子类中需要实现所有delegate方法,尽管大部分delegate消息又直接转给了外部delegate处理。
另一种比较优雅的方式是使用消息转发,创建一个proxy类,将proxy类设置为父类的delegate,在proxy中分别将消息转发给子类或外部Delegate。
比如,创建一个UISCrollView的子类可使用如下代码
MessageInterceptor.h

@interface MessageInterceptor : NSObject {
    id receiver;
    id middleMan;
}
@property (nonatomic, assign) id receiver;
@property (nonatomic, assign) id middleMan;
@end

MessageInterceptor.m

@implementation MessageInterceptor
@synthesize receiver;
@synthesize middleMan;

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([middleMan respondsToSelector:aSelector]) { return middleMan; }
    if ([receiver respondsToSelector:aSelector]) { return receiver; }
    return [super forwardingTargetForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([middleMan respondsToSelector:aSelector]) { return YES; }
    if ([receiver respondsToSelector:aSelector]) { return YES; }
    return [super respondsToSelector:aSelector];
}

@end

MyScrollView.h

#import "MessageInterceptor.h"

@interface MyScrollView : UIScrollView {
    MessageInterceptor * delegate_interceptor;
    //...
}

//...

@end

MyScrollView.m

@implementation MyScrollView

- (id)delegate { return delegate_interceptor.receiver; }

- (void)setDelegate:(id)newDelegate {
    [super setDelegate:nil];
    [delegate_interceptor setReceiver:newDelegate];
    [super setDelegate:(id)delegate_interceptor];
}

- (id)init* {
    //...
    delegate_interceptor = [[MessageInterceptor alloc] init];
    [delegate_interceptor setMiddleMan:self];
    [super setDelegate:(id)delegate_interceptor];
    //...
}

- (void)dealloc {
    //...
    [delegate_interceptor release];
    //...
}

// delegate method override:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // 1. your custom code goes here
    // 2. forward to the delegate as usual
    if ([self.delegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [self.delegate scrollViewDidScroll:scrollView];
    }
}

@end

MessageInterceptor对象会自动将将子类中实现的delegate消息转发给子类,而将其他所有delegate消息转发给外部设置的delegate对象。

在MessageInterceptor中除了实现forwardingTargetForSelector:方法外,还实现了respondsToSelector:方法,因为UIScrollView在发送delegate消息之前会首先使用respondsToSelector:判断delegate是否实现了该方法,而转发的消息对respondsToSelector:也应返回YES。

参考:
Friday Q&A 2009-03-27: Objective-C Message Forwarding
Objective-C Runtime Programming Guide – Dynamic Method Resolution
Objective-C Runtime Programming Guide – Message Forwarding
Intercept obj-c delegate messages within a subclass
Hacking Block Support Into UIMenuItem
NSObject Class Reference
NSObject Protocol Reference
NSInvocation Class Reference
NSMethodSignature Class Reference

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值