iOS基础全面分析之三(KVO全面分析)

iOS基础全面分析之一(KVC全面分析)
iOS基础全面分析之二(RunLoop全面分析)
iOS基础全面分析之三(KVO全面分析)

KVO全面分析

简介

KVO的全程是 Key-Value Observing,翻译过来就是键值监听,可以用于监听某个对象属性值的改变

API

相关的API在NSKeyValueObserving.h文件中都有声明

常用的API

主要是下面几种方法,这是KVO的基本组成部分

基本使用API

//基本使用
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;  //添加观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); //删除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;  //删除观察者

观察者的回调

//观察者的回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

KVO属性依赖关系

//观察者依赖关系  需要对象中重写
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));  //全部属性的依赖关系建立
+ (NSSet<NSString *> *)keyPathsForValuesAffecting<#DependentKey#> //单独属性的依赖关系建立

KVO属性观察自动通知

//KVO属性观察自动通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;//只有添加到观察者中的key才能调用这个方法,根据key的名称来确认这个key是否能够被观察  YES可以NO不可以

+ (BOOL)automaticallyNotifiesObserversOf<key>  //单个属性是否需要通知

上述返回值为YES,设置好观察属性以及观察回调以后就会自动通知


KVO属性观察手动通知

//手动通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;//只有添加到观察者中的key才能调用这个方法,根据key的名称来确认这个key是否能够被观察  YES可以NO不可以

+ (BOOL)automaticallyNotifiesObserversOf<key>  //单个属性是否需要通知

上述返回值为NO,设置好观察属性以及观察回调以后,属性修改并不会自动的去调用回调,而是需要在修改属性的前后加上两个方法。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

KVO的原理分析

isa指向问题

在原理分析中我们需要知道isa指针的含义,先简单的回顾一下,isa指向的一个结构图:

  • isa 指针指向
    • 类的实例对象的 isa 指向该类;该类的 isa 指向该类的 MetaClass
    • MetaCalss的isa对象指向RootMetaCalss
    • 如果MetaClass是RootMetaCalss,那么该MetaClass的isa指针指向它自己

创建KVO以后的isa指向

验证上述改变

在类的实例对象设置观察者以后,类的isa指向发生了改变,如下图:

KVO原理之isa指向.png

NSKVONotifying_<ObjectClass>这个对象如何得到的,观察者设置好以后,debug时在Xcode``lldb我们可以得到这个对象,如下图:

isa指向变更验证.png

什么时候生成的 NSKVONotifying_<ObjectClass

代码如下:

p = [Person3 new];
    
Class class = NSClassFromString(@"NSKVONotifying_Person3");
if(class){
    NSLog(@"NSKVONotifying_Person3 存在");
}else{
    NSLog(@"NSKVONotifying_Person3 不存在");
}
    
[p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
    
Class class1 = NSClassFromString(@"NSKVONotifying_Person3");
    
if(class1){
    NSLog(@"NSKVONotifying_Person3 存在");
}else{
    NSLog(@"NSKVONotifying_Person3 不存在");
}

上述代码证明, NSKVONotifying_<ObjectClass是在注册观察者以后生成的

NSKVONotifying_<ObjectClass

NSKVONotifying_<ObjectClass当中都有什么方法

在注册了观察者以后我们查看,新添加的NSKVONotifying_<ObjectClass中都有哪些方法。

[p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
Class class1 = NSClassFromString(@"NSKVONotifying_Person3");
[self printfMethod:class1];

- (void)printfMethod:(Class)class{
    unsigned int count = 0;
    Method *methods = class_copyMethodList(class, &count);
    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i< count ; i++) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        IMP im = method_getImplementation(method);
        NSString * selstr = NSStringFromSelector(sel);
        NSLog(@"%@",selstr);
        [array addObject:selstr];
    }
    NSLog(@"%@",array);
    free(methods);
}

用lldb查看NSKVONotifying_<ObjectClass中方法以及imp的实现

//1
(lldb) po im
(Foundation`_NSSetObjectValueAndNotify)
2019-06-29 15:32:24.974049+0800 KVO的全面分析[4370:607232] setName:
//2
(lldb) po im
(Foundation`NSKVOClass)
2019-06-29 15:36:09.822166+0800 KVO的全面分析[4370:607232] class
//3
(lldb) po im
(Foundation`NSKVODeallocate)
2019-06-29 15:36:37.790595+0800 KVO的全面分析[4370:607232] dealloc
//4
(lldb) po im
(Foundation`NSKVOIsAutonotifying)
2019-06-29 15:36:56.961576+0800 KVO的全面分析[4370:607232] _isKVOA

NSKVONotifying_<ObjectClass的SuperClass验证

[self printClasses:[Person3 class]];
/// 打印这个类常见的实例的所有父类
- (void) printClasses:(Class) cls {
    
    /// 注册类的总数
    int count = objc_getClassList(NULL, 0);
    
    /// 创建一个数组, 其中包含给定对象
    NSMutableArray* array = [NSMutableArray arrayWithObject:cls];
    
    /// 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    
    /// 遍历s
    for (int i = 0; i < count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [array addObject:classes[i]];
        }
    }
    
    free(classes);
    
    NSLog(@"classes = %@", array);
}

