runtime 几个要点总结-方法交换


前言:

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


(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
代码示例一:
 
  @implementation  CA
  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  *格式的名称来修改变量的值,而不是通过变量

  


后面主要介绍oc类的运行时行为。这里面包括运行时方法的更换,消息的转发,以及动态属性。这些对于面向方面编程AOP的热爱者还是很有用的,当然还有很多其他的作用,例如可配置编程之类的。但是按照我之前在java和dotnet的编程经验,这些都是使用性能为代价的。所以尽量在程序开始部分完成操作,而不是用于程序行为的代码。

第一段代码是方法交换。下面的例子将使用自己的代码替换[NSString stringByAppendingPathComponent]方法的实现。

这里是替换代码:

1
2
3
4
5
6
7
8
9
10
11
NSString * NSStringstringByAppendingPathComponent(id SELF, SEL _cmd, NSString * path){
 
//开发文档推荐的是这种定义形式,其实在方法交换场景下,这是没有必要的,你甚至可以给一个().但是如果你要替换的方法实际并不存在,那么这个定义形式是必须的。
 
NSLog(@” this  is  a fake imp for  method %@”, NSStringFromSelctor(_cmd));
 
NSLog(@”I won’t do  anything! but I will return  a virus!”); //疯狂医生的逻辑
 
return  [NSString stringWithCString: “virus!!!” encoding:NSUTF8StringEncoding];
 
}

下面是方法交换的代码:

Class strcls = [NSString class];

SEL oriStringByAppendingPathComponent = @selector(stringByAppendingPathComponent:);

class_replaceMethod(strcls,

  oriStringByAppendingPathComponent,

  (IMP)NSStringstringByAppendingPathComponent,

  NULL);

//后面的type参数可以是NULL。如果要替换的方法不存在,那么class_addMethod会被调用,type参数将被用来设置被添加的方法

/*

Apple development reference 的描述如下:

type参数:An array of characters that describe the types of the arguments to the method. For possible values, see Objective-C Runtime Programming Guide > Type Encodings. Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).

  • If the method identified by name does not yet exist, it is added as if class_addMethod were called. The type encoding specified by types is used as given.

  • If the method identified by name does exist, its IMP is replaced as if method_setImplementation were called. The type encoding specified by types is ignored.

*/




objective-c 2.0中增加了一个新的关键字@dynamic, 用于定义动态属性。所谓动态属性相对于@synthesis,不是由编译器自动生成setter或者getter,也不是由开发者自己写的setter或getter,而是在运行时动态添加的setter和getter。

一般我们定义一个属性都是类似以下方法:

1
2
3
4
5
6
7
8
@interface Car:NSObject;
@property (retain) NSString* name;
@end
 
 
@implement Car;
@synthesize name;
@end

这种情况下,@synthesize关键字告诉编译器自动实现setter和getter。另外,如果不使用@synthesize,也可以自己实现getter或者setter

1
2
3
4
5
6
7
@implement Car;
(NSString*) name{
     return  _name;
}
( void ) setName:(NSString*) n{
     _name = n;
}

现在通过@dynamic,还可以通过第三种方法来实现name的setter和getter。实现动态属性,需要在代码中覆盖resolveInstanceMethod来动态的添加name的setter和getter。这个方法在每次找不到方法实现的时候都会被调用。事实上,NSObject的默认实现就是抛出异常。

参考以下代码:

下面是定义动态属性和实现动态属性的代码:

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
@interface Car:NSObject
@property (retain) NSString* name;
@end
 
---car.m
( void ) dynamicSetName(id SELF, SEL _cmd, NSString * n){
//这个定义形式是必须的。
//结合下面的类型描述字符v表示返回为void
//@表示第一个参数id
//:表示SEL
//@表示参数n
 
     NSLog(@ "the new name is going to send in:%@!" , n);
}
@implement Car
@dynamic name;
-( BOOL ) resolveInstanceMethod:(SEL) sel{
     NSString * method = NSStringFromSelector(sel);
     if ([method isEqualToString:@ "setName:" ]){
         class_addMethod([self class ], sel, (IMP)dynamicSetName, "v@:@" ); //类型描述字符,可以参考开发文档中有关@encode关键字的说明。
         return  YES:
     }
     return  [super resolveInstanceMethod:sel];
}
@end;

上面的代码动态实现了name的setter,当然这个时候如果想要调用car.name,就会抛出错误,因为我并没有实现它的getter。





第七篇中讲动态属性时,提到了resolveInstanceMethod,这个方法不仅在这里用,还用来实现消息的转发。

消息的转发就是向对象发送一个它本身并没有实现的消息,在运行时确定它实际产生的行为。

举个例子来说,一个Person对象,在运行时根据实际情况,决定是否响应fly这样的方法。如果条件具备,则fly被响应。否则,则不具备这样的方法。类似于AoP的做法。

要实现消息转发,需要覆盖三个方法:

1, resolveInstanceMethod(可选),这个方法为你提供了一个机会,在消息被发现本身没有在类中定义时你可以通过class_addMethod将它添加进去。如果你不这样做,不管你最终返回YES还是NO,程序都会jmp到下一个方法。

2, –(NSMethodSignature*)methodSignatureForSelector: (SEL) sel;通过覆盖这个方法,可以将你要转发的消息的签名创建并返回。如果在这里你返回nil,那么就会直接抛出异常。如果返回一个签名,则程序会jmp到第三个方法。这里返回的方法签名,必须满足两个条件之一(方法名相同||输入参数相同).

3, –(void)forwardInvocation:(NSInvocation * )anInvocation;在这里进行实际的方法调用。参考以下代码:

复制代码
#import <Foundation/Foundation.h>
#import  " Human.h "
@interface Plane : NSObject
-( void)fly:(Human*)p;
-( int)fly1;
-( void)fly2:(Human*)p withBird:(NSString*)birdName;
@end

#import  " Plane.h "

@implementation Plane
-( void)fly:(Human*)p{
    NSLog( @" Fly with a guy whose information is \ "%@\ "", [p description]);
    NSLog( @" fly...... ");
}
-( int)fly1{
    NSLog( @" I can fly in Plane for fly1 ");
     return  0;
}
-( void)fly2:(Human*)p withBird:(NSString*)birdName{
    NSLog( @" Fly with a guy whose information is \ "%@\ "", [p description]);
    NSLog( @" fly......bird:'%@'. ", birdName);
}

@end

#import <Foundation/Foundation.h>
void dynamicMethod(id self, SEL _cmd,  float height);

@interface Human : NSObject{
     float _height;
}
@property(retain, nonatomic) NSString * name;
@property( readonlyfloat weight;
@property  float height;
-(id) initWithWeight:( float)weight;
-(NSString*)description;
@end


#import <objc/runtime.h>
#import  " Human.h "
#import  " Plane.h "
void dynamicMethod(id self, SEL _cmd,  float height){
    NSLog( @" dynamicMethod:%@ ", NSStringFromSelector(_cmd));
     //  here is a question. how to implement the statements to assign the property of id(human.height);
    
//  looks like the dynamic property is just for linkage.
    
//  当赋值发生时,由于动态函数的全局性,可以访问其他全局变量,对象。所以更像一种链接机制
}
@implementation Human
@synthesize name;
@synthesize weight;
@dynamic height;
-(id)initWithWeight:( float)w{
     if(self=[super init]){
        weight = w;
    }
     return self;
}
-(NSString*)description{
     return [NSString stringWithFormat: @" The human whose name is \ "%@\ " . weight is [%f]. height is [%f] ",
            self.name, self.weight, self.height];
}
-( float)height{
     return _height;
}
-( void)fly2{
    NSLog( @" yes I can fly in 2! ");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
    NSString* method = NSStringFromSelector(sel);
     if([method isEqualToString: @" setHeight: "]){
        class_addMethod([self  class], sel, (IMP)dynamicMethod,  " v@:f "); 
    }
     return [super resolveInstanceMethod:sel];
}
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector{
    NSMethodSignature * signature = [super methodSignatureForSelector:selector];
     if(!signature&&[NSStringFromSelector(selector) isEqualToString: @" fly "]){
         // signature = [[self class] instanceMethodSignatureForSelector:@selector(fly2)];
        SEL newSel = NSSelectorFromString( @" fly1 ");   // will ok.
        
//  SEL newSel = NSSelectorFromString(@"fly:"); // will ok.
        
//  SEL newSel = NSSelectorFromString(@"fly2:withBird"); // will wrong.
        
// 这里返回的消息,必须符合以下条件之一:
        
// 方法名相同
        
// 输入参数相同
         if([Plane instancesRespondToSelector:newSel]){
            signature = [Plane instanceMethodSignatureForSelector:newSel];
        }
    }
     return signature;    
}
-( void)forwardInvocation:(NSInvocation *)anInvocation{
    NSString * selName = NSStringFromSelector([anInvocation selector]);
    NSMethodSignature * sig = [anInvocation methodSignature];
    NSLog( @" the signature %@ ", [NSString stringWithCString:[sig methodReturnType] encoding:NSUTF8StringEncoding]);
     if([selName isEqualToString: @" fly "]){
        Plane * p = [[Plane alloc] init];
        SEL newSel = NSSelectorFromString( @" fly: ");
        IMP imp = [p methodForSelector:newSel];
        imp(p, newSel, self);
        [p release];
    }
}
-( void)dealloc{
    [self setName:nil];
    [super dealloc];
}
@end

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import  " Human.h "
#import  " Plane.h "
int main ( int argc,  const  char * argv[])
{

    @autoreleasepool {
        
         //  insert code here...
        
        Human * h = [[Human alloc] initWithWeight: 17.3];
        h.name= @" Chris ";
        h.height =  18.0;
        NSLog( @" %@ ", [h description]);
        [h fly];
        [h release];
        h=nil;
复制代码

}} 




学习到目前为止,我看到oc实现的序列化方式有两种:NSKeyedArchiver,NSPropertyListSerialization。

在这两种序列化方式中,NSData都是序列化的目标。两种方式的不同点在于NSPropertyListSerialization只是针对字典类型的,而NSKeyedArchiver是针对对象的。(补充一下,在Mac OS环境下,还可以使用NSArchiver获得更加精简的二进制序列化内容,但是NSArchiver在iOS环境下不支持)。

首先讲NSPropertyListSerialization,这个相对简单,只要创建一个NSDictionary,然后调用NSPropertyListSerialization dataFromPropertyList, 将内容写入NSData中就可以了。如果要读入,就调用propertyListFromData.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSString * filepath = @”…”; //omitted.
NSString * err; //不需要初始化。如果有错误发生,会被复制。
NSDictionary * props = [NSDictionary dictionaryWithObjectsAndKey:@”Lucy”, @"name”,
             @ "Beijing, China”, @" city”,
             @ "supervior”, @" position”,
             @ "Qitiandasheng”, @" company”, nil];
NSData * data = [NSPropertyListSerialization dataFromPropertyList:props
         format:NSPropertyListXMLFormat_v1_0
         errorDescription:&err];
if (!err){
     [data writeToFile:filePath atomically:YES]; //写入文件
} else {
     NSLog(@ "error with:%@" , err);
}

然后再来看NSKeyedArchiver。从基本的代码示例来看也很简单:

1
2
3
4
5
Person  * lucy  = [[Person alloc] initWithName:@ "lucy" ];
lucy.address = @ "Beijing, China" ;
 
NSData * data = [NSKeyedArchiver archiveDataWithRootObject:lucy];
[data writeToFile:filePath];

 

这里要求Person类必须事先了NSCoding协议。NSCoding协议中包含两个必须事先的方法initWithCoder和encodeWithCoder. 参考下面这两个方法的实现:

1
2
3
4
5
6
7
8
9
-( void )encodeWithCoder:(NSCoder *)aCoder{
     [aCoder encodingObject:[NSNumber numberWithInt:self.age] forKey @ "age" ];
}
-(id)initWithCoder:(NSCoder*) aDecoder{
     if (self=[super init]){
         self.age = ( int )[(NSNumber*)[aDecoder decodeObjectForKey:@ "age" ] intValue];
     }
     return  self;
}

这里之所以用int age作为序列化属性的示例,是为了引出另一个话题,就是在序列化时对基础类型的处理。NSCoder中是不能存储基础类型的,包括int,float,char *什么的。要将这些进行序列化,必须要进行封装。

一般封装有两种方式。对于数值基本类型,使用Cocoa提供的封装,NSNumber就可以了。对于定长的struct,要分情况,如果struct的所有类型都是定长,并且成员中没有struct类型,那么可以直接使用NSValue来封装;否则,需要自己想办法进行key-value编写。char * 应该直接使用NSString进行封装。

























  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值