前面已经介绍了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(二)的更多相关文章

  1. iOS学习之Runtime(一)

    一.Runtime简介 因为Objective-C是一门动态语言,所以它总是想办法把一些决定性工作从编译链接推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system ...

  2. ios学习笔记(二)第一个应用程序--Hello World

    原文地址:http://blog.csdn.net/shangyuan21/article/details/18416537 上一篇文章,Windows7上使用VMWare搭建iPhone开发环境介绍 ...

  3. IOS学习之路二十(程序json转换数据的中文字符问题解决)

    ios请求web中的json数据的时候经常出现乱码问题: 例如请求结果可能如下:"\U00e5\U00a5\U00bd\U00e8\U00ae\U00a4" 在网上查到的解决方法是 ...

  4. ios学习笔记(二)之Objective-C类、继承、类别和协议

    二:Objective-C类与继承和协议 在前面已经提过了对象的初始化,这里首先讲的是变量. 2.1 变量 局部变量(内部变量): 局部变量是在方法内作定义说明的,其作用域仅限于方法内,离开方法后使用 ...

  5. iOS学习笔记(二)——Hello iOS

    前面写了iOS开发环境搭建,只简单提了一下安装Xcode,这里再补充一下,点击下载Xcode的dmp文件,稍等片刻会有图一(拖拽Xcode至Applications)的提示,拖拽至Applicatio ...

  6. iOS学习笔记42-Swift(二)函数和闭包

    上一节我们讲了Swift的基础部分,例如数据类型.运算符和控制流等,现在我们来看下Swift的函数和闭包 一.Swift函数 函数是一个完成独立任务的代码块,Swift中的函数不仅可以像C语言中的函数 ...

  7. IOS学习之路二十四(UIImageView 加载gif图片)

    UIImageView 怎样加载一个gif图片我还不知道(会的大神请指教),不过可以通过加载不同的图片实现gif效果 代码如下: UIImageView* animatedImageView = [[ ...

  8. IOS学习之路二十四(custom 漂亮的UIColor)

    下面简单列举一下漂亮的和颜色,大家也可以自己依次试一试选出自己喜欢的. 转载请注明 本文转自:http://blog.csdn.net/wildcatlele/article/details/1235 ...

  9. IOS学习之路二十三(EGOImageLoading异步加载图片开源框架使用)

    EGOImageLoading 是一个用的比较多的异步加载图片的第三方类库,简化开发过程,我们直接传入图片的url,这个类库就会自动帮我们异步加载和缓存工作:当从网上获取图片时,如果网速慢图片短时间内 ...

随机推荐

  1. PHP生成缩略图函数

    function img_create_small($big_img, $width, $height, $small_img) { // 大图文件地址,缩略宽,缩略高,小图地址$imgage = g ...

  2. day7 面向对象编程

    编程范式 编程是程序员用特定的语法+数据结构+算法组成的代码来告诉计算机如何执行任务的过程,一个程序是程序员为了得到一个任务结果而编写的一组指令的集合,正所谓条条大路通罗马,实现一个任务的方式有很多种 ...

  3. Linux:用at和crontab调度作业

    一.有2种作业调度方式 1.突发性的,就是只运行作业一次而不是定期运行,使用at命令. 例如在进程A运行一段时间后关闭该进程. 2.定期运行,就是每隔一定的周期运行一次,使用crontab命令. 如每 ...

  4. vs2012中程序集生成无法自动在网站Bin目录下生成Dll文件?(已解决!)

    最近,突然发现生成程序集后,网站bin目录下dll没有更新,也没有自动生成dll文件,通过近半个小时的摸索和实验,找到了解决方法: 1.右键网站,不是项目,选择[属性页],在左侧[引用]中如果没有,就 ...

  5. JVM参数设置、分析

    不管是YGC还是Full GC,GC过程中都会对导致程序运行中中断,正确的选择不同的GC策略,调整JVM.GC的参数,可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java ...

  6. .Net程序员学用Oracle系列(6):表、字段、注释、约束、索引

    <.Net程序员学用Oracle系列:导航目录> 本文大纲 1.表 1.1.创建表 1.2.修改表 & 删除表 2.字段 2.1.添加字段 2.2.修改字段 & 删除字段 ...

  7. C#通过外部别名,解决DLL冲突问题

    今天遇到一个有两个DLL文件,命名空间,部分类名与部分方法名一样,但是方法的功能实现不一样.调用方法时,无法调用指定DLL的指定方法.在网上找了好多,简单总结一下. 1.首先添加引用,不细说. 2.右 ...

  8. Angular2中的Service并不是单例模式

    2015年做了一个使用angularjs 1框架的项目,2016年伊始公司的项目转为使用Angular2框架. 在开发过程中发现了一个坑,这个坑就是在Angular JS 1.x中的Service是单 ...

  9. 金蝶KIS专业版替换SXS.dll 遭后门清空数据被修改为【恢复数据联系QQ 735330197,2251434429】解决方法 修复工具。

    金蝶KIS专业版 替换SXS.dll 遭后门清空数据(凭证被改为:恢复数据联系QQ 735330197,2251434429)恢复解决方法. [客户名称]:山东青岛福隆发纺织品有限公司 [软件名称]: ...

  10. Photoshop像素级画笔工具

    1.直线工具 2.选择颜色,选择像素 dd