iOS学习之Runtime(二)
前面已经介绍了Runtime系统的概念、作用、部分技术点和应用场景,这篇将会继续学习Runtime的其他知识。
一、Runtime技术点之类/对象的关联对象
关联对象不是为类/对象添加属性或者成员变量(因为在设置关联后也无法通过copyIvarList或者copyPropertyList取得),而是为类添加一个相关的对象,通常用于存储类信息,例如存储类的属性列表数组,方便以后字典转模型的操作。
Runtime为我们提供了三个函数进行关联对象的相关操作:
/**
* 为某个类关联某个对象
*
* id object,当前对象
* const void *key,关联的key,是C字符串
* id value,被关联的对象
* objc_AssociationPolicy policy,关联引用的规则,取值有以下几种:
* enum {
* OBJC_ASSOCIATION_ASSIGN = 0,
* OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
* OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
* OBJC_ASSOCIATION_RETAIN = 01401,
* OBJC_ASSOCIATION_COPY = 01403
* };
*
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) /**
* 获取到某个类的某个关联对象
*
*/
id objc_getAssociatedObject(id object, const void *key) /**
* 移除已经关联的对象
*
*/
void objc_removeAssociatedObjects(id object)
我们可以将关联对象的设置与获取封装起来,用于方便获取类的属性列表。
@implementation Person const char *propertiesKey = "propertiesKey"; + (NSArray *)properties
{
// 通过key取出关联对象
NSArray *pList = objc_getAssociatedObject(self, propertiesKey);
if (pList != nil)
{
return pList;
} // 如果没有关联对象,则取出成员变量和属性,存入数组
unsigned int outCount;
Ivar *ivarList = class_copyIvarList(self, &outCount); NSMutableArray *array = [NSMutableArray arrayWithCapacity:outCount]; for (int i = ; i < outCount; i++)
{
Ivar *ivar = &ivarList[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)];
NSString *key = [name substringFromIndex:];
[array addObject:key];
} // 释放ivarList
free(ivarList); // 设置关联对象
objc_setAssociatedObject(self, propertiesKey, array, OBJC_ASSOCIATION_COPY_NONATOMIC); return array.copy;
} - (NSString *)description
{
NSLog(@"name: %@---------age: %d---------height: %f", self.name, self.age, _height);
return nil;
} @end
这样的话,我们只需在外部调用这个类方法,即可获得该类的所有属性列表。
不过我在网上也看到有人将关联对象应用到分类中,目前来说我还不能确定这种做法是否恰当,不过倒是可以提供一种思路。这种用法的初衷是在不使用继承的方式下给系统类添加一个公共变量。我们都知道,分类只能为类添加方法,而延展里面为类添加的变量或方法都是私有的(这里简单介绍一下延展的作用,延展其实就是C语言中的前向声明,不过现在苹果已经弥补了这个缺陷,所以这里不再细述)。那么怎样才能在不使用继承的方式下给系统类添加一个公共变量呢?这里就用的了关联对象。
我们可以在NSDictionary的分类MyDict.h中新增一个属性property1,一般情况下如果我们只声明了这些变量,在外面使用的时候就会报错,因为分类是不允许你这么做的。那么我们就需要通过设置关联对象来实现property1的set、get方法,其实原理很简单,就是在set方法中,通过一个key将property1的值关联到类中;在get方法中,再通过这个key将property1的值取出即可。
1 const char *property1Key = "property1Key";
2
3 - (void)setProperty1:(NSString *)property1
4 {
5 // 通过key设置关联对象
6 objc_setAssociatedObject(self, property1Key, property1, OBJC_ASSOCIATION_COPY_NONATOMIC);
7 }
8
9 - (NSString *)property1
10 {
11 // 通过key获取关联对象
12 return objc_getAssociatedObject(self, property1Key);
13 }
这样我们就可以在外部使用这个分类的新增属性了。同样的,我们也可以为其设置block,原理都是一样的,这里就不再累述了。
二、Runtime技术点之消息转发
在学习消息转发知识之前,我们需要知道几个概念:
1、OC中调用方法就是向对象发送消息。比如[person walk];实际上是给person对象发送了walk这个消息。调用类方法也一样,类实际上也是一个对象,是元类的实例。方法调用的流程如下:
(1)系统会查看这个对象能否接收这个消息(查看这个类有没有这个方法,或者有没有实现这个方法);
(2)如果不能接收这个消息,就会调用下面这几个方法,给出“补救”的机会;
(3)如果在这几个方法中都没有做处理,那么程序就会报错;
需要注意的是,下面这几个方法调用是有先后顺序的,并且如果前一个方法做出相应处理了,就不会再调用后面的方法了。
+ resolveInstanceMethod:(SEL)sel // 实例方法没有实现时会调用这个方法
+ resolveClassMethod:(SEL)sel // 类方法没有实现时会调用这个方法
- (id)forwardingTargetForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
其中:- (void)forwardInvocation:(NSInvocation *)anInvocation; 需要跟 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; 结合使用才能实现消息转发,methodSignatureForSelector的作用是为方法创建一个有效的签名。如果没有找到方法的对应实现,则会返回一个空的方法签名,最终导致程序崩溃。关于怎样使用它们实现消息转发,下面会介绍。
2、SEL的概念。
SEL就是对方法的一种包装。包装的SEL类型数据,它对应相应的方法地址,找到方法地址就可以调用方法。在内存中每个类的方法都存储在类对象中,每个方法都有一个与之对应的SEL类型的数据,根据一个SEL数据就可以找到对应的方法地址,进而调用方法。
每个类都有一个包含SEL和对应的IMP的Method列表,也就是说一个Method包含着一个SEL和一个对应的IMP,而消息转发就是将原本的SEL和IMP的这种对应关系给分开,跟其他的Method重新组合。
SEL类型的定义:typedef struct objc_selector *SEL
3、OC中的方法默认被隐藏了两个参数:self和_cmd。self指向对象本身,_cmd指向方法本身。比如- (void)walk; 这个方法实际有两个参数:self和_cmd。再比如- (void)walk:(NSString *)address; 这个方法实际有三个参数:self、_cmd和address。而且self的类型必须是id,_cmd的类型必须是SEL,这也就解释了为什么_cmd能够指向方法本身了,因为_cmd的类型就是SEL,而SEL就是对方法的一种包装。
有了对上面这些概念的认知,我们才能更好的理解消息转发的原理与实现。
(一)、动态添加方法实现消息转发
根据概念1我们知道,假如一个方法没有对应的实现,那么系统首先会调用+ (BOOL)resolveInstanceMethod:(SEL)sel; 来进行“补救”,那我们是否可以在这里手动添加一个该方法对应的实现呢?答案是肯定的,这也就是有些文档中提到的动态添加方法。现在假设Person.h中有一个- (void)walk; 方法,但是Person.m中并没有实现它,现在我们需要重写+ (BOOL)resolveInstanceMethod:(SEL)sel; 来实现动态添加方法。
@implementation Person + (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selString = NSStringFromSelector(sel);
if ([selString isEqualToString:@"walk"])
{
class_addMethod(self, @selector(walk), (IMP)goTo, "v@:");
}
return [super resolveInstanceMethod:sel];
} // 这是C语言的语法
void goTo(id self, SEL sel)
{
NSLog(@"Person walk.");
} @end
这里有几点需要解释:
(1)根据概念2我们知道,SEL是对方法的封装,那么通过SEL我们可以获取到方法名,只有在walk被调用时,我们才动态添加这个方法的实现;
(2)我们再来分析一下class_addMethod。官方的解释是这样的:Adds a new method to a class with a given name and implementation. 直接可以理解为给类添加一个新的方法。
第一个参数:The class to which to add a method. 要添加方法的类。
第二个参数:A selector that specifies the name of the method being added. 可以理解为没有实现的方法名称。
第三个参数:A function which is the implementation of the new method. The function must take at least two arguments—self
and _cmd
. 要添加的方法实现。注意,这个方法最少要有两个参数:self和_cmd。
第四个参数:An array of characters that describe the types of the arguments to the method. 描述要添加的方法的参数类型的数组。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). 因此这个方法最少要有两个参数:self和_cmd,并且第二个字符和第三个字符必须是“@:”,第一个字符是这个方法的返回值类型。
(3)根据概念3我们知道,OC中的方法默认被隐藏了两个参数,但是C语言并非如此,而Runtime又是基于C语言和汇编的,所以也就很好理解为什么这个方法的实现必须要有self和_cmd这两个参数了。但是“@:”又是什么东西?还记得上一篇中我们提到的类型编码吗?具体可以看这里。“@”代表的就是对象,也就是对应这里的self;“:”代表的就是SEL,也就是对应这里的_cmd;而上面的“v”则是代表这个方法的返回值是void类型。
这样一来,当我们调用[person walk]; 时,实际上调用的就是goTo方法,所以最终打印结果为:
-- ::54.073 RunTimeTest[:] Person walk.
(二)、切换消息接收者实现消息转发
消息转发的另一种形式相比起来更容易理解,直接将消息转发给其他对象,相当于调用其他对象的同名方法,这就用到了- (id)forwardingTargetForSelector:(SEL)aSelector;
现在我们再创建一个Car类,同样在Car.h中声明一个方法- (void)walk; 并且在Car.m中实现它,然后重写Person类的- (id)forwardingTargetForSelector:(SEL)aSelector; 注意,此时不要在+ (BOOL)resolveInstanceMethod:(SEL)sel 做任何处理。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSString *selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"walk"])
{
// 将消息转发给Car
return [[Car alloc] init];
} return [super forwardingTargetForSelector:aSelector];
}
外部同样是调用[person walk]; 这样就实现了将消息由Person转发到Car中了。
我们还可以利用- (void)forwardInvocation:(NSInvocation *)anInvocation; 和- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; 结合来实现消息转发。如果一个对象收到一条无法处理的消息,运行时系统会在抛出错误前,给该对象发送一条forwardInvocation:消息,该消息的唯一参数是个NSInvocation类型的对象,该对象封装了原始的消息和消息的参数。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSign = [super methodSignatureForSelector:aSelector];
if (!methodSign)
{
// 手动设置方法的有效签名
methodSign = [Car instanceMethodSignatureForSelector:aSelector];
} return methodSign;
} - (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
NSString *selString = NSStringFromSelector(selector);
if ([selString isEqualToString:@"walk"])
{
if ([Car instancesRespondToSelector:selector])
{
// 消息调用
[anInvocation invokeWithTarget:[[Car alloc] init]];
}
}
}
三、Runtime技术点之交换方法实现
交换方法实现的需求场景还是比较多的,假设我们写了一个功能性的方法,该方法在整个项目中被多次调用,当需求更改时,要求使用另一种功能代替现有的这个功能,这个时候我们通常有以下几种做法:
(1)将这个方法的现有实现删掉,重新实现新的功能;
(2)重新实现一个方法,将项目中所有调用现有方法的地方,都改成调用新的方法;
......
这两种做法无疑都存在一定的缺陷,第(1)中方案,假设需求又要再改成之前的功能呢?这种现象是很常见的。第(2)种方案,耗时耗力,实施起来太麻烦。
那利用Runtime该怎么操作呢?我们确实还是需要重新实现一个方法的,因为是一个新的功能需求嘛,但是原来的方法我们不去动它,只需在Runtime时将它们的实现交换一下即可,听起来是不是很简单呢?那就直接上代码吧。
@implementation Person - (void)walk
{
NSLog(@"Person walk.");
} - (void)eat
{
NSLog(@"Person eat.");
} + (void)load
{
Method methodOne = class_getInstanceMethod(self, @selector(walk));
Method methodTwo = class_getInstanceMethod(self, @selector(eat)); method_exchangeImplementations(methodOne, methodTwo);
} @end
交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在第一次使用的时候调用,当有分类的时候会调用多次。通过method_exchangeImplementations我们将walk和eat方法的实现进行了交换,这样在外边调用[person walk]; 时,实际上执行的是eat中的实现。
有两点是需要注意一下的:
(1)如果两个方法都是有参数的,那么参数的类型必须是匹配的,也即参数的类型必须一致;但是如果一个有参数,一个没有参数,经过测试,也是可以执行成功的。
(2)如果方法一调用了方法二,就像这样:
- (void)walk
{
NSLog(@"Person walk.");
} - (void)eat
{
NSLog(@"Person eat."); [self walk];
}
那么在执行交换方法实现之后,需要将调用方法二的地方改成调用方法一,就像这样:
- (void)walk
{
NSLog(@"Person walk.");
} - (void)eat
{
NSLog(@"Person eat."); [self eat];
}
否则会造成死循环。其实很好理解,交换之后,walk方法的实现实际已经变成了eat的实现,再去调用walk时相当于调用的eat,所以会一直调用下去。
如果明白了下面这个原理,上面的这个技术点很好理解:
任何一个方法都有两个重要的属性:SEL是方法的编号,IMP是方法的实现,方法的调用过程实际上去根据SEL去寻找IMP。
ps:好了,关于iOS的Runtime学习,就先整理到这吧,有一些东西只是停留在原理上,还没有实际应用到具体场景,所以还是有些地方是不太透彻的,欢迎大家评论交流,共同进步。
代码地址仍然是上一篇中的地址:GitHub,依然是每一个知识点对应一个版本,需要的小伙伴可以下载查看。
iOS学习之Runtime(二)的更多相关文章
- iOS学习之Runtime(一)
一.Runtime简介 因为Objective-C是一门动态语言,所以它总是想办法把一些决定性工作从编译链接推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system ...
- ios学习笔记(二)第一个应用程序--Hello World
原文地址:http://blog.csdn.net/shangyuan21/article/details/18416537 上一篇文章,Windows7上使用VMWare搭建iPhone开发环境介绍 ...
- IOS学习之路二十(程序json转换数据的中文字符问题解决)
ios请求web中的json数据的时候经常出现乱码问题: 例如请求结果可能如下:"\U00e5\U00a5\U00bd\U00e8\U00ae\U00a4" 在网上查到的解决方法是 ...
- ios学习笔记(二)之Objective-C类、继承、类别和协议
二:Objective-C类与继承和协议 在前面已经提过了对象的初始化,这里首先讲的是变量. 2.1 变量 局部变量(内部变量): 局部变量是在方法内作定义说明的,其作用域仅限于方法内,离开方法后使用 ...
- iOS学习笔记(二)——Hello iOS
前面写了iOS开发环境搭建,只简单提了一下安装Xcode,这里再补充一下,点击下载Xcode的dmp文件,稍等片刻会有图一(拖拽Xcode至Applications)的提示,拖拽至Applicatio ...
- iOS学习笔记42-Swift(二)函数和闭包
上一节我们讲了Swift的基础部分,例如数据类型.运算符和控制流等,现在我们来看下Swift的函数和闭包 一.Swift函数 函数是一个完成独立任务的代码块,Swift中的函数不仅可以像C语言中的函数 ...
- IOS学习之路二十四(UIImageView 加载gif图片)
UIImageView 怎样加载一个gif图片我还不知道(会的大神请指教),不过可以通过加载不同的图片实现gif效果 代码如下: UIImageView* animatedImageView = [[ ...
- IOS学习之路二十四(custom 漂亮的UIColor)
下面简单列举一下漂亮的和颜色,大家也可以自己依次试一试选出自己喜欢的. 转载请注明 本文转自:http://blog.csdn.net/wildcatlele/article/details/1235 ...
- IOS学习之路二十三(EGOImageLoading异步加载图片开源框架使用)
EGOImageLoading 是一个用的比较多的异步加载图片的第三方类库,简化开发过程,我们直接传入图片的url,这个类库就会自动帮我们异步加载和缓存工作:当从网上获取图片时,如果网速慢图片短时间内 ...
随机推荐
- Linux操作系统学习_用户态与内核态之切换过程
因为操作系统的很多操作会消耗系统的物理资源,例如创建一个新进程时,要做很多底层的细致工作,如分配物理内存,从父进程拷贝相关信息,拷贝设置页目录.页表等,这些操作显然不能随便让任何程序都可以做,于是就产 ...
- python cookbook学习笔记 第一章 文本(1)
1.1每次处理一个字符(即每次处理一个字符的方式处理字符串) print list('theString') #方法一,转列表 结果:['t', 'h', 'e', 'S', 't', 'r', 'i ...
- poj 2378 树型dp
和poj1655那道求树的重心基本上一样的,代码也没多大改动. 详情请见 #include<cstdio> #include<algorithm> #include<cs ...
- 排序算法用C++的基本算法实现十个数排序
本文个人在青岛喝咖啡的时候突然想到的...近期就有想写几篇关于排序算法的文章,所以回家到之后就奋笔疾书的写出来发布了 冒泡排序法 道理: 它重复地访问过要排序的数列,一次比较两个元素,如果他们的顺序错 ...
- 【NOIP2013】Day2不完全题解+代码
T1 直接递归区间,从1-n开始,找到这个区间中的最小值然后将区间里的所有值都减去这个最小值 以被减去最小值之后的零点为分段分别递归处理即可. #include <algorithm> # ...
- linux 学习-软件的安装
Linux软件的安装rpm -ivh安装软件全名 -i install 安装 -v verbose 显示详细信息 -h hash 显示进度 --nodeps 不检测依赖性(不推荐使用) rpm -U ...
- Long类型比较大小,long型和Long型区别
今天写代码发现发现本地程序是正常的,但是发送到测试环境就不正常了,本着对数据的怀疑态度链接了测试数据库,调试程序发现,确实是数据问题,然后数据出现在什么地方呢?才发现是在判断用户所属的teamGrou ...
- json的细节
之前一直纳闷为什么在js里直接写的json数据可以不用eval()直接解析,而后台传入ajax的json数据需要eval()一下才能解析 原来是我没搞清楚json格式字符串跟json对象 var te ...
- Git提交到多个远程仓库
在已经习惯使用git同步写代码,github无疑是最的托管平台,但是国内由于"你懂的"原因,速度很慢,有时无法访问,于是想把自己的代码同步到多个不同的远程仓库备份. 我的主要仓库: ...
- Chapter 21_4 捕获
捕获功能在很多地方都在使用,就是从目标字符串中抽出匹配于该模式的内容,在指定捕获时,应将模式中需要捕获的部分写到一对圆括号内. 对于具有捕获的模式,函数match会将所有捕获到的值作为单独的结果返回. ...