对Objective-C中runtime的理解

Objective-C是面向runtime(运行时)的语言,在应用程序运行的时候来决定函数内部实现什么以及做出其它决定的语言。程序员可以在程序运行时创建,检 查,修改类,对象和它们的方法,Objective-C runtime库也负责找出方法的最终执行代码。举例说明,当程序执行[object doSomething]时,不会直接调用doSomething这个方法,而是一条消息(doSomething)会发送给对象(object)。runtime库里有个c函数来传递这条消息,[object doSomething]这条代码将会被转化成c函数形式的函数调用:objc_msgSend(object,@selector(doMethodWith:)),函数内部按照以下顺序进行:

1.检查接受对象是否为nil,如果是,调用nil处理程序。直接表现就是什么都不做。

2.如果是在支持垃圾收集环境中,进行一些处理,由于ios不支持垃圾收集,所以我们不必关心这步。

3.检查类缓存中是不是已经有方法实现了,有的话,直接调用。(每个类都有一些经常执行的方法,有些方法则很少执行,当代码里执行到某一个方法时如果都要去类的方法列表里找一遍就太影响效率了,所以运行时系统会把调用过的方法缓存起来,以提高查找的效率,这一步就是在缓存里查找)

4.比较请求的选择器(就是方法名,比如doSomething)和类中定义的选择器,如果找到了,调用方法执行。

5.比较请求的选择器和父类中定义的选择器,找到就执行,找不到继续向父类的父类寻找,如果找到就执行,如果一直找到根类都找不到,那么将执行第6步。

6.调用resolveInstanceMethod:(或resolveClassMethod:)。这个方法返回一个BOOL值,如果返回no,将执行第7步,如果返回yes,则重新从上面的第1步开始。我们可以在这个方法里用runtime给我们提供的class_addMethod函数为类添加方法,这便是给类动态加方法的一个机会。

7.调用forwardingTargetForSelector:,这个方法返回一个对象,消息将转发给我们返回的对象,这是我们可以把通过以上6步都不能处理的方法转发到其他对象上的机会。

8.调用methodSignatureForSelector:,生成一个方法签名,创建一个NSInvocation(这个类把target,selector,方法签名和参数打包在一起)并传给forwardInvocation:方法。在forwardInvocation:里这是我们对这个消息进行处理的最后机会,里如果我们不做处理,默认的程序就会crash,抛出找不到该方法的错误信息。

在这个过程中我们可以给类添加方法(第6步),可以将消息转发到别的对象上(第7步),对NSInvocation(包含了这条消息的所有信息)进行我们想要的处理(第8步)。

下面我们将通过代码来看看如何在运行时给类添加方法。

假设我们有个Person类,

@interface Person : NSObject

@end

@implementation Person

@end

这个类没有任何方法,接下来我们在执行的地方创建一个Person对象,出于测试目的,我们将person定为id类型,这样我们就可以随便调用一个NSObject子类的方法,这里我们调用tableView的reloadData方法。

id person = [[Person alloc] init];
[person reloadData];

然后执行以上语句,我们来详细看看执行[person reloadData]时的过程,[person reloadData]执行时其实是c函数objc_msgSend(person,@selector(reloadData)),进入函数内部,由于在Person类中我们并没有reloadData这个方法,按照我们前面列出的顺序,于是就走到了第6步,消息分发函数内部调用这个类中的resolveInstanceMethod:方法,我们可以在这一步中给person类加上reloadData方法,

@implementation Person