打印结果:

2019-06-29 15:50:23.740570+0800 KVO的全面分析[4607:663940] classes = (
    Person3,
    "NSKVONotifying_Person3"
)

上述证明NSKVONotifying_Person3 是Person3的子类

_NSSetObjectValueAndNotify这个方法的实现逻辑

在lldb中我们添加一个观察:

wetchpoint set variable self->p->_name

从汇编当中我们可以看到这个方法大概使用了哪些逻辑

willChangeValueForKey
[Person setName];
didChangeValueForKey
NSKeyValueNotifyobserver
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

注册监听以后Class应该是NSKVONotifying_<ObjectClass>,为什么我们打印calss的时候还是<ObjectClass>

猜测可能是在NSKVONotifying_<ObjectClass>重载了-(Class)class

+ (Class)class{
    return class_getSuperclass(object_getClass(self));
}

移除监听以后NSKVONotifying_<ObjectClass>存在还是不存在?

- (void)dealloc中移除观察者

- (void)dealloc
{
    [p removeObserver:self forKeyPath:@"name"];
}
移除监听观察者类是否还存在.png

然后在打印所有<ObjectClass>的子类

2019-06-29 16:14:20.841977+0800 KVO的全面分析[4807:728105] classes = (
    Person3,
    "NSKVONotifying_Person3"
)

并没有消失

自定义KVO

自定义KVO的步骤

  1. 定义添加观察者- (void)GG_addObserver:(NSObject *)observer forKeyPath:(NSString *)KeyPath options:(NSKeyValueObservingOptions)options context:(void *)context和移除观察者方法- (void)gv_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
  2. 创建一个子类 NSKVONotifying_<Objectclass>
  3. 动态添加一些方法(setter class)
  4. 修改isa的指向
  5. 用runtime关联对象方法动态添加观察者server

步骤一:定义添加观察者

- (void)GG_addObserver:(NSObject *)observer forKeyPath:(NSString *)KeyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
}
/// 移除观察者
- (void)gv_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {

}

步骤二:在添加观察者方法中创建一个子类 NSKVONotifying_<Objectclass>


//1. 拼接子类名  //person
NSString *oldName = NSStringFromClass([self class]);
NSString *newName = [NSString stringWithFormat:@"NSKVONotifying_%@",oldName];
    
//2. 创建并注册类
Class newclass = NSClassFromString(newName);
if(!newclass){
    //创建并注册类
    newclass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    objc_registerClassPair(newclass);
}

步骤三:在添加观察者方法中NSKVONotifying_<Objectclass>动态添加一些方法(setter class)

//添加一些方法
//class
Method classMethod = class_getInstanceMethod([self class], @selector(class));
const char* classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newclass, @selector(class), (IMP)GG_class, classTypes);

//自定义的class方法
Class GG_class(id self, SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

//setter
NSString *setterMethodName = setterForGetter(keyPath);
SEL setterSEL = NSSelectorFromString(setterMethodName);
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char * setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newclass, setterSEL, (IMP)GG_setter, setterTypes);


//自定义的setter方法
static void GG_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"%s", __func__);
    
    struct objc_super superStruct = {
        self,
        class_getSuperclass(object_getClass(self))
    };
    
    // 改变父类的值
    objc_msgSendSuper(&superStruct, _cmd, newValue);
    
    // 通知观察者, 值发生改变了
    // 观察者
    id observer = objc_getAssociatedObject(self, (__bridge void *)@"objc");
    NSString* setterName = NSStringFromSelector(_cmd);
    NSString* key = getterForSetter(setterName);
    
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{key:newValue}, nil);
}



//key ===>>> setKey:
static NSString  * setterForGetter(NSString *getter){
    
    if (getter.length <= 0) { return nil; }
    
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}


//set<Key>:===> Key
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];
    getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
    
    return getter;
}


步骤四:修改isa的指向

object_setClass(self, newclass);

步骤五:用runtime关联对象方法动态添加观察者server并在自定义setter方法中实现改变父类的值,并通知观察者

// 关联方法
objc_setAssociatedObject(self, (__bridge void *)@"objc", observer, OBJC_ASSOCIATION_ASSIGN);



//setter方法干的内容
static void GG_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"%s", __func__);
    
    struct objc_super superStruct = {
        self,
        class_getSuperclass(object_getClass(self))
    };
    
    // 改变父类的值
    objc_msgSendSuper(&superStruct, _cmd, newValue);
    
    // 通知观察者, 值发生改变了
    // 观察者
    id observer = objc_getAssociatedObject(self, (__bridge void *)@"objc");
    NSString* setterName = NSStringFromSelector(_cmd);
    NSString* key = getterForSetter(setterName);
    
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{key:newValue}, nil);
}

自定义BlockKVO

