OC底层 - KVO原理总结
前言
一、KVO基础使用总结
二、KVO的底层原理探索
三、模拟系统自定义KVO
四、优化KVO实现
KVO算是IOS领域响应式编程思想的鼻祖了。它全称是:“Key-Value Observing”,主要用来监听对象属性值的改变。
基于响应式思想的框架,还有ReactiveCocoa、Rx系列以及Swift的Combine等等。这些框架在日后会一个一个来分析总结。
官网KVO的具体介绍:官网指南
一、KVO基础使用总结
1、KVO和KVC一样,同样是NSObjct的非正式协议:NSKeyValueObserving、NSKeyValueObserverRegistration、NSKeyValueObserverNotification、NSKeyValueObservingCustomization等。
简单的KVO实现如下:
@interface MyKVOObject : NSObject
@property (nonatomic, copy) NSString *kvoName;
@end
......
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.kvoObjc = [MyKVOObject new];
//1.添加观察者
[self.kvoObjc addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
}
//2.观察属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"old = %@",change[NSKeyValueChangeOldKey]);
NSLog(@"new = %@",change[NSKeyValueChangeNewKey]);
}
- (void)dealloc {
//3.释放观察者
[self.kvoObjc removeObserver:self forKeyPath:@"kvoName"];
}
@end
2、context(上下文)的作用总结代码如下:
static void *kvoObjctContext = &kvoObjctContext;
static void *kvoInfoContext = &kvoInfoContext;
@interface ViewController ()
@property (nonatomic, strong) MyKVOObject *kvoObjc;
@property (nonatomic, strong) MyKVOInfo *kvoInfo;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.kvoObjc = [MyKVOObject new];
self.kvoInfo = [MyKVOInfo new];
//1.添加观察者
[self.kvoObjc addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:kvoObjctContext];
[self.kvoInfo addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:kvoInfoContext];
}
//2.观察属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == kvoObjctContext) {
NSLog(@"kvoObjc -> old = %@",change[NSKeyValueChangeOldKey]);
NSLog(@"kvoObjc -> new = %@",change[NSKeyValueChangeNewKey]);
} else if (context == kvoInfoContext) {
NSLog(@"kvoInfo -> old = %@",change[NSKeyValueChangeOldKey]);
NSLog(@"kvoInfo -> new = %@",change[NSKeyValueChangeNewKey]);
}
}
- (void)dealloc {
//3.释放观察者
[self.kvoObjc removeObserver:self forKeyPath:@"kvoName"];
[self.kvoInfo removeObserver:self forKeyPath:@"kvoName"];
}
增加了一个kvoInfo对象,MyKVOObject和MyKVOInfo类具有相同名称的属性,此时我们在监听方法中就可以通过context来区分不同上下文对应的属性值变化。当然我们也可以使用object去判断,但是使用context的优势就是:(1)代码可读性增加 (2)当有多个对象多个属性的时候,可以更方便的去做归类的判断。
3、移除观察者
每次创建完监听之后,在dealloc方法中需要移除观察者。如果不移除,"可能"会造成野指针的问题(EXC_BAD_ACCESS )。
比如一个单利对像添加了一个观察者A,在离开页面时没有移除它。当其他地方再发消息通知所有添加的观察者,因为观察者A已经被释放了,所以再给观察者A发消息的时候就会造成野指针访问。(如果由于循环引用的问题,造成观察A没有被释放的话,那不会造成野指针访问。这就是为什么有时候KVO莫名其妙的就闪退,有时候又没有问题)。
4 、手动添加观察
@implementation MyKVOObject
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return false;
}
- (void)setKvoName:(NSString *)kvoName {
[self willChangeValueForKey:@"kvoName"];
_kvoName = kvoName;
[self didChangeValueForKey:@"kvoName"];
}
@end
(1)通过+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key来控制手动和自动观察。
(2)如果是手动观察,需要在set方法里面去执行willChangeValueForKey和didChangeValueForKey方法。
5、合并监听
@interface MyKVOObject : NSObject
@property (nonatomic, copy) NSString *kvoName;
@property (nonatomic, copy) NSString *kvoSubName1;
@property (nonatomic, copy) NSString *kvoSubName2;
@end
.......
@implementation MyKVOObject
+ (NSSet <NSString *>*)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet <NSString *>*keypaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"kvoName"]) {
NSArray *keys = @[@"kvoSubName1",@"kvoSubName2"];
keypaths = [keypaths setByAddingObjectsFromArray:keys];
}
return keypaths;
}
- (NSString *)kvoName {
return [NSString stringWithFormat:@"%@ - %@",_kvoSubName1,_kvoSubName2];
}
@end
通过keyPathsForValuesAffectingValueForKey方法,判断当key等于kvoName时,设置keypaths为kvoSubName1和kvoSubName2。当改变kvoSubName1或者kvoSubName2的值时,也会收到通知消息。
6、数组监听
- (void)viewDidLoad {
[super viewDidLoad];
//1.添加观察者
[self.kvoObjc addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
//设置dataArray
[self.kvoObjc setValue:[[NSMutableArray alloc] init] forKey:@"dataArray"];
}
//2.观察属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSString *kind = change[NSKeyValueChangeKindKey];
switch ([kind intValue]) {
case NSKeyValueChangeSetting:
NSLog(@"设置 - %@",change);
break;
case NSKeyValueChangeInsertion:
NSLog(@"添加 - %@",change);
break;
case NSKeyValueChangeRemoval:
NSLog(@"移除 - %@",change);
break;
case NSKeyValueChangeReplacement:
NSLog(@"替换 - %@",change);
break;
}
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[[self.kvoObjc mutableArrayValueForKey:@"dataArray"] addObject:@"1"];
[[self.kvoObjc mutableArrayValueForKey:@"dataArray"] addObject:@"2"];
[[self.kvoObjc mutableArrayValueForKey:@"dataArray"] removeObject:@"1"];
[[self.kvoObjc mutableArrayValueForKey:@"dataArray"] replaceObjectAtIndex:0 withObject:@"3"];
}
..........
简单的说,KVO是建立在KVC基础之上的。监听数组时,必须使用mutableArrayValueForKey方法获取到数组然后执行操作。另外通过以上代码可以看到,NSKeyValueChangeKindKey可以来判断是设置、添加、移除、替换的操作。
二、KVO的底层原理探索
1、首先明确一点,KVO只能观察属性,不能观察成员变量。
2、获取一个类的所有子类的方法:
- (void)printSubClass:(Class)curClass {
//获取注册类的总数count (注意不包括元类,因为如果知道一个类,通isa就能找到元类)
int count = objc_getClassList(NULL, 0);
//获取所有已注册的类
Class *allClass = (Class *)malloc(sizeof(Class) * count);
objc_getClassList(allClass, count);
//遍历所有类,将curClass的子类加入数组
NSMutableArray *mArray = [[NSMutableArray alloc] init];
for (int i = 0; i < count; i++) {
Class cls = allClass[i];
if (curClass == class_getSuperclass(cls)) {
[mArray addObject:cls];
}
}
free(allClass);
NSLog(@" ==== %@",mArray);
}
//底层:
int
objc_getClassList(Class *buffer, int bufferLen)
{
mutex_locker_t lock(runtimeLock);
realizeAllClasses();
return objc_getRealizedClassList_nolock(buffer, bufferLen);
}
static int
objc_getRealizedClassList_nolock(Class *buffer, int bufferLen)
{
int count = 0;
//buffer存在就进行赋值
if (buffer) {
int c = 0;
foreach_realized_class([=, &count, &c](Class cls) {
count++;
if (c < bufferLen) {
buffer[c++] = cls;
}
return true;
});
} else {
//buffer不存在就进行count++
foreach_realized_class([&count](Class cls) {
count++;
return true;
});
}
return count;
}
3、通过上面获取子类的方法,我们可以断点跟踪一下" [self.kvoObjc addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew context:NULL];"前后有什么不同。