static void testIMP(id self, SEL _cmd) 
{ NSLog(@"reload data"); } + (BOOL)resolveInstanceMethod:(SEL)aSEL
{ if ([NSStringFromSelector(aSEL) isEqualToString:@"reloadData"]) {//这里只是测试,只针对person调用了reloadData这个方法 class_addMethod([self class], aSEL, (IMP)testIMP, "v@:");//这样我们就在运行时为person类动态添加了reloadData方法。 return YES;//retrun YES后消息分发函数又会从头开始执行,由与我们上面已经给类添加了relodData方法,消息分发函数将会终止在第5步不再往下执行。 } return NO; } @end

对class_addMethod(Class cls, SEL name, IMP imp,const char *types)函数参数的说明:

Class cls:就是我们要给哪个类添加方法

SEL name:添加的方法叫什么名字(方法名)

IMP:一个函数指针,这是它的定义:typedef id (*IMP)(id self,SEL _cmd,...),由编译器生成,指向最终执行的函数的地址。该函数类型接受一个目标对象,一个选择器,以及其他参数。

const char *types:方法的返回类型、参数类型编码,我们可以用@encode(type)获得类型的字符串编码,比如上面的-(void)reloadData方法的返回类型和参数类型(有两个参数会隐式传递)编码为“v@:”,详细是v=@encode(void),@=encode(id),:=encode(SEL),(每个object方法会把id self和SEL _cmd隐式传递)

到这,我们就成功的为person类添加了他本来没有的方法。

下面我们继续来看通过运行时如何将消息转发。还是这个Person类,假如我们没有动态的给类添加方法,那么消息分发函数将会运行到第7步,我们可以在这一步通过方法forwardingTargetForSelector:把消息转发到另一个对象上。

假设有另一个类叫Computer类,它有实现reloadData方法。

@interface Computer : NSObject
-(void)reloadData;
@end @implementation Computer -(void)reloadData {   NSLog(@"reload data"); } @end

下面我们就在Person类里将消息转发到Computer类去

@implementation Person

- (id)forwardingTargetForSelector:(SEL)selector 
{ if ([NSStringFromSelector(selector) isEqualToString:@"reloadData"]) { return [[Computer alloc] init]; } return nil; } @end

运行后,[person reloadData]真正执行的是Computer里的reloadData方法。

假如我们在第6步和第7步都不做处理,那么消息分发函数将会进入到第8步,这是我们处理这条消息的最后机会,如果我们不处理,程序将会抛出unrecognized selector sent to instance错误。接下来我们来看看如何处理。

因为person类里没有reloadData这个方法,所以返回了一个空的方法签名,最终程序报错崩溃,所以我们要在报错之前返回一个方法签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

获得方法签名后,会生成一个NSInvocation传给person类的以下方法,在这个方法中,我们就可以对该条消息处理了,我们可以改变消息的target,也可以改变selector,就看我们的需要了,以下我们将target改为Computer,将消息转发到Computer类去,最终执行的是Computer里的reloadData方法。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    Computer *test = [[Computer alloc] init];
    [anInvocation setTarget:test];
    [anInvocation invoke];
}

利用运行时的这种动态特性,我们可以改变那些没有源代码的对象(包括系统对象)的行为。比如我们要对UINavigationController的pushViewController:animated:进行改写,每当我们调用pushViewController: animated:时就打印一下日志。我们用分类的方式给UINavigationController添加一个方法,然后再我们需要这个功能的地方运行一下我们的分类方法,这之后的所有pushViewController: animated:将会执行我们指定的行为。

@interface UINavigationController (Mypush)

+ (void)tellmePushVC;

@end


@implementation UINavigationController (Mypush)

typedef void (*voidIMP)(id, SEL, ...);

//用来保存系统原有的实现
static voidIMP sOrigPushVCIMP = NULL;

//我们自定义的实现
static void tellmePush(id self, SEL _cmd, id viewcontroller,bool flag) {
    
    //打印日志
    NSLog(@"push push vc");
    
    // 调用系统原有的实现
    if (sOrigPushVCIMP) {
        sOrigPushVCIMP(self, _cmd, viewcontroller,flag);
    }
}

+ (void)tellmePushVC
{
    //确保以下代码只运行一次,否则将会引发递归循环
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken,^{
        
        Class class = [self class];
        
        SEL sel = @selector(pushViewController:animated:);
        
        Method origMethod = class_getInstanceMethod(class,sel);
        
        sOrigPushVCIMP = (void *)method_getImplementation(origMethod);//把系统原有的实现存起来
        
        method_setImplementation(origMethod, (IMP)tellmePush);//把新实现替换旧实现
    });
}
@end

在我们需要的地方写上[UINavigationController tellmePushVC];这之后凡是我们调用了系统的pushViewController: animated:方法,就会先打印日志,再执行系统原有的pushViewController: animated:方法。通过以上手段,就达到了给系统方法添加行为的目的。

以上都是对方法的操作,我们还可以利用运行时的公开的c函数接口修改类的实例变量的值。还是以Person类举例,假如Person类里有个_age实例变量,下面我们就在运行时修改_age的值。

@interface Person : NSObject
- (void)printAge;
@end @implementation Person { NSString *_age; } - (id)init
{ if ((self = [super init])) { _age = @"18"; } return self; } - (void)printAge { Ivar ivar = class_getInstanceVariable([self class], "_age"); object_setIvar(self, ivar, @"100"); NSLog(@"age %@",_age); } @end

这样我们便在运行时把实例变量的值修改了。

我们都知道分类只能给类添加方法,不能添加实例变量,但是利用运行时,我们可以给分类动态添加实例变量,这样即使我们不创建子类,也能够对类动态扩展。这个功能称之为关联引用。需要注意的是通过分类添加的实例变量并不是真正的实例变量,所以在对象复制和归档的时候要特别注意。我们给Person类创建一个分类,添加属性address。

#import "Person.h"

@interface Person (AddProperty)

@property (strong, nonatomic) NSString *address;

@end

 
#import "Person+AddProperty.h"
#import <objc/runtime.h>

@implementation Person (AddProperty)

static char key;//由于一个对象可以有很多个关联引用,所以需要一个key来区分,一般我们用一个静态变量的地址作为key。

- (void)setAddress:(NSString *)address
{
    objc_setAssociatedObject(self, &key, address, OBJC_ASSOCIATION_COPY);
}

- (NSString *)address
{
    return objc_getAssociatedObject(self, &key);
}
@end

这样就完成了在分类中给类添加实例变量。