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 = @"";
}
return self; } - (void)printAge
{
Ivar ivar = class_getInstanceVariable([self class], "_age"); object_setIvar(self, ivar, @""); 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

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

对Objective-C中runtime的理解的更多相关文章

  1. 理解Objective C 中id

    什么是id,与void *的区别 id在Objective C中是一个类型,一个complier所认可的Objective C类型,跟void *是不一样的,比如一个 id userName, 和vo ...

  2. 刨根问底Objective-C Runtime

    http://chun.tips/blog/2014/11/05/bao-gen-wen-di-objective%5Bnil%5Dc-runtime-(2)%5Bnil%5D-object-and- ...

  3. 浅谈Objective—C中的面向对象特性

    Objective-C世界中的面向对象程序设计 面向对象称程序设计可能是现在最常用的程序设计模式.如何开发实际的程序是存在两个派系的-- 面向对象语言--在过去的几十年中,很多的面向对象语言被发明出来 ...

  4. Objective C内存管理之理解autorelease------面试题

    Objective C内存管理之理解autorelease   Autorelease实际上只是把对release的调用延迟了,对于每一个Autorelease,系统只是把该Object放入了当前的A ...

  5. Java 泛型 <? super T> 中 super 怎么 理解?与 < ? extends T>有何不同?

    Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同? 简介 前两篇文章介绍了泛型的基本用法.类型擦除以及泛型数组.在泛型的使用中,还有个重要的 ...

  6. 刨根问底Objective-C Runtime(4)- 成员变量与属性

    http://chun.tips/blog/2014/11/08/bao-gen-wen-di-objective[nil]c-runtime(4)[nil]-cheng-yuan-bian-lian ...

  7. Fouandation(NSString ,NSArray,NSDictionary,NSSet) 中常见的理解错误区

    Fouandation 中常见的理解错误区 1.NSString //快速创建(实例和类方法) 存放的地址是 常量区 NSString * string1 = [NSString alloc]init ...

  8. linux中socket的理解

    对linux中socket的理解 一.socket 一般来说socket有一个别名也叫做套接字. socket起源于Unix,都可以用“打开open –> 读写write/read –> ...

  9. IOS 中runtime 不可变数组__NSArray0 和__NSArrayI

    IOS 中runtime 不可变数组__NSArray0 和__NSArrayI 大家可能都遇到过项目中不可变数组避免数组越界的处理:runtime,然而有时候并不能解决所有的问题,因为类簇不一样 # ...

随机推荐

  1. 帝国cms伪静态设置方法(收藏)

    众所周知,动态页面不利于收录和排名.伪静态可以完美的解决这问题,配合百度云加速CDN,可以让动态页面有静态页面一样快的访问速度. 今天开拓族给大家带来帝国CMS伪静态的详细设置方法. 1.栏目设置为动 ...

  2. laravel4.2 Redis 使用

    laravel4.2 Redis 使用 配置文件,app/config/database.php 'redis' => array( 'cluster' => false, 'defaul ...

  3. ruby中的三目操作符和include?操作

    三目操作符:口?口:口 问号前面的是布尔型的判断,true的话执行第二个方块的语句,false的话执行第三个方块的语句例如:value =(nil ? 0 : 1)p value=>1 .inc ...

  4. java instanceof 的理解

    简单来说,java 中的instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例.instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例. 用法 ...

  5. 【blockly教程】第二章 Blockly编程基础

    2.1 Blockly的数据类型 2.1.1 数据的含义  在计算机程序的世界里,程序的基本任务就是处理数据,无论是数值还是文字.图像.图形.声音.视频等信息,如果要在计算机中处理的话,就必须将它们转 ...

  6. 如何在同一个Excel里,对两个很相似的工作簿比对出不同之处

    如何在同一个Excel里,对两个很相似的工作簿比对出不同之处

  7. python中自定义超时异常的几种方法

    最近在项目中调用第三方接口时候,经常会出现请求超时的情况,或者参数的问题导致调用异代码异常.针对超时异常,查询了python 相关文档,没有并发现完善的包来根据用户自定义的时间来抛出超时异常的模块.所 ...

  8. tcp滑动窗口与拥塞控制

    TCP协议作为一个可靠的面向流的传输协议,其可靠性和流量控制由滑动窗口协议保证,而拥塞控制则由控制窗口结合一系列的控制算法实现.一.滑动窗口协议     所谓滑动窗口协议,自己理解有两点:1. “窗口 ...

  9. BZOJ1800_fly飞行棋_KEY

    题目传送门 看数据范围,N<=20! 你没看错,搜索都能过. O(N^2)的做法,就是先求出有几对点之间的距离为圆周长的一半. 然后求C(N,2)即可. code: /************* ...

  10. tarjan算法求最近公共祖先

    tarjian算法 LCA: LCA(Least Common Ancestor),顾名思义,是指在一棵树中,距离两个点最近的两者的公共节点.也就是说,在两个点通往根的道路上,肯定会有公共的节点,我们 ...