如图所示,经过调用addObserver:forKeyPath:options:context 方法之后,MyKVOObject多了一个子类"NSKVONotifying_MyKVOObject"。
我们通过x/4gx 打印kvoObjc:

或者 通过object_getClassName获取当前的类

此时kvoObjc的isa指向了NSKVONotifying_MyKVOObject。
也就证明了kvo会动态的创建一个子类。
4、探索中间子类
(1)首先看下NSKVONotifying_MyKVOObject中有什么方法:
......
[self printClassAllMethod:objc_getClass("NSKVONotifying_MyKVOObject")];
//获取类中所有方法
- (void)printClassAllMethod:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
}
打印结果:

通过打印的方法可以猜测,是在这个子类中进行了set方法的监听。
(2)那么问题来了,这些方法在子类中是继承还是重写呢?
答案肯定是重写了,因为class_copyMethodList是获取当前类中的方法,
Method *
class_copyMethodList(Class cls, unsigned int *outCount)
{
unsigned int count = 0;
Method *result = nil;
if (!cls) {
if (outCount) *outCount = 0;
return nil;
}
mutex_locker_t lock(runtimeLock);
const auto methods = cls->data()->methods();
ASSERT(cls->isRealized());
count = methods.count();
if (count > 0) {
result = (Method *)malloc((count + 1) * sizeof(Method));
count = 0;
for (auto& meth : methods) {
result[count++] = &meth;
}
result[count] = nil;
}
if (outCount) *outCount = count;
return result;
}
(3)核心就在于子类重写的那个set方法是如何实现监听值的。那么在研究核心问题之前,先来研究一下“取消监听之后,子类是否移除,isa是否会指回原来的类?”
这个问题其实很好验证:

