对Objective-C中runtime的理解

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

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

2.若是是在支持垃圾收集环境中,进行一些处理,因为ios不支持垃圾收集,因此咱们没必要关心这步。缓存

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

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

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

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

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

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

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

相关文章
相关标签/搜索