使用运行时方法需要引入runtime.h文件。
一、基础知识
runtime简称运行时,OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
对于C语言,函数的调用是在编译的时候会决定调用哪个函数。
对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
事实证明:
-- 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
-- 在编译阶段,C语言调用未实现的函数就会报错。
Method :成员方法
Ivar : 成员变量
二、常用方法
class_copyPropertyList : 获取属性列表
class_copyMethodList : 获取成员方法列表
class_copyIvarList:获取成员变量列表
ivar_getName:获取变量名
property_getName:获取属性名
使用示例:
2.1.获取成员变量list
unsigned int ivarCount = 0; //成员变量数
Ivar *ivarList = class_copyIvarList([self class], &ivarCount);//ivar数组
for (int i = 0; i < ivarCount; i++) {//遍历
Ivar ivar = ivarList[i]; //获取ivar
const char *name = ivar_getName(ivar);//获取变量名
NSString *key = [NSString stringWithUTF8String:name];
NSLog(@"%@", key);
}
free(ivarList);
2.2.获取属性列表
unsigned int count = 0;
objc_property_t *propertList = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = propertList[i];
const char *name = property_getName(property);
const char *attrs = property_getAttributes(property);
// property_copyAttributeValue(,) 第一个参数为objc_property_t,第二个参数"V"获取变量名,"T"获取类型
const char *value = property_copyAttributeValue(property, "V");
NSLog(@"name = %s, attrs = %s, value = %s", name, attrs, value);
/*
各种符号对应类型,部分类型在新版SDK中有所变化,如long 和long long
c char C unsigned char
i int I unsigned int
l long L unsigned long
s short S unsigned short
d double D unsigned double
f float F unsigned float
q long long Q unsigned long long
B BOOL
@ 对象类型 //指针 对象类型 如NSString 是@“NSString”
propertyType,你可以打印出来,看看它是什么。
要判断某个属性的类型,只需要[propertyType hasPrefix:@"T@\"NSString\""]
这代表它是NSString 类型。
*/
}
free(propertList);
2.3.获取方法列表
unsigned int count = 0;
Method *methodList = class_copyMethodList([self class], &count);
for (int i = 0 ; i < count; i++) {
Method method = methodList[i];
SEL selector = method_getName(method);//方法入口
const char *sel_name = sel_getName(selector);
NSLog(@"方法名 %s", sel_name);
}
free(methodList);
2.4.Runtime-动态创建类添加属性和方法
- (void)createClass
{
Class MyClass = objc_allocateClassPair([NSObject class], "myclass", 0);
//添加一个NSString的变量,第四个参数是对其方式,第五个参数是参数类型
if (class_addIvar(MyClass, "itest", sizeof(NSString *), 0, "@")) {
NSLog(@"add ivar success");
}
//myclasstest是已经实现的函数,"v@:"这种写法见参数类型连接
class_addMethod(MyClass, @selector(myclasstest:), (IMP)myclasstest, "v@:");
//注册这个类到runtime系统中就可以使用他了
objc_registerClassPair(MyClass);
//生成了一个实例化对象
id myobj = [[MyClass alloc] init];
NSString *str = @"asdb";
//给刚刚添加的变量赋值
// object_setInstanceVariable(myobj, "itest", (void *)&str);在ARC下不允许使用
[myobj setValue:str forKey:@"itest"];
//调用myclasstest方法,也就是给myobj这个接受者发送myclasstest这个消息
[myobj myclasstest:10];
}
//这个方法实际上没有被调用,但是必须实现否则不会调用下面的方法
- (void)myclasstest:(int)a
{
}
//调用的是这个方法
static void myclasstest(id self, SEL _cmd, int a) //self和_cmd是必须的,在之后可以随意添加其他参数
{
Ivar v = class_getInstanceVariable([self class], "itest");
//返回名为itest的ivar的变量的值
id o = object_getIvar(self, v);
//成功打印出结果
NSLog(@"%@", o);
NSLog(@"int a is %d", a);
}
详解Objective-C的meta-class:http://www.jianshu.com/p/a5ab8d998b9e
三、使用方向:归档、字典<---->模型、框架封装等
3.1. 实现归档
encode:编码 decode:解码
#define WKCodingImplementing
- (void)encodeWithCoder:(NSCoder *)aCoder
{
unsigned int ivarCount = 0;
Ivar *ivarList = class_copyIvarList([self class], &ivarCount);
for (int i = 0; i < ivarCount; i++) {
Ivar ivar = ivarList[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSLog(@"%s-----%s", name, type);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivarList);
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
unsigned int ivarCount = 0;
Ivar *ivarList = class_copyIvarList([self class], &ivarCount);
for (int i = 0; i < ivarCount; i++) {
Ivar ivar = ivarList[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key]; \
NSLog(@"%@ %@", key, value);
[self setValue:value forKey:key];
}
}
return self;
}
3.2.如何快速生成Plist文件属性名
实现原理:通过遍历字典,判断类型,拼接字符串
// 拼接属性字符串代码
NSMutableString *strM = [NSMutableString string];
// 1.遍历字典,把字典中的所有key取出来,生成对应的属性代码
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSString *type;
if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
type = @"NSString";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
type = @"NSArray";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
type = @"int";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
type = @"NSDictionary";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
type = @"BooL";
}
// 属性字符串
NSString *str;
if ([type containsString:@"NS"]) {
str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
}else{
str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
}
// 每生成属性字符串,就自动换行。
[strM appendFormat:@"\n%@\n",str];
}];
// 把拼接好的字符串打印出来,就好了。
NSLog(@"%@",strM);
打印结果
3.3.KVC实现字典转模型
KVC弊端
模型中属性必须和字典的key一致,否则就报错
如果不一致,系统会调用setValue: forUndefinedKey:
解决办法,只需要重写setValue: forUndefinedKey:即可
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}
3.4. RunTime实现字典转模型
实现思路:遍历模型中所有属性,根据模型的属性名去字典中查找key,取出对应的的值,给模型的属性赋值
3.4.1.一级转换
class_copyIvarList(self, &count)该方法第一个参数是要获取哪个类中的成员属性,第二个参数是这个类中有多少成员属性,需要传入地址,返回值Ivar是个数组,会将所有成员属性放入这个数组中
@implementation NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 思路:遍历模型中所有属性-》使用运行时
// 0.创建对应的对象
id objc = [[self alloc] init];
// 1.利用runtime给对象中的成员属性赋值
// class_copyIvarList:获取类中的所有成员属性
// Ivar:成员属性的意思
// 第一个参数:表示获取哪个类中的成员属性
// 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值
// 返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
/* 类似下面这种写法
Ivar ivar;
Ivar ivar1;
Ivar ivar2;
// 定义一个ivar的数组a
Ivar a[] = {ivar,ivar1,ivar2};
// 用一个Ivar *指针指向数组第一个元素
Ivar *ivarList = a;
// 根据指针访问数组第一个元素
ivarList[0];
*/
unsigned int count;
// 获取类中的所有成员属性
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
// 获取成员属性名
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员属性名->字典中的key
// (去掉 _ ,从第一个角标开始截取)
NSString *key = [name substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
3.4.2.二级转换
判断字典中是否存在字典,如果存在,转为模型
字典属性生成的是@"@"xxxx""类型,需要裁减为@"xxxx"
if ([value isKindOfClass:[NSDictionary class]]) {
// 获取成员属性类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 生成的是这种@"@\"User\"" 类型 -》 @"User" 在OC字符串中 \" -> ",\是转义的意思,不占用字符
type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
NSLog(@"type-----------%@",type);
type = [type stringByReplacingOccurrencesOfString:@"@" withString:@""];
NSLog(@"type-----------%@",type);
// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(type);
if (modelClass) {
value = [modelClass modelWithDict:value];
}
}
3.4.3.三级转换
通过给分类添加一个协议,来实现将数组中的字典转为模型
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDict:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
if (value) { // 有值,才需要给模型的属性赋值
// 利用KVC给模型中的属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
@end
3.5. 使用runtime实现方法的交换
3.5.1 前言
在开发过程中,我们常常会遇到一种问题: 即当我们使用系统自带的方法时,发现系统方法的功能不能满足我们的需求,这时候需要我们给系统的方法添加额外的功能.
下面以一个例子来实现给系统自带的方法添加额外的功能.
项目背景 : 公司有个开发了很久的项目,之前使用添加图片的方法用的是:imageName:方法, 现在的项目需求是给系统自带的imageName:添加一个功能:如果下载的图片不存在,那么就提示我们当前下载的图片为空.
思路 : 给系统自带的方法添加功能的几种方法
- 自定义类, 重写系统自带的imageName:方法,这种方法虽然可以实现,但是它的弊端就是必须要使用自己的类,依赖性强.
- 给UIImage添加一个分类, 改变系统类的实现,给系统的类添加方法的时候调用(强烈不推荐)
- 使用runtime的交互方法,给系统的方法添加功能. 具体实现 : 添加一个分类 --> 在分类中提供一个需要添加的功能的方法 --> 将这个方法的实现和系统自带的方法的实现交互.
3.5.2.步骤
3.5.2.1. 新建一个继承自UIImage的分类,定义一个方法实现给系统的自带的方法添加功能.
#import <UIKit/UIKit.h>
@interface UIImage (WGImage)
// 声明方法
// 如果跟系统方法差不多功能,可以采取添加前缀,与系统方法区分
+ (UIImage *)wg_imageWithName:(NSString *)imageName;
@end
3.5.2.2. 实现方法的交互
定义完毕新方法后,需要弄清楚什么时候实现与系统的方法交互?
答 : 既然是给系统的方法添加额外的功能,换句话说,我们以后在开发中都是使用自己定义的方法,取代系统的方法,所以,当程序一启动,就要求能使用自己定义的功能方法.说道这里:我们必须要弄明白一下两个方法 :
+(void)initialize(当类第一次被调用的时候就会调用该方法,整个程序运行中只会调用一次)
- (void)load(当程序启动的时候就会调用该方法,换句话说,只要程序一启动就会调用load方法,整个程序运行中只会调用一次)
+ (void)load {
/*
self:UIImage
谁的事情,谁开头 1.发送消息(对象:objc) 2.注册方法(方法编号:sel) 3.交互方法(方法:method) 4.获取方法(类:class)
Method:方法名
获取方法,方法保存到类
Class:获取哪个类方法
SEL:获取哪个方法
imageName
*/
// 获取imageName:方法的地址
Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
// 获取wg_imageWithName:方法的地址
Method wg_imageWithNameMethod = class_getClassMethod(self, @selector(wg_imageWithName:));
// 交换方法地址,相当于交换实现方式
method_exchangeImplementations(imageNameMethod, wg_imageWithNameMethod);
}
3.5.2.3. 加载图片, 判断当前图片是否为空
// 加载图片, 判断是否为空
+ (UIImage *)wg_imageWithName:(NSString *)imageName
{
// 这里调用imageWithName,相当于调用imageName
UIImage *image = [UIImage wg_imageWithName:imageName];
if (!image) {
NSLog(@"Alex : 图片不存在");
}
return image;
}
3.5.2.4. 使用交互后的方法
#import "ViewController.h"
#import "WGStudent.h"
#import "UIImage+WGImage.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// wg_imageWithName ---> imageNamed(底层的操作 : 1, 下载图片. 2, 判断图片是否存在 )
[UIImage imageNamed:@"WilliamAlex.png"];
}
@end
3.5.2.5. 打印结果
2016-03-08 17:29:50.372 fdsfsdf[1545:96854] Alex : 图片不存在
知识拓展
不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
runtime的那点事儿(一)消息机制
http://blog.csdn.net/jq2530469200/article/details/51880836
runtime的那点事儿(二)消息机制
http://blog.csdn.net/jq2530469200/article/details/51886532
runtime的那点事儿(三)消息机制
http://blog.csdn.net/jq2530469200/article/details/51886578
runtime简单使用之方法的交换:
http://www.jianshu.com/p/d28abb706add
使用RunTime实现字典转模型:
http://www.jianshu.com/p/cecfe78e9cd8