在dealloc打个断点,当在执行removeObserver之前,打印self.kvoObjc依然指向NSKVONotifying_MyKVOObject;removeObserver之后,self.kvoObjc的isa重新指向了MyKVOObject。此时继续打印MyKVOObject的子类,发现NSKVONotifying_MyKVOObject依然存在,并没有从内存中移除。
(4)言归正传,继续来看子类的那个set方法实现了什么。
这里直接进行汇编探索,通过lldb的命令 watchpoint set variable 来设置观察断点:

然后当kvoName值改变的时候,继续看汇编:
在设置kvoName值的前后,分别出现了NSKeyValueWillChange和NSKeyValueDidChange。所以这里可以简单的写下伪代码:
- (void)setKvoName:(NSString *)kvoName {
....
willChange
[super setKvoName:kvoName];
didChange
....
}
以上就是对KVO底层实现的整体分析。
三、模拟系统自定义KVO
KVO的原理分析完之后,我们先尝试模拟系统的实现来自定义一个KVO
创建NSObject+MyKVO的分类,声明方法:
@interface NSObject (MyKVO)
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options context:(nullable void *)context;
- (void)my_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)my_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end
核心的实现部分如下:
1、验证是否存在setter方法
#pragma mark - 验证是否存在setter方法
- (void)verifySetterMethodFromKeyPath:(NSString *)keyPath {
//获取当前的类
Class curClass = object_getClass(self);
//通过keyPath获取sel
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
//判断当前类中是否有setter方法
Method setterMethod = class_getInstanceMethod(curClass, setterSEL);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"can not find %@ property",keyPath] userInfo:nil];
}
}
#pragma mark - get/set 名称获取
//从get方法中获取setter方法的名称
static NSString *setterForGetter(NSString *getter) {
if ([getter length] <= 0) {
return nil;
}
//第一个字母大写
NSString *nGetter = [getter capitalizedString];
return [NSString stringWithFormat:@"set%@:",nGetter];
}
//从set方法中获取getter方法的名称
static NSString *getterForSetter(NSString *setter) {
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
2、动态生成子类 (申请类、注册、添加方法)
#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
//类名称
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kMyKVOPrefix,oldClassName];
//防止重复创建生成新类
Class newClass = NSClassFromString(newClassName);
if (newClass) {
return newClass;
}
/**
* 如果内存不存在,创建生成
* 参数一: 父类
* 参数二: 新类的名字
* 参数三: 新类的开辟的额外空间
*/
//申请类
newClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
//注册类
objc_registerClassPair(newClass);
//添加class,这里-(Class)class; 的底层是object_getClass(self);这里相当于重写了class实例方法。
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)my_class, classTypes);
return newClass;
}
Class my_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
其中需要注意3个地方:
(1)在创建子类之后添加了-(Class)class方法。
原因是:-(Class)class的底层实现是object_getClass(self),是获取当前对象isa指向的class。由于不应该改变原有类的逻辑,所以这里重写了-(Class)class,使得外界获取class的时候,还是原来的类。
(2)假如添加成员变量,那么一定要在objc_allocateClassPair和objc_registerClassPair之间,因为在注册类之后,ro(clean memery)是不能进行修改的。
class_addIvar的核心源码如下:
// class allocated but not yet registered
#define RW_CONSTRUCTING (1<<26)
BOOL
class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *type)
{
.....
// No class variables
if (cls->isMetaClass()) {
return NO;
}
// Can only add ivars to in-construction classes.
// 这里会判断类是否注册
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
return NO;
}
..........
// fixme allocate less memory here
ivar_list_t *oldlist, *newlist;
//判断类中是否有成员变量
if ((oldlist = (ivar_list_t *)cls->data()->ro()->ivars)) {
size_t oldsize = oldlist->byteSize();
newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
memcpy(newlist, oldlist, oldsize);
free(oldlist);
} else {
newlist = (ivar_list_t *)calloc(ivar_list_t::byteSize(sizeof(ivar_t), 1), 1);
newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
}
..........
//重新设置ivars
ro_w->ivars = newlist;
//重新计算空间大小
cls->setInstanceSize((uint32_t)(offset + size));
return YES;
}
(3)重点说一下class_addProperty方法:
objc_property_attribute_t type = { "T", "@\"NSString\"" };//T:类型
objc_property_attribute_t ownership = { "C", "" }; // C:copy
objc_property_attribute_t nonatomic = { "N", "" }; // N:nonatomic
objc_property_attribute_t backingivar = { "V", "_myVar" };//V: 实例变量
objc_property_attribute_t attributs[] = { type, ownership,nonatomic, backingivar };
class_addProperty(newClass,
"myVar",
attributs,4);
如上面代码,动态创建了一个myVar的属性,但是此时类中的属性和成员变量是什么情况呢?
继续来打印一下类中属性和成员变量。
//打印属性
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList(object_getClass(self), &count);
for (unsigned int i = 0; i< count; i++) {
const char *name = property_getName(propertyList[i]);
NSLog(@" == %@",[NSString stringWithUTF8String:name]);
objc_property_t property = propertyList[i];
const char *a = property_getAttributes(property);
NSLog(@"属性信息 == %@",[NSString stringWithUTF8String:a]);
}
//打印成员变量
unsigned int iCount;
Ivar *ivars = class_copyIvarList(object_getClass(self), &iCount);
for (int i = 0; i< iCount; i++) {
Ivar var = ivars[i];
const char *name = ivar_getName(var);
printf("成员变量 == %s",name);
}

