您的位置:首页 > 其它

RUNTIME简介以及常见的使用场景(比较全面)

2016-08-23 16:56 225 查看




Runtime简称运行时,是一套比较底层的纯C语言的API, 作为OC的核心,运行时是一种面向对象的编程语言的运行环境,其中最主要的是消息机制,Objective-C 就是基于运行时的。
所谓运行时,是指尽可能地把决定从编译期推迟到运行期,就是尽可能地做到动态.只是在运行的时候才会去确定对象的类型和方法的.因此利用Runtime机制可以在程序运行时动态地修改类和对象中的所有属性和方法。
对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
 
Objective-C 从三种不同的层级上与 Runtime 系统进行交互,分别是
①通过 Objective-C 源代码;
②通过 Foundation 框架的NSObject类定义的方法;
③通过对 Runtime 函数的直接调用(需要导入#import <objc/runtime.h>);
大部分情况下只管写OC代码就行,因为OC底层默认实现Runtime,每一个OC的方法,底层必然有一个与之对应的Runtime方法。。
 
以下是Runtime的一些使用场景:
1.发送消息
方法调用的本质,就是让对象发送消息。objc_msgSend,只有对象才能发送消息
举个简单的例子:如下

(1).调用对象方法:
// 创建Dog对象
Dog *dog = [[Dog alloc] init];

// 调用对象方法
[dog run];
// 调用对象本质:让对象发送消息
objc_msgSend(dog, @selector(run));

//(2).调用类方法方式:
//有两种
// 第一种通过类名调用
[Dog run];
// 第二种通过类对象调用
[[Dog class] run];
// 用类名调用类方法,底层会自动把类名转换成类对象调用
// 调用类方法本质:让类对象发送消息
objc_msgSend([Dog class], @selector(run));


消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现
2.动态添加方法
开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。简单使用

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

Person *p = [[Person alloc] init];

// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
// 动态添加方法就不会报错
[p performSelector:@selector(eat)];

}

@end


在person类.m中:

@implementation Person
// void(*)()
// 默认方法都有两个隐式参数,
void eat(id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{

if (sel == @selector(eat)) {
// 动态添加eat方法

// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");

}

return [super resolveInstanceMethod:sel];
}
@end


注意:
+ (BOOL) resolveInstanceMethod:(SEL)aSEL 这个函数与forwardingTargetForSelector类似,都会在对象不能接受某个selector时触发,执行起来略有差别。前者的目的主要在于给客户一个机会来向该对象添加所需的selector,后者的目的在于允许用户将selector转发给另一个对象。另外触发时机也不完全一 样,该函数是个类函数,在程序刚启动,界面尚未显示出时,就会被调用。
在类不能处理某个selector的情况下,如果类重载了该函数,并使用class_addMethod添加了相应的selector,并返回YES,那么后面forwardingTargetForSelector 就不会被调用,如果在该函数中没有添加相应的selector,那么不管返回什么,后面都会继续调用 forwardingTargetForSelector,如果在forwardingTargetForSelector并未返回能接受该 selector的对象,那么resolveInstanceMethod会再次被触发,这一次,如果仍然不添加selector,程序就会报异常
3.运行时关联对象提高效率,给分类添加属性。

1, 给类动态添加属性,实质就是让一个属性与某个对象产生关联的关系.值得注意的一点: 使用该功能的前提是给系统的类添加属性,如果是自定义的类添加属性,就没有必要用到runtime了.

2, 背景: 给系统的uibutton类动态添加一个name属性.

1, 创建uibutton对象

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)


如果我们需要动态的加载一个属性,我们就要使用这个set方法

首先我们分析一下,参数含义

id object : 源对象 -指定我们需要绑定的对象,e.g ,给一个字符串添加一个内容
const void * key : 设置一个静态常亮,也就是Key 值,通过这个我们可以找到我们关联对象的那个数据值
id value 这个是我们打点调用属性的时候会自动调用set方法进行传值
objc_AssociationPolicy policy : 这个是关联策略,这几个管理策略,我们看下都有什么,
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {

// 第一个关联策略是基于基本类型的,也就是我们常用的assign 属性
OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
//关联策略是基于对象类型的,如我们正常的是retain nonatomic (非原子操作)类型 ,retain  : 保留一个引用指针,内存地址不修改,指向同一块内存地址
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
*   The association is not made atomically. */
//这个相当于属性中对一些对象或者字符串进行的copy 这个是拷贝一个新对象,内存地址不在一起,还是使用的非原子类型,非原子类型我们也称之为线程不安全的操作,但是对于OC里面的数据操作,我们尽量避开原子操作,原子操作是线程安全的,会影响我们对数据的写入操作
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied.
*   The association is not made atomically. */
// 简单的指针引用, retain 操作
OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
*   The association is made atomically. */
//把简单的对象拷贝到一个新的内存地址
OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
*   The association is made atomically. */
};

以上就是我们动态添加属性索要用到的一些方法,简单理解就是

: set(源对象,常亮key,数值,属性关联策略)

下面这个想必都可以看明白,类似我们的getter 方法

OBJC_EXPORT id objc_getAssociatedObject(id object, const void*key)__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);


同样 传入的参数有两个:

id object :当前的属性是关联在那个对象上的,和set 保持一致
const void * key : 静态关键key 要获取的值就是根据关联对象可以关键key 获取对应的数据

使用的时候与懒加载的特点相似,从`关联对象`中获取对象属性,如果有,直接返回。
UIControl+RYButton.h 文件

#import <UIKit/UIKit.h>