自定义BlockKVO的实现步骤

  1. 在NSObject的分类NSObject+KVO中添加一个blockGGKVOBlock- (void)GG_addobserverrBlock:(NSObject *)observer forkeyPath:(NSString *)keyPath handle:(GGKVOBlock)handleBlock观察者方法

  2. 观察者方法方法实现一个存储GGInfo的一个数组,来存储观察者,并使用Runtime方法关联到这个对象当中

  3. setter方法方法中获取GGInfo的数组,便利其中回调GGInfo中的block

自定义BlockKVO第一步

typedef void(^GGKVOBlock)(id observer,NSString *keyPath,id oldValue, id newValue);
- (void)GG_addobserverrBlock:(NSObject *)observer forkeyPath:(NSString *)keyPath handle:(GGKVOBlock)handleBlock;

自定义BlockKVO第二步

static const char* GGKVOAssiociateKey = "GGKVOAssiociateKey";
@interface GGInfo : NSObject

@property (nonatomic, weak) NSObject* observer;
@property (nonatomic, strong) NSString* keyPath;
@property (nonatomic, copy) GGKVOBlock hanleBlock;

@end


@implementation GGInfo

- (instancetype) initWithObserver:(NSObject*)observer forKeyPath:(NSString*) keyPath handleBlock:(GGKVOBlock) block {
    if (self == [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _hanleBlock = block;
    }
    return self;
}

@end

// 信息保存
GGInfo* info = [[GGInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:handleBlock];
NSMutableArray* array = objc_getAssociatedObject(self, GGKVOAssiociateKey);
if (!array) {
    array = [NSMutableArray array];
    objc_setAssociatedObject(self, GGKVOAssiociateKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[array addObject:info];

自定义BlockKVO第三步

static void GG_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"%s", __func__);
    
    struct objc_super superStruct = {
        self,
        class_getSuperclass(object_getClass(self))
    };
    
    NSString* keyPath = getterForSetter(NSStringFromSelector(_cmd));

    id oldValue = objc_msgSendSuper(&superStruct, NSSelectorFromString(keyPath));
    
    
    // 改变父类的值
    objc_msgSendSuper(&superStruct, _cmd, newValue);
    
    NSMutableArray* array = objc_getAssociatedObject(self, GGKVOAssiociateKey);
    if (array) {
        for (GGInfo* info in array) {
            if ([info.keyPath isEqualToString:keyPath]) {
                info.hanleBlock(info.observer, keyPath, oldValue, newValue);
                return;
            }
        }
    }
    

自定义KVO自动销毁

第一种自动销毁的方式

  1. 在被观察者对象别名person中添加dealloc方法
    • 如果不添加dealloc,NSObject->isa指针会一直往上去寻找,这样你在交换的时候会交换很多次数这个方法
  2. 如果需要自动移除观察者,我们必须在person``dealloc中移除
  3. 在对象中自定义Mydealloc并交换其dealloc
  4. Mydealloc移除我们的观察者,从而实现KVO的自动销毁
@implementation Person

// NSObject dealloc
- (void) dealloc {
    NSLog(@"%s", __func__);
}

- (void) hookDealloc {
    Method m1 = class_getInstanceMethod(object_getClass(self), NSSelectorFromString(@"dealloc"));
    Method m2 = class_getInstanceMethod(object_getClass(self), @selector(myDealloc));
    method_exchangeImplementations(m1, m2);
}

- (void) myDealloc {

 
    Class superClass = class_getSuperclass(object_getClass(self));

    object_setClass(self, superClass);

    [self myDealloc];
}

@end

第二种自动销毁的方式

第一种的销毁方式,我们必须要在Person被观察者中实现dealloc方法来实现我们的方法交换,为什么?

  • RunLoop在不断的处理事情,中间是由一些临时对象的,当RunLoop休眠或者退出的时候会调用对象的析构函数,而我们在NSObject分类中交换的该方法,所以会出现一只调用的现象,类中的方法是如果查找了,我们可以查看上面isa指向的一个图

  • 所以第二种自动销毁方式就是在新建的子类NSKVONotifying_<Objectclass>中添加dealloc方法。

在我们创建子类NSKVONotifying_<Objectclass>的时候其中添加dealloc析构方法

// 添加析构方法
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char* deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)myDealloc, deallocTypes);

KVO的概括图

KVO的内容.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 初探 Key-value observing is a mechanism that allows objects...
    xxxxxxxx_123阅读 654评论 2 2
  • 1. 介绍 键值观察是一种机制,它允许将其他对象的指定属性的更改通知给对象。 对于MVC中model层和contr...
    沉江小鱼阅读 74评论 0 1
  • KVO原理浅析 KVO,即Key-Value Observing,官方文档中的介绍是 Key-value obse...
    wilsonhan阅读 1,692评论 1 7
  • 上一篇:iOS-KVC浅谈 前言:KVO 作为 KVC 的同袍兄弟,功能更强大,聊聊 KVO。 一、KVO 简介 ...
    梦蕊dream阅读 707评论 0 0
  • KVO原理及使用 我们之前讨论过KVO的原理,知道KVO机制是生成了一个中间类NSKVONotifying,该中间...
    xxxxxxxx_123阅读 296评论 0 0