从打印结果来看,只添加了属性,并没有添加成员变量。
在从源码来分析一下:
static bool
_class_addProperty(Class cls, const char *name,
const objc_property_attribute_t *attrs, unsigned int count,
bool replace)
{
//参数校验
if (!cls) return NO;
if (!name) return NO;
//获取对应的属性
property_t *prop = class_getProperty(cls, name);
//1.如果属性存在并且不需要替换时,直接返回
if (prop && !replace) {
// already exists, refuse to replace
return NO;
}
else if (prop) {
//2.如果属性存在,替换属性设置
// replace existing
mutex_locker_t lock(runtimeLock);
try_free(prop->attributes);
prop->attributes = copyPropertyAttributeString(attrs, count);
return YES;
}
else {
//2.如果属性不存在,进行添加
mutex_locker_t lock(runtimeLock);
//创建rwe
auto rwe = cls->data()->extAllocIfNeeded();
ASSERT(cls->isRealized());
property_list_t *proplist = (property_list_t *)
malloc(property_list_t::byteSize(sizeof(property_t), 1));
proplist->count = 1;
proplist->entsizeAndFlags = sizeof(property_t);
proplist->begin()->name = strdupIfMutable(name);
proplist->begin()->attributes = copyPropertyAttributeString(attrs, count);
//添加到rwe的proplist中
rwe->properties.attachLists(&proplist, 1);
return YES;
}
}
class_addProperty并不会添加ivar和get/set方法。
那么如果要实现真正的属性添加,我测试了2个方法可以实现 :
<1>在创建类之后,注册类之前添加成员变量,同时在属性添加完之后添加get/set方法;
<2>不添加成员变量的情况下,使用一个全局字典来保存“get/set方法维护的变量”。
3、添加setter方法:
- (void)addSetterMethod:(Class)newClass keyPath:(NSString *)keyPath {
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)my_setter, setterTypes);
}
static void my_setter(id self,SEL _cmd,id newValue) {
//获取到keypath,也是get方法
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
//拿到旧值
id oldValue = [self valueForKey:keyPath];
//获取父类 class_getSuperclass(object_getClass(self)) 或者 直接[self class]
Class superClass = [self class];
//方法1: 获取父类方法的imp调用
// void (*superIMP)(id,SEL,id);
// superIMP = class_getMethodImplementation(superClass, _cmd);
// superIMP(self,_cmd,newValue);
//方法2:通过objc_msgSend
//记录当前的cls
// Class curClass = object_getClass(self);
//将self的isa指向父类
// object_setClass(self, superClass);
//发送消息
// objc_msgSend(self, _cmd,newValue);
//完成后再将isa指回来
// object_setClass(self, curClass);
//方法3:通过objc_msgSendSuper消息发送
void(*my_msgSendSuper)(void *,SEL,id) = (void*)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = superClass
};
my_msgSendSuper(&superStruct,_cmd,newValue);
/*
my_addObserver的时候通过关联对象进行信息保存
//4.保存信息
MyKVOInfo *info = [[MyKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
//这里简单的用数组实现,当然用字典也可以更好的优化
NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
if (!infoArray) {
infoArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, &kMyKVOAssiociateKey, infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[infoArray addObject:info];
*/
//拿到观察者
NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
for (MyKVOInfo *info in infoArray) {
if ([info.keyPath isEqualToString:keyPath]) {
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
//对新旧值处理
if (info.options & MyKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & MyKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
} else {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
//消息发送给观察者
SEL observerSEL = @selector(my_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.oberver,observerSEL,keyPath,self,change,NULL);
}
}
}
<1> 通过_cmd获取keypath
<2>通过kvc获取旧值
<3>获取父类
<4>通过3种方法(1、获取父类方法imp直接调用 2、通过objc_msgSend发送消息 3、通过objc_msgSendSuper发送消息)调用父类set方法。
<5>拿到观察者信息,给观察者发送消息。
4、设置对象的isa指向新创建的类
object_setClass(self, newClass);
5、添加观察者的方法总结为:
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options context:(nullable void *)context {
//1.验证是否有setter方法
[self verifySetterMethodFromKeyPath:keyPath];
//2.动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
//3.添加setter方法
[self addSetterMethod:newClass keyPath:keyPath];
//4.设置isa的指向
object_setClass(self, newClass);
//5.保存信息
MyKVOInfo *info = [[MyKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
//这里简单的用数组实现,当然用字典也可以更好的优化
NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
if (!infoArray) {
infoArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, &kMyKVOAssiociateKey, infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[infoArray addObject:info];
}
6、移除观察者
- (void)my_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
//遍历判断keyPath,然后移除info
NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
if (infoArray.count <= 0) {
return;
}
for (MyKVOInfo *info in infoArray) {
if ([info.keyPath isEqualToString:keyPath] && info.oberver == observer) {
[infoArray removeObject:info];
break;
}
}
if (infoArray.count <= 0) {
//isa指回父类
Class class = [self class];
object_setClass(self, class);
}
}
7、MyKVOInfo补充:
typedef NS_OPTIONS(NSUInteger, MyKeyValueObservingOptions) {
MyKeyValueObservingOptionNew = 0x01,
MyKeyValueObservingOptionOld = 0x02,
};
@interface MyKVOInfo : NSObject
////此处使用unsafe_unretained,因为在dealloc去遍历移除的时候,如果用weak,底层会因为多线程的原因,observer已经置为nil了。(但是如果不及时remove的话,会造成也指针访问)
@property (nonatomic, unsafe_unretained) id oberver;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) MyKeyValueObservingOptions options;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options;
@end
.................
@implementation MyKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options {
self = [super init];
if (self) {
self.oberver = observer;
self.keyPath = keyPath;
self.options = options;
}
return self;
}
@end
以上就是模拟系统自定义KVO的全部过程。
在使用KVO的过程中有2个地方使用起来比较麻烦:
(1)每次添加完观察者,都必须手动移除掉。
(2)不支持函数式。
另外以上自定义KVO,不支持基本数据类型的监听。
接下来,对自定义KVO进行优化。
四、优化KVO实现
1、支持函数式
首先修改声明的方法
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath callBack:(LLKVOBlock)callBack;
改成block回调的方式通知属性值改变。
然后改造MyKVOInfo的保存信息
typedef void(^LLKVOBlock)(NSString *keyPath,id oldValue,id newValue);
@interface LLKVOInfo : NSObject
//此处使用unsafe_unretained,因为在dealloc去遍历移除的时候,如果用weak,底层会因为多线程的原因,observer已经置为nil了。(但是如果不及时remove的话,会造成也指针访问)
@property (nonatomic, unsafe_unretained) id oberver;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) LLKVOBlock callBack;
- (instancetype)initWithObserver:(id)observer KeyPath:(NSString *)keyPath handleBlock:(LLKVOBlock)callBack;
@end
相当于每次监听都会多保存一个block。
最后改造setter方法:
//setter方法
void my_setter(id self,SEL _cmd,id newValue) {
//获取keyPath
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
//kvc->获取旧值
id oldValue = [self valueForKey:keyPath];
//获取父类
Class superClass = [self class];
//给父类发消息
void (*my_msgSendSuper)(void *,SEL,id) = (void*)objc_msgSendSuper;
struct objc_super my_objc_super = {
.receiver = self,
.super_class = superClass
};
my_msgSendSuper(&my_objc_super,_cmd,newValue);
//信息数据回调
NSMutableArray *infoArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLLKVOAssiociateKey));
for (LLKVOInfo *info in infoArray) {
if ([info.keyPath isEqualToString:keyPath]) {
info.callBack(keyPath, oldValue, newValue);
}
}
}
这里也可以进一步的优化保存信息:把infoArray改成infoDic(key = keyPath,value = info数组)。这样的话,可以在信息回调时,根据keyPath找到所有监听keyPath的block,然后遍历发送。
2、支持基本数据类型监听
这个功能可以参考DIS_KVC_KVO的源码来实现:
void _DSKVONotifyingEnableForInfoAndKey(DSKeyValueNotifyingInfo *info, NSString *key) {
..........
DSKeyValueSetter * setter = _DSKeyValueSetterForClassAndKey(info->originalClass, key, info->originalClass);
if([setter isKindOfClass: [DSKeyValueMethodSetter class]]) {
Method setMethod = [(DSKeyValueMethodSetter *)setter method];
const char *encoding = method_getTypeEncoding(setMethod);
if (*encoding == 'v') {
char *argType = method_copyArgumentType(setMethod, 2);
IMP imp = NULL;
switch (*argType) {
case 'c': {
imp = (IMP)_DSSetCharValueAndNotify;
} break;
case 'd': {
imp = (IMP)_DSSetDoubleValueAndNotify;
} break;
case 'f': {
imp = (IMP)_DSSetFloatValueAndNotify;
} break;
case 'i': {
imp = (IMP)_DSSetIntValueAndNotify;
} break;
case 'l': {
imp = (IMP)_DSSetLongValueAndNotify;
} break;
case 'q': {
imp = (IMP)_DSSetLongLongValueAndNotify;
} break;
case 's': {
imp = (IMP)_DSSetShortValueAndNotify;
} break;
case 'S': {
imp = (IMP)_DSSetUnsignedShortValueAndNotify;
} break;
case '{': {
if(strcmp(argType, @encode(CGPoint)) ==0) {
imp = (IMP)_DSSetPointValueAndNotify;
}
#if TARGET_OS_OSX
else if (strcmp(argType, @encode(NSPoint)) == 0) {
imp = (IMP)_DSSetPointValueAndNotify;
}
#endif
else if (strcmp (argType, @encode(NSRange)) == 0) {
imp = (IMP)_DSSetRangeValueAndNotify;
}
else if (strcmp(argType,@encode(CGRect)) == 0) {
imp = (IMP)_DSSetRectValueAndNotify;
}
#if TARGET_OS_OSX
else if (strcmp(argType,@encode(NSRect)) == 0) {
imp = (IMP)_DSSetRectValueAndNotify;
}
#endif
else if(strcmp(argType, @encode(CGSize)) == 0) {
imp = (IMP)_DSSetSizeValueAndNotify;
}
#if TARGET_OS_OSX
else if (strcmp(argType, @encode(NSSize)) == 0) {
imp = (IMP)_DSSetSizeValueAndNotify;
}
#endif
else {
imp = (IMP)_CF_forwarding_prep_0;
}
} break;
case 'B': {
imp = (IMP)_DSSetBoolValueAndNotify;
} break;
case 'C': {
imp = (IMP)_DSSetUnsignedCharValueAndNotify;
} break;
case 'I': {
imp = (IMP)_DSSetUnsignedIntValueAndNotify;
} break;
case 'L': {
imp = (IMP)_DSSetUnsignedLongValueAndNotify;
} break;
case 'Q': {
imp = (IMP)_DSSetUnsignedLongLongValueAndNotify;
} break;
case '#':
case '@':{
imp = (IMP)_DSSetObjectValueAndNotify;
} break;
default: {
//
} break;
}
............
核心原理是:
<1> 根据方法签名判断返回值是是void类型
<2> 根据method_copyArgumentType(setMethod, 2) 获取参数类型。
<3> 根据参数类型实现不同的setter方法。
3、自动销毁机制实现
在实现自动销毁机制之前,先探索几个问题:
(1)我们可以在分类里面重写dealloc吗?
很明显,分类肯定不能重写dealloc方法,重写的话那么不就把系统的dealloc方法给覆盖了吗?那还怎么去释放对象呢?
如果是在NSObject的分类中去重写dealloc,确实会覆盖掉系统的dealloc方法。
如果是在非NSObject的类中去重写dealloc,通过汇编跟踪,在分类的dealloc执行完之后,会进入NSObject的dealloc。这里应该是llvm中一层代码注入。
void DeallocInCategoryCheck::registerMatchers(MatchFinder *Finder) {
// Non-NSObject/NSProxy-derived objects may not have -dealloc as a special
// method. However, it seems highly unrealistic to expect many false-positives
// by warning on -dealloc in categories on classes without one of those
// base classes.
Finder->addMatcher(
objcMethodDecl(isInstanceMethod(), hasName("dealloc"),
hasDeclContext(objcCategoryImplDecl().bind("impl")))
.bind("dealloc"),
this);
}
通过llvm源码分析,证明了这一点。
所以在非NSObject分类中重写dealloc方法,不会影响系统去释放对象。但是会覆盖主类的dealloc。
(2)我们通过method swizzling交换dealloc方法:
//error!!!!!
- (void)my_addKeyPath:(NSString *)keyPath callBack:(LLKVOBlock)callBack {
..........
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
Method m1 = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
Method m2 = class_getInstanceMethod([self class], NSSelectorFromString(@"myDealloc"));
method_exchangeImplementations(m1, m2);
});
}
- (void)myDealloc {
[self myDealloc];
NSLog(@"====");
}
以上的交换方法存在2个严重问题:
<1> 如果“[self class]”(也就是原来的类)中没有实现dealloc方法的话,那么就会交换NSObject中的dealloc方法。会造成所有类的释放都会走myDealloc方法。
<2>当第一个问题成立之后,一旦在myDealloc方法中做NSLog打印,就会造成无限递归。因为NSLog打印结束也会释放对象。当然除了NSLog,还会有其他的情况造成无限递归,这里就不具体分析了。
显然如果用方法交换,会存在很多问题。
通过以上问题的探索,我们应该怎么正确的实现自动销毁机制呢?
其实不难想到,我们只需要在创建子类之后,再添加一个dealloc方法就ok了
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
class_addMethod(newClass, deallocSEL, (IMP)my_dealloc, method_getTypeEncoding(deallocMethod));
..........
void my_dealloc(id self, SEL _cmd) {
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLLKVOAssiociateKey));
if (observerArr.count <= 0) {
return;
}
for (LLKVOInfo *info in observerArr) {
[observerArr removeObject:info];
}
if (observerArr.count <= 0) {
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}