@interface UIControl (RYButton)
// 声明一个时间间隔
@property (assign,nonatomic)NSTimeInterval  ry_time;

@end




UIControl+RYButton.m 文件

#import "UIControl+RYButton.h"
#import <objc/runtime.h>
static const char * RY_CLICKKEY = "ry_clickkey";
@implementation UIControl (RYButton)

- (void)setRy_time:(NSTimeInterval)ry_time{
objc_setAssociatedObject(self, RY_CLICKKEY, @(ry_time), OBJC_ASSOCIATION_ASSIGN);

}
- (NSTimeInterval)ry_time{
return [objc_getAssociatedObject(self, RY_CLICKKEY) doubleValue];

}
@end


使用方法

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
ThirdPartService * service = [ThirdPartService new];
NSLog(@"%@ ---%@",NSStringFromSelector(_cmd),service);

UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
button.ry_time = 1.0f;

}

这个就是动态添加属性的简单使用

4.使用运行时字典转模型
大体思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。(所有字典转模型框架的核心算法)

创建NSObject的分类Runtime:

在.h中的类方法如下:
#import <Foundation/Foundation.h>

@interface NSObject (Runtime)

///  给定一个字典,创建 self 类对应的对象
///
///  @param dict 字典
///
///  @return 对象
+ (instancetype)hd_objWithDict:(NSDictionary *)dict;

///  获取类的属性列表数组
///
///  @return 类的属性列表数组
+ (NSArray *)hd_objProperties;

@end

在.m中的类方法如下:

// 所有字典转模型框架的核心算法
+ (instancetype)hd_objWithDict:(NSDictionary *)dict {
// 实例化对象
id object = [[self alloc] init];

// 使用字典,设置对象信息
// 1> 获得 self 的属性列表
NSArray *proList = [self cz_objProperties];

// 2> 遍历字典
[dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

NSLog(@"key %@ --- value %@", key, obj);
// 3> 判断 key 是否在 proList 中
if ([proList containsObject:key]) {
//  说明属性存在,可以使用 `KVC` 设置数值
[object setValue:obj forKey:key];
}
}];
return object;
}

const char * kPropertiesListKey = "CZPropertiesListKey";

+ (NSArray *)hd_objProperties {

// --- 1. 从`关联对象`中获取对象属性,如果有,直接返回!
/**
获取关联对象 - 动态添加的属性
参数:
1. 对象 self
2. 动态属性的 key
返回值
动态添加的`属性值`
*/
NSArray *ptyList = objc_getAssociatedObject(self, kPropertiesListKey);
if (ptyList != nil) {
return ptyList;
}

// 调用运行时方法,取得类的属性列表
// Ivar 成员变量
// Method 方法
// Property 属性
// Protocol 协议
/**
参数
1. 要获取的类
2. 类属性的个数指针

返回值
所有属性的`数组`,C 语言中,数组的名字,就是指向第一个元素的地址

retain/create/copy 需要 release,最好 option + click
*/
unsigned int count = 0;
objc_property_t *proList = class_copyPropertyList([self class], &count);

NSLog(@"属性的数量 %d", count);
// 创建数组
NSMutableArray *arrayM = [NSMutableArray array];

// 遍历所有的属性
for (unsigned int i = 0; i < count; i++) {

// 1. 从数组中取得属性
/**
C 语言的结构体指针,通常不需要 `*`
*/
objc_property_t pty = proList[i];

// 2. 从 pty 中获得属性的名称
const char *cName = property_getName(pty);

NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];

//        NSLog(@"%@", name);
// 3. 属性名称添加到数组
[arrayM addObject:name];
}

// 释放数组
free(proList);

// --- 2. 到此为止,对象的属性数组已经获取完毕,利用关联对象,动态添加属性
/**
参数

1. 对象 self [OC 中 class 也是一个特殊的对象]
2. 动态添加属性的 key,获取值的时候使用
3. 动态添加的属性值
4. 对象的引用关系
*/
objc_setAssociatedObject(self, kPropertiesListKey, arrayM.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

return arrayM.copy;
}

注意:必须保证,模型中的属性和字典中的key一一对应。 如果不一致,就会调用[ setValue:forUndefinedKey:],报key找不到的错。
分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,
就能继续使用KVC,字典转模型了。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{

}
通过运行时字典转模型的好处在于写在NSObject的分类中,和类的关联性不强对类解耦,以后再做字典转模型的时候只需要把这个分类往任何一个程序中一拖,程序中的对象就都具备了这个字典转模型的方法。


5.交叉方法(黑魔法)
开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。方式一:继承系统的类,重写方法.方式二:使用runtime,交换方法.
Runtime在AFN中的使用细节:在AFN的NSURLSessionMangerM方法里面第363行写了一个静态的内联函数,做了一个交叉方法,交叉的是af_resume和resume方法,这样的话,可以在发送网络之前发起一个通知,能接受到任何一个网络请求的事件的变化。

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
// 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
// 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
UIImage *image = [UIImage imageNamed:@"123"];
}
@end

@implementation UIImage (Image)
// 加载分类到内存的时候调用
+ (void)load
{
// 交换方法

// 获取imageWithName方法地址
Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));

// 获取imageWithName方法地址
Method imageName = class_getClassMethod(self, @selector(imageNamed:));

// 交换方法地址,相当于交换实现方式
method_exchangeImplementations(imageWithName, imageName);

}

// 既能加载图片又能打印
+ (instancetype)imageWithName:(NSString *)name
{

// 这里调用imageWithName,相当于调用imageName
UIImage *image = [self imageWithName:name];

if (image == nil) {
NSLog(@"加载空的图片");
}

return image;
}

@end


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: