赞
踩
Runtime是一套底层纯
C语言API
,我们编写的OC代码最终都会被编译器转化为运行时代码
,通过消息机制
决定函数调用方式,这也是OC作为动态语言
使用的基础。
在Object-C的语言中,对象方法调用都是类似[receiver selector]
的形式,其本质:就是让对象在运行时发送消息的过程。
而方法调用[receiver selector]
分为两个过程:
编译阶段
[receiver selector] 方法被编译器转化,分为两种情况:
1.不带参数的方法被编译为:objc_msgSend(receiver,selector)
2.带参数的方法被编译为:objc_msgSend(recevier,selector,org1,org2,…)
运行时阶段
消息接收者recever
寻找对应的selector
,也分为两种情况:
1.接收者能找到对应的selector,直接执行接收receiver对象的selector方法。
2.接收者找不到对应的selector,消息被转发
或者临时向接收者添加这个selector对应的实现内容,否则崩溃
总而言之:
OC调用方法
[receiver selector]
,编译阶段
确定了要向哪个接收者
发送message消息,但是接收者
如何响应决定于运行时的判断
所有
Objective-C 方法调用在编译时
都会转化为对C
函数objc_msgSend
的调用。objc_msgSend(receiver,selector); 是
[receiver selector]; `对应的 C 函数
在 objc/runtime.h
中,Object(对象)
被定义为指向 objc_object
结构体的指针,objc_object
结构体 的数据结构如下:
//runtime对objc_object结构体的定义
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
//id是一个指向objc_object结构体的指针,即在Runtime中:
typedef struct objc_object *id;
//OC中的对象虽然没有明显的使用指针,但是在OC代码被编译转化为C之后,每个OC对象其实都是拥有一个isa的指针的
在 objc/runtime.h
中,Class(类)
被定义为指向 objc_class
结构体 的指针,objc_class
结构体 的数据结构如下:
//runtime对objc_class结构体的定义 struct objc_class { Class _Nonnull isa; // objc_class 结构体的实例指针 #if !__OBJC2__ Class _Nullable super_class; // 指向父类的指针 const char * _Nonnull name; // 类的名字 long version; // 类的版本信息,默认为 0 long info; // 类的信息,供运行期使用的一些位标识 long instance_size; // 该类的实例变量大小; struct objc_ivar_list * _Nullable ivars; // 该类的实例变量列表 struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表 struct objc_cache * _Nonnull cache; // 方法缓存 struct objc_protocol_list * _Nullable protocols; // 遵守的协议列表 #endif }; //class是一个指向objc_class结构体的指针,即在Runtime中: typedef struct objc_class *Class;
在 objc/runtime.h
中,SEL (方法选择器)
被定义为指向 objc_selector
结构体 的指针:
typedef struct objc_selector *SEL;
//Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
注意:
1.不同类中相同名字的方法对应的方法选择器是相同的。
2.即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。
通常获取SEL有三种方法:
1.OC中,使用
@selector("方法名字符串")
2.OC中,使用NSSelectorFromString("方法名字符串")
3.Runtime
方法,使用sel_registerName("方法名字符串")
在 objc/runtime.h
中,Ivar
被定义为指向 objc_ivar
结构体 的指针,objc_ivar
结构体 的数据结构如下:
struct objc_ivar {
char * Nullable ivar_name OBJC2UNAVAILABLE;
char * Nullable ivar_type OBJC2UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef LP64
int space OBJC2_UNAVAILABLE;
#endif
}
//Ivar代表类中实例变量的类型,是一个指向ojbcet_ivar的结构体的指针
typedef struct objc_ivar *Ivar;
在objc_class
中看到的ivars
成员列表,其中的元素就是Ivar
,可以通过实例查找其在类中的名字,这个过程被称为反射,下面的class_copyIvarList
获取的不仅有实例变量还有属性:
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
在 objc/runtime.h
中,Method(方法)
被定义为指向 objc_method
结构体 的指针,在objct_class
定义中看到methodLists
,其中的元素就是Method,objc_method
结构体 的数据结构如下:
struct objc_method {
SEL _Nonnull method_name; // 方法名
char * _Nullable method_types; // 方法类型
IMP _Nonnull method_imp; // 方法实现
};
//Method表示某个方法的类型
typedef struct objc_method *Method;
OC代码会在
编译阶段
被编译器转化。OC中的类、方法和协议等在Runtime
中都由一些数据结构
来定义。
所以在日常的项目开发过程中,使用OC语言进行编码时,这已经是在和Runtime进行交互了,只是这个过程对于开发者而言是无感的
Runtime的最大特征就是实现了OC语言的
动态特性
。
作为大部分Objective-C
类继承体系的根类的NSObject
,其本身就具有了一些非常具有运行时动态特性的方法, 比如:
-respondsToSelector:
方法可以检查在代码运行阶段当前对象是否能响应指定的消息-description:
返回当前类的描述信息-isKindOfClass:
和-isMemberOfClass:
检查对象是否存在于指定的类的继承体系中-conformsToProtocol:
检查对象是否实现了指定协议类的方法;-methodForSelector:
返回指定方法实现的地址。
Runtime
系统是一个由一系列函数
和数据结构
组成,具有公共接口
的动态共享库
。头文件存放于/usr/include/objc
目录下。
在项目工程代码里引用Runtime的头文件,同样能够实现类似OC代码的效果:
//相当于:Class class = [UIView class];
Class viewClass = objc_getClass("UIView");
//相当于:UIView *view = [UIView alloc];
UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc"));
//相当于:UIView *view = [view init];
((id (*)(id, SEL))(void *)objc_msgSend)((id)view, sel_registerName("init"));
Runtime足够强大,能够在运行时
动态添加一个未实现的方法
,这个功能主要有两个应用场景:
1.动态添加未实现方法,解决代码中因为方法未找到而报错的问题;
2.利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法
方法动态解析主要用到的方法如下:
//OC方法: //类方法未找到时调起,可于此添加类方法实现 + (BOOL)resolveClassMethod:(SEL)sel //实例方法未找到时调起,可于此添加实例方法实现 + (BOOL)resolveInstanceMethod:(SEL)sel //Runtime方法: /** 运行时方法:向指定类中添加特定方法实现的操作 @param cls 被添加方法的类 @param name selector方法名 @param imp 指向实现方法的函数指针 @param types imp函数实现的返回值与参数类型 @return 添加方法是否成功 */ BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
执行OC方法其实就是一个发送消息的过程,若方法未实现,可以利用方法动态解析
与消息转发
来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:
在这个过程中,可能还会使用到的方法有:
例子:
#import "ViewController.h" #import <objc/runtime.h> @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 执行 fun 函数 [self performSelector:@selector(fun)]; } // 重写 resolveInstanceMethod: 添加对象方法实现 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMP class_addMethod([self class], sel, (IMP)funMethod, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } void funMethod(id obj, SEL _cmd) { NSLog(@"funMethod"); //新的 fun 函数 } @end //日志输出: 2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] funMethod
从执行任务的输出日志中,可以看到:
虽然没有实现
fun
方法,但是通过重写resolveInstanceMethod:
,利用class_addMethod
方法添加对象方法实现funMethod
方法,并执行。从打印结果来看,成功调起了funMethod
方法。
接收者
重定向:如果上一步中 +resolveInstanceMethod:
或者 +resolveClassMethod:
没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。
如果当前对象实现了-forwardingTargetForSelector:
,Runtime 就会调用这个方法,允许将消息的接受者转发给其他对象,其主要方法如下:
//重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector
//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
例子:
#import "ViewController.h" #import <objc/runtime.h> @interface Person : NSObject - (void)fun; @end @implementation Person - (void)fun { NSLog(@"fun"); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 执行 fun 方法 [self performSelector:@selector(fun)]; } + (BOOL)resolveInstanceMethod:(SEL)sel { return YES; // 为了进行下一步 消息接受者重定向 } // 消息接受者重定向 - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(fun)) { return [[Person alloc] init]; // 返回 Person 对象,让 Person 对象接收这个消息 } return [super forwardingTargetForSelector:aSelector]; } //日志输出: 2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] fun
从执行任务的输出日志中,可以看到:
虽然当前 ViewController 没有实现
fun
方法,+resolveInstanceMethod:
也没有添加其他函数实现。
但是我们通过forwardingTargetForSelector
把当前 ViewController 的方法转发给了Person 对象
去执行了
通过forwardingTargetForSelector
可以修改消息的接收者
,该方法返回参数是一个对象,如果这个对象是不是 nil
,也不是 self
,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程
消息
重定向:如果经过消息动态解析
、消息接受者重定向
,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector:
方法获取函数的参数和返回值类型。
其过程:
1.如果
-methodSignatureForSelector:
返回了一个NSMethodSignature
对象(函数签名),Runtime 系统就会创建一个NSInvocation
对象,
并通过 -forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
2.如果-methodSignatureForSelector:
返回 nil。则 Runtime 系统会发出-doesNotRecognizeSelector:
消息,程序也就崩溃了
所以可以在-forwardInvocation:
方法中对消息进行转发
。
其主要方法:
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
例子:
#import "ViewController.h" #import <objc/runtime.h> @interface Person : NSObject - (void)fun; @end @implementation Person - (void)fun { NSLog(@"fun"); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 执行 fun 函数 [self performSelector:@selector(fun)]; } + (BOOL)resolveInstanceMethod:(SEL)sel { return YES; // 为了进行下一步 消息接受者重定向 } - (id)forwardingTargetForSelector:(SEL)aSelector { return nil; // 为了进行下一步 消息重定向 } // 获取函数的参数和返回值类型,返回签名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } // 消息重定向 - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL sel = anInvocation.selector; // 从 anInvocation 中获取消息 Person *p = [[Person alloc] init]; if([p respondsToSelector:sel]) { // 判断 Person 对象方法是否可以响应 sel [anInvocation invokeWithTarget:p]; // 若可以响应,则将消息转发给其他对象处理 } else { [self doesNotRecognizeSelector:sel]; // 若仍然无法响应,则报错:找不到响应方法 } } @end //日志输出: 2019-09-01 23:24:34.911774+0800 XKRuntimeKit[30032:8724248] fun
从执行任务的输出日志中,可以看到:
在
-forwardInvocation:
方法里面让 Person 对象去执行了 fun 函数
既然 -forwardingTargetForSelector:
和 -forwardInvocation:
都可以将消息转发给其他对象处理,那么两者的区别在哪?
区别就在于
-forwardingTargetForSelector:
只能将消息转发给一个对象
。而-forwardInvocation:
可以将消息转发给多个对象
。
实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:
通过
Runtime
获取到方法实现的地址,进而动态交换两个方法的功能
关键方法:
//获取类方法的Mthod
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//获取实例对象方法的Mthod
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交换两个方法的实现
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
#import "RuntimeKit.h" #import <objc/runtime.h> @implementation RuntimeKit - (instancetype)init { self = [super init]; if (self) { //交换方法的实现,并测试打印 Method methodA = class_getInstanceMethod([self class], @selector(testA)); Method methodB = class_getInstanceMethod([self class], @selector(testB)); method_exchangeImplementations(methodA, methodB); [self testA]; [self testB]; } return self; } - (void)testA{ NSLog(@"我是A方法"); } - (void)testB{ NSLog(@"我是B方法"); } @end 日志输出: 2019-09-01 21:25:32.858860+0800 XKRuntimeKit[1662:280727] 我是B方法 2019-09-01 21:25:32.859059+0800 XKRuntimeKit[1662:280727] 我是A方法
#import "UIViewController+xk.h" #import <objc/runtime.h> @implementation UIViewController (xk) + (void)load{ //获取系统方法地址 Method sytemMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:)); //获取自定义方法地址 Method customMethod = class_getInstanceMethod([self class], @selector(run_viewWillAppear:)); //判断存在与否 if (!class_addMethod([self class], @selector(viewWillAppear:), method_getImplementation(customMethod), method_getTypeEncoding(customMethod))) { method_exchangeImplementations(sytemMethod, customMethod); } else{ class_replaceMethod([self class], @selector(run_viewWillAppear:), method_getImplementation(sytemMethod), method_getTypeEncoding(sytemMethod)); } } - (void)run_viewWillAppear:(BOOL)animated{ [self run_viewWillAppear:animated]; NSLog(@"我是运行时替换的方法-viewWillAppear"); } - (void)run_viewWillDisappear:(BOOL)animated{ [self run_viewWillDisappear:animated]; NSLog(@"我是运行时替换的方法-viewWillDisappear"); } @end 日志输出: 2019-09-01 21:36:55.610385+0800 XKRuntimeKit[1921:310118] 我是运行时替换的方法-viewWillAppear
将该分类引入,从执行结果可以看到,但系统的控制器执行viewWillAppear
时,则会进入已经替换的方法run_viewWillAppear
之中。
在日常开发过程中,常常会使用类目Category
为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。
遗憾的是,OC的类目并不支持直接添加属性
为了实现给分类添加属性,还需借助 Runtime
的关联对象(Associated Objects)
特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上:
/** 1.给对象设置关联属性 @param object 需要设置关联属性的对象,即给哪个对象关联属性 @param key 关联属性对应的key,可通过key获取这个属性, @param value 给关联属性设置的值 @param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等) OBJC_ASSOCIATION_ASSIGN @property(assign)。 OBJC_ASSOCIATION_RETAIN_NONATOMIC @property(strong, nonatomic)。 OBJC_ASSOCIATION_COPY_NONATOMIC @property(copy, nonatomic)。 OBJC_ASSOCIATION_RETAIN @property(strong,atomic)。 OBJC_ASSOCIATION_COPY @property(copy, atomic)。 */ void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) /** 2.通过key获取关联的属性 @param object 从哪个对象中获取关联属性 @param key 关联属性对应的key @return 返回关联属性的值 */ id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) /** 3.移除对象所关联的属性 @param object 移除某个对象的所有关联属性 */ void objc_removeAssociatedObjects(id _Nonnull object)
注意:
key与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key
例子:
在UIViewController+xk.h
中新增一个name
属性:
@interface UIViewController (xk)
//新增属性:名称
@property(nonatomic,copy)NSString * name;
- (void)clearAssociatedObjcet;
@end
在UIViewController+xk.m
中补充对应的实现:
#import "UIViewController+xk.h" #import <objc/runtime.h> @implementation UIViewController (xk) //set方法 - (void)setName:(NSString *)name{ objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } //get方法 - (NSString *)name{ return objc_getAssociatedObject(self, @selector(name)); } //添加一个自定义方法,用于清除所有关联属性 - (void)clearAssociatedObjcet{ objc_removeAssociatedObjects(self); } @end
执行任务:
ViewController * vc = [ViewController new];
vc.name = @"我是根控制器";
NSLog(@"获取关联属性name:%@",vc.name);
[vc clearAssociatedObjcet];
NSLog(@"获取关联属性name:%@",vc.name);
日志输出:
2019-09-01 21:50:05.162915+0800 XKRuntimeKit[2066:335327] 获取关联属性name:我是根控制器
2019-09-01 21:50:05.163080+0800 XKRuntimeKit[2066:335327] 获取关联属性name:(null)
同样的,使用运行时还可以为类目新增一些自身没有的方法,比如给UIView
新增点击事件:
#import <objc/runtime.h> static char onTapGestureKey; static char onTapGestureBlockKey; @implementation UIView (Gesture) //添加轻拍手势 - (void)addTapGestureActionWithBlock:(onGestureActionBlock)block{ UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &onTapGestureKey); self.userInteractionEnabled = YES; if (!gesture){ gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(xk_handleActionForTapGesture:)]; [self addGestureRecognizer:gesture]; objc_setAssociatedObject(self, &onTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN); } //添加点击手势响应代码块属性 objc_setAssociatedObject(self, &onTapGestureBlockKey, block, OBJC_ASSOCIATION_COPY); } //点击回调 - (void)xk_handleActionForTapGesture:(UITapGestureRecognizer*)sender{ onGestureActionBlock block = objc_getAssociatedObject(self, &onTapGestureBlockKey); if (block) block(sender); } @end
但是使用运行时给类目新增代理属性时,需要注意循环引用
问题,由于运行时执行添加的属性都是retain
操作,所以往往在执行过程会导致对应的 delegate
得不到释放,因而会导致崩溃,对此,可以进行以下修改操作:
场景: 给UIView
新增emptyDataDelegate
空页面代理,以处理一些异常情况的显示
在UIView+EmptyDataSet.h
中新增一个emptyDataDelegate
属性:
//页面无数据代理
@protocol XKEmptyDataSetDelegate <NSObject>
@optional
//占位文字
- (NSString*)placeholderForEmptyDataSet:(UIScrollView*)scrollView;
@end
//空页面设置
@interface UIView (EmptyDataSet)
@property (nonatomic,weak) id<XKEmptyDataSetDelegate>emptyDataDelegate;
@end
在UIView+EmptyDataSet.m
中借助XKEmptyDataWeakObjectContainer
实现其方法:
//弱引用代理 @interface XKEmptyDataWeakObjectContainer : NSObject @property (nonatomic,weak,readonly)id weakObject; - (instancetype)initWithWeakObject:(id)object; @end @implementation XKEmptyDataWeakObjectContainer - (instancetype)initWithWeakObject:(id)object{ self = [super init]; if (self) { _weakObject = object; } return self; } @end static char xk_EmptyDataSetDelegateKey; //空视图设置 @implementation UIView (EmptyDataSet) - (void)setEmptyDataDelegate:(id<XKEmptyDataSetDelegate>)emptyDataDelegate{ objc_setAssociatedObject(self, &xk_EmptyDataSetDelegateKey, [[XKEmptyDataWeakObjectContainer alloc] initWithWeakObject:emptyDataDelegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id<XKEmptyDataSetDelegate>)emptyDataDelegate{ XKEmptyDataWeakObjectContainer * container = objc_getAssociatedObject(self, &xk_EmptyDataSetDelegateKey); return container.weakObject; } @end
获取类属性列表用到runtime的 class_copyPropertyList
方法,该方法接收一个类对象及返回属性数量的地址引用
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
获取类中所有的成员变量,使用的是runtime的class_copyIvarList
方法。
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
获取类中所有的方法列表,使用runtime的class_copyMethodList
方法。
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
Method method = methodList[i];
SEL mthodName = method_getName(method);
NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
}
free(methodList);
获取当前遵循的所有协议,使用class_copyProtocolList
方法。
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i<count; i++) {
Protocol *protocal = protocolList[i];
const char *protocolName = protocol_getName(protocal);
NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(propertyList); //C语言中使用Copy操作的方法,要注意释放指针,防止内存泄漏
上面几组获取类的属性列表,成员列表,方法列表及遵循的协议列表的方法最后都调用了free
函数。 这是因为C语言
中使用Copy
操作的方法,要注意释放指针
,防止内存泄漏
Runtime源码中的IMP
作为函数指针
,指向方法的实现
。通过它,可以绕开发送消息的过程
来提高函数调用的效率。当需要持续大量重复调用某个方法的时候,会十分有用,如下:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
场景:
我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性nickName,这样的操作我们就可以使用Runtime可以动态修改对象属性。
Person *ps = [[Person alloc] init]; NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //null //第一步:遍历对象的所有属性 unsigned int count; Ivar *ivarList = class_copyIvarList([ps class], &count); for (int i= 0; i<count; i++) { //第二步:获取每个属性名 Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSString *propertyName = [NSString stringWithUTF8String:ivarName]; if ([propertyName isEqualToString:@"_nickName"]) { //第三步:匹配到对应的属性,然后修改;注意属性带有下划线 object_setIvar(ps, ivar, @"allenlas"); } } NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //allenlas
归档
是一种常用的轻量型
文件存储方式,但是它有个弊端:
在归档过程中,若一个
Model
有多个属性,我们不得不对每个属性进行处理,非常繁琐
归档操作主要涉及两个方法: encodeObject
和 decodeObjectForKey
,对于这两个方法,可以利用Runtime 来进行改进:
//原理:使用Runtime动态获取所有属性 //解档操作 - (instancetype)initWithCoder:(NSCoder *)aDecoder{ self = [super init]; if (self) { unsigned int count = 0; Ivar *ivarList = class_copyIvarList([self class], &count); for (int i = 0; i < count; i++) { Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSString *key = [NSString stringWithUTF8String:ivarName]; id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivarList); //释放指针 } return self; } //归档操作 - (void)encodeWithCoder:(NSCoder *)aCoder{ unsigned int count = 0; Ivar *ivarList = class_copyIvarList([self class], &count); for (NSInteger i = 0; i < count; i++) { Ivar ivar = ivarList[i]; NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; } free(ivarList); //释放指针 }
测试:
//--测试归档
Person *ps = [[Person alloc] init];
ps.name = @"allenlas";
ps.age = 20;
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
[NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];
//--测试解档
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
NSLog(@"person-name:%@,person-age:%ld",person.name,person.age);
//person-name:allenlas,person-age:20
在日常项目开发中,经常会使用YYModel
或 MJExtension
等对接口返回的数据对象实现转模型操作。对于此,可以利用KVC
和Runtime
来进行类似的功能实现,在这个过程中需要解决的问题有:
利用Runtime实现的思路大体如下:
借助Runtime可以
动态获取
成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。
json数据:
{ "id":"10089", "name": "Allen", "age":"20", "position":"iOS开发工程师", "address":{ "country":"中国", "province": "广州" }, "tasks":[{ "name":"Home", "desc":"app首页开发" },{ "name":"Train", "desc":"app培训模块开发" },{ "name":"Me", "desc":"完成个人页面" } ] }
1.创建NSObject的类目 NSObject+model,用于实现字典转模型
//在NSObject+model.h中
NS_ASSUME_NONNULL_BEGIN
//AAModel协议,协议方法可以返回一个字典,表明特殊字段的处理规则
@protocol AAModel<NSObject>
@optional
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
@end;
@interface NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+model.h" #import <objc/runtime.h> @implementation NSObject (model) + (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary{ //创建当前模型对象 id object = [[self alloc] init]; //1.获取当前对象的成员变量列表 unsigned int count = 0; Ivar *ivarList = class_copyIvarList([self class], &count); //2.遍历ivarList中所有成员变量,以其属性名为key,在字典中查找Value for (int i= 0; i<count; i++) { //2.1获取成员属性 Ivar ivar = ivarList[i]; NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)] ; //2.2截取成员变量名:去掉成员变量前面的"_"号 NSString *propertyName = [ivarName substringFromIndex:1]; //2.3以属性名为key,在字典中查找value id value = dictionary[propertyName]; //3.获取成员变量类型, 因为ivar_getTypeEncoding获取的类型是"@\"NSString\""的形式 //所以我们要做以下的替换 NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替换: //3.1去除转义字符:@\"name\" -> @"name" ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""]; //3.2去除@符号 ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""]; //4.对特殊成员变量进行处理: //判断当前类是否实现了协议方法,获取协议方法中规定的特殊变量的处理方式 NSDictionary *perpertyTypeDic; if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){ perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil]; } //4.1处理:字典的key与模型属性不匹配的问题,如id->uid id anotherName = perpertyTypeDic[propertyName]; if(anotherName && [anotherName isKindOfClass:[NSString class]]){ value = dictionary[anotherName]; } //4.2.处理:模型嵌套模型 if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) { Class modelClass = NSClassFromString(ivarType); if (modelClass != nil) { //将被嵌套字典数据也转化成Model value = [modelClass xk_modelWithDictionary:value]; } } //4.3处理:模型嵌套模型数组 //判断当前Vaue是一个数组,而且存在协议方法返回了perpertyTypeDic if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) { Class itemModelClass = perpertyTypeDic[propertyName]; //封装数组:将每一个子数据转化为Model NSMutableArray *itemArray = @[].mutableCopy; for (NSDictionary *itemDic in value) { id model = [itemModelClass xk_modelWithDictionary:itemDic]; [itemArray addObject:model]; } value = itemArray; } //5.使用KVC方法将Vlue更新到object中 if (value != nil) { [object setValue:value forKey:propertyName]; } } free(ivarList); //释放C指针 return object; } @end
2.分别新建UserModel、AddressModel、TasksModel
对json处理进行处理:
UserModel类
#import "NSObject+model.h" #import "AddressModel.h" #import "TasksModel.h" NS_ASSUME_NONNULL_BEGIN @interface UserModel : NSObject<AAModel> //普通属性 @property (nonatomic, copy) NSString * uid; @property (nonatomic, copy) NSString * name; @property (nonatomic, copy) NSString * position; @property (nonatomic, assign) NSInteger age; //嵌套模型 @property (nonatomic, strong) AddressModel *address; //嵌套模型数组 @property (nonatomic, strong) NSArray *tasks; @end NS_ASSUME_NONNULL_END @implementation UserModel + (NSDictionary<NSString *,id> *)modelContainerPropertyGenericClass{ //需要特别处理的属性 return @{@"tasks" : [TasksModel class],@"uid":@"id"}; } @end
AddressModel类
#import "NSObject+model.h"
NS_ASSUME_NONNULL_BEGIN
@interface AddressModel : NSObject
@property (nonatomic, copy) NSString * country;
@property (nonatomic, copy) NSString * province;
@end
NS_ASSUME_NONNULL_END
@implementation AddressModel
@end
TasksModel类
#import "NSObject+model.h"
NS_ASSUME_NONNULL_BEGIN
@interface TasksModel : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * desc;
@end
NS_ASSUME_NONNULL_END
@implementation TasksModel
@end
3.代码测试
- (void)viewDidLoad { [super viewDidLoad]; //读取JSON数据 NSDictionary * jsonData = @{ @"id":@"10089", @"name": @"Allen", @"age":@"20", @"position":@"iOS开发工程师", @"address":@{ @"country":@"中国", @"province":@"广州" }, @"tasks":@[@{ @"name":@"Home", @"desc":@"app首页开发" },@{ @"name":@"Train", @"desc":@"app培训模块开发" },@{ @"name":@"Me", @"desc":@"完成个人页面" } ] }; //字典转模型 UserModel * user = [UserModel xk_modelWithDictionary:jsonData]; TasksModel * task = user.tasks[0]; NSLog(@"%@",task.name); }
其执行结果,数据结构如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。