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. 用Jquery控制元素的上下移动 实现排序功能

    在页面上,控制元素上下移动,进行排序是我们比较常用的功能,今天我用jQuery 写个 简单方便,功能齐全的实现方式. 话不多说,直接上代码,下面是基础的引入jq和html元素部分: <scrip ...

  2. Python入门 —— 2048实战(字符界面和图形界面)

    2048 game (共4种实现方法) 目录: .. 图形界面 ... pygame 和 numpy .. 字符界面 ... 第一种 ... curses ... wxpython ... 第二种 . ...

  3. linux 操作系统之磁盘管理

    磁盘管理 存储设备:硬盘 , U盘 , 移动硬盘 , 光盘 , 软件. 组装一台电脑 无法被修改 df -h #查看磁盘分区的情况 , 可用的. 查看磁盘或者目录的内容 df “df” 常用的 “-i ...

  4. 在线预览word,excel文档

    Google Doc 示例:https://jsfiddle.net/7xr419yb/ Microsoft Office 示例:https://jsfiddle.net/gcuzq343/

  5. APSC4xSeries_Ver32.exe在win764位提示缺少DLL错误解决办法

    APSC4xSeries_Ver32.exe在win764位提示缺少DLL错误解决办法 从网上下载oatime_epson-me1清零软件,Stylus4xProgram_Ver32的 解决办法:还是 ...

  6. Java实例 Part1:Java基础输出语句

    ** Part1:Java基础输出语句 ** 第一部分最基础,就是标准的输出语句. ps:(目前还没熟悉这个编辑器,先尝试一下) Example01 : 输出"hello world&quo ...

  7. Divisibility题解

    From lyh 学长 2018.5.3 信(liang)心(liang)杯T3 一道略弱的数论题. 题目描述 给定 n个数,问是否能从中选出恰好 k个数,使得这些数两两之差可以被 m 整除. 输入输 ...

  8. 使用MATLAB设计FIR滤波器

    1.      采用fir1函数设计,fir1函数可以设计低通.带通.高通.带阻等多种类型的具有严格线性相位特性的FIR滤波器.语法形式: b = fir1(n, wn) b = fir1(n, wn ...

  9. HDU 5972 Regular Number

    Regular Number http://acm.hdu.edu.cn/showproblem.php?pid=5972 题意: 给定一个字符串,求多少子串满足,子串的第i位,只能是给定的数(小于等 ...

  10. LeetCode: 55. Jump Game(Medium)

    1. 原题链接 https://leetcode.com/problems/jump-game/description/ 2. 题目要求 给定一个整型数组,数组中没有负数.从第一个元素开始,每个元素的 ...