初识Runtime之KVO实现原理
Runtime运行时是在iOS代码运行时将其翻译成对应的C语言程序。
Runtime 运行时是苹果提供的一个API,属于一个C语言的库。那么在iOS开发中,Runtime运行时有什么用呢?
1. 利用Runtime运行时,在程序的运行过程中,动态创建一个类。
2. 利用Runtime运行时,在程序的运行过程中,动态修改一个类的属性/方法。
3. 利用Runtime运行时,遍历一个类的所有的属性和方法。
用的时候需要导入头文件: objc/runtime.h/objc/message.h(包含了objc/runtime.h)(需要加上尖括号,由于这个编辑器的原因,直接加上尖括号,导致无法显示)
Method:成员方法
Ivar :成员变量
上面是对Runtime运行时的一个简单概括,我之前一直只是知道有Runtime这个东西的存在,但是具体实例一直没有研究过,感觉跟Runtime相关的更多的是长篇大论的理论,所以总是不能很好的去理解Runtime具体是怎么工作和应用的。最近看了Hank老师的公开课,感觉明了了很多,下面就介绍一下用Runtime运行时来实现KVO的一个过程,基本步骤如下:
- 动态添加被监听对象类的一个子类;
- 在该子类中动态添加被监听的属性的set方法,即重写父类的属性的set方法;
- 当调用被监听的属性的set方法且改属性的值发生改变的时候通知外界。
特别注意:
一个类的属性赋值形式为object.property = …
一个类的成员变量赋值形式为object->MemberVariable = …
KVO不能观察到成员变量的值的变化,因为KVO是动态添加了一个被监听对象类的子类,然后重写了被监听对象类的属性的set方法,在该方法中来做的通知,所以只有调用set方法才能被检测到。
我这篇文章是看了Hank老师的公开课后整理的,那我先总结一下Hank老师讲的内容,下面是我做的笔记:
KVO原理和实现过程说明(非代码,最后面会给出我自己实现KVO的完整demo):
Person * _p = [[Person alloc] init];
[_p addObserver:self forKeyPath:@“age” options:NSKeyValueObservingOptionNew context:nil];
只要添加这行代码,调用时,runtime会动态添加一个Person类的子类 NSKVONotifying_Person类,然后在这个类中重写age属性的set方法,并把_p的真实类型改为NSKVONotifying_Person,然后一旦调用_p.age就是调用子类的age的set方法,一旦子类的age的值发生改变就会通知-(void)observeValueForKeyPath:ofObject:change: context;
KVO重写的set方法:
willChangeValueForKey和didChangeValueForKey都会通知–(void)observeValueForKeyPath:ofObject:change: context;方法,change是dictionary,里面放的是keyPath的旧值和新值,willChangeValueForKey会把旧值给过去,didChangeValueForKey会把新值给过去
以上是我听了Hank老师的视频总结的(如果理解有错请指教),然后我自己就想验证一下,但是写到重写set方法的时候,调用了willChangeValueForKey和didChangeValueForKey方法,然而并没有收到通知,原因我还没弄明白,但是我查了一些资料,一些只是说调用willChangeValueForKey和didChangeValueForKey方法就会触发通知,但是并没有给出具体代码。还有看到实现了KVO的,但是并不是调用的willChangeValueForKey和didChangeValueForKey方法,所以我的这个demo也没有调用willChangeValueForKey和didChangeValueForKey方法,如果有用willChangeValueForKey和didChangeValueForKey方法来实现的希望可以分享下代码。我的demo地址为KVO_Runtime_Demo
有兴趣的可以看一下demo的源码,下面我会根据我的demo的代码来说一下KVO的实现过程:
首先创建一个Person类和一个NSObject类的分类(因为需要所有类都可以调用,可参考原生KVO的addObserver方法),demo目录结构如下:
Person类代码如下:
Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic,assign) int age;
@end
Person.m
#import "Person.h"
@implementation Person
-(void)setAge:(int)age{
NSLog(@"我是Person的setAge");
//自定义操作
age = 10 + age;
_age = age;
}
@end
Person类是用来测试的类,即充当被监听的类,我们下面要监听一下Person类的age属性的变化。
直接调用系统的addObserver:self forKeyPath:options:context:方法是可以的,我们这里是要探讨KVO内部的实现,那么我们就在NSObject+ZZ_KVO这个分类里自己写一个添加观察者的方法,然后自己去用Runtime运行时来实现监听的功能。
我在NSObject+ZZ_KVO中定义一个添加监听的方法(不建议直接重写系统的addObserver:self forKeyPath:options:context:,加上前缀以作为区别):
-(void)ZZ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
然后在ViewController中定义一个Person对象,并让该Person对象调用ZZ_addObserver方法
_p = [[Person alloc] init];
[_p ZZ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
NSObject+ZZ_KVO中的代码如下:
NSObject+ZZ_KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (ZZ_KVO)
-(void)ZZ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
NSObject+ZZ_KVO.m
#import "NSObject+ZZ_KVO.h"
#import <objc/message.h>
@implementation NSObject (ZZ_KVO)
//添加观察者方法
-(void)ZZ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
//动态添加一个类
NSString * oldClassName = NSStringFromClass([self class]);
NSString * newClassName = [@"ZZKVO_" stringByAppendingString:oldClassName];
//OC转C
const char * newName = [newClassName UTF8String];
//定义一个类
Class MyClass = objc_allocateClassPair([self class], newName, 0);
//重写setAge方法
class_addMethod(MyClass, @selector(setAge:), (IMP)setAge, "");
//注册这个类
objc_registerClassPair(MyClass);
//改变isa指针,让self指向子类
object_setClass(self, MyClass);
//给对象绑定观察者对象
objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/*
有默认参数
void setAge(id self,SEL _cmd,int age){
NSLog(@"哥们来啦~~~");
}
*/
//id self,SEL _cmd,默认参数
//子类重写父类的setAge方法,OC中一般都会先调用[super method],这样如果父类的setAge方法里对age值做过什么处理,子类里面也会做一样的处理,所以在这里,也需要先调用父类的setAge方法
void setAge(id self,SEL _cmd,int age){
//子类自己
id class = [self class];
//父类
id superClass = class_getSuperclass([self class]);
//让自己指向父类
object_setClass(self, superClass);
//存放age改变之前的值
NSMutableDictionary * changeDic = [NSMutableDictionary dictionaryWithCapacity:0];
unsigned int oldOutCount = 0;
//由于已经让self指向了自己的父类,所以在取属性的时候,可以直接用[self class]拿到age
Ivar * oldivars = class_copyIvarList([self class], &oldOutCount);
for (unsigned int i = 0; i < oldOutCount; i ++) {
Ivar ivar = oldivars[i];
const char * name = ivar_getName(ivar);
NSString * nameString = [[NSString alloc] initWithUTF8String:name];
if ([nameString isEqualToString:@"_age"]) {
[changeDic setValue:[self valueForKey:nameString] forKey:[NSString stringWithFormat:@"old%@",nameString]];
}
}
free(oldivars);
//调用父类的setAge方法,类似于OC的[super setAge:age]
//objc_getAssociatedObject(<#id object#>, <#const void *key#>)这个方法需要设置xcode的一个属性才能用,默认的无法提示出这个方法并且会报错
objc_msgSend(self, @selector(setAge:),age);
//让self指向自己
object_setClass(self, class);
NSLog(@"我是ZZKVO_Person的setAge");
unsigned int newOutCount = 0;
//已经让self指向了自己,所以如果还是直接用[self class]的话是拿不到age属性的,因为age是父类的,并且现在这个子类的属性列表为空,容易在取值的时候造成崩溃,所以用superClass
Ivar * newivars = class_copyIvarList(superClass, &newOutCount);
for (unsigned int i = 0; i < newOutCount; i ++) {
Ivar ivar = newivars[i];
const char * name = ivar_getName(ivar);
NSString * nameString = [[NSString alloc] initWithUTF8String:name];
if ([nameString isEqualToString:@"_age"]) {
[changeDic setValue:[self valueForKey:nameString] forKey:[NSString stringWithFormat:@"new%@",nameString]];
}
}
free(newivars);
if ([[changeDic objectForKey:@"old_age"] intValue] != [[changeDic objectForKey:@"new_age"] intValue]) {
//通知外界
id observer = objc_getAssociatedObject(self, @"observer");
[observer ZZ_observeValueForKeyPath:@"age" ofObject:self change:changeDic context:nil];
}
}
-(void)ZZ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
}
然后我在NSObject+ZZ_KVO中的addObserve方法中打上断点,来看一下在ZZ_addObserver方法中self的类型:
可以看到这个时候self的类型是Person,经过改变isa那行代码之后:
可以看到self的类型变为我动态加的一个类了,这也跟Hank老师在视频中讲的吻合,也验证了KVO的实现步骤。系统KVO执行时添加的子类的前缀为NSKVONotifying_,这个可以行验证,也可以看Hank老师的视频,我这里就不再验证。
最后我是在ViewController中的touchBegin方法中调用了_p.age给age属性赋值,然后我们可以看一下打印的结果:
第一次touch(即age变化)
然后再touch一次
这样就简单的实现了对Person类的age属性值的变化的一个监测。
上一篇: 达梦DM8数据库的备份与还原
下一篇: matplotlib绘图库探索(一)