深入理解KVO

先来说说什么是KVO,KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法。

KVO的使用

1、注册观察者

//注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

option:

  • NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法
  • NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法
  • NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
  • NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。

context:

这里的context字面上面的意思是上下文。但是在实际的开发中,我们可以把它理解成为一个标记。通常是为了在类和类的子类中同时对一个属性进行监听时,为了区分两个监听,则可以在context中传入一个标识"person_name"。如果不需要传入值,则传入NULL即可。

那么用context有什么好处呢?在我们接收属性变化的回调的时候,同时会拿到相应的keyPathobjectchangecontext。如果去判断keyPath的话,我们需要判断缓存列表,还要判断类的列表,才能找到相应的属性值。但是context可以看做是一个静态的值放在内存中,所以用context会让性能提升不少。

2、监听回调

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {    
    NSLog(@"%@",change);
}

3、移除观察者

- (void)dealloc {    
    [self.person removeObserver:self forKeyPath:@"name"];
}

那么我们可能要思考为什么要移除观察者呢?我们再查看了官方文档之后,就能清楚的看见:

When removing an observer, keep several points in mind:

  • Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either callremoveObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.
  • An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
  • The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.

解除分配时,观察者不会自动删除自身。被观察对象继续发送通知,无视观察者的状态。但是发送到已发布对象的更改通知与任何其他消息一样,会去出发内存访问异常。因此,要确保观察者在从内存中消失之前将其移除。

KVO的监听

1、自动监听属性值

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {    
    if ([key isEqualToString:@"name"]) {        
        return YES;    
    }    
    return NO;
}

2、手动观察属性值

- (void)setName:(NSString *)name {    
    [self willChangeValueForKey:@"name"];
    _name = name;    
    [self didChangeValueForKey:@"name"];
}

KVO原理分析

在查看了官方文档之后,发现KVO的原理其实是改变了isa指针,将isa指针指向了另一个动态生成的类。为了分析其中的原理,我们在下图地方做一个断点,看看到底是生成了什么样的类。

image

然后我们用LLDB打印一下相应的类。

image

我们发现出来了一个新的类NSKVONotifing_Person。那么这个新的类跟Person类是什么关系呢?于是决定打印添加了observer前后类到底有什么变化。

调用printClasses方法可以打印所有的子类的信息:

#pragma mark - ======== 遍历类以及子类 ========
- (void)printClasses:(Class)cls {    
    //注册类的总和    
    int count = objc_getClassList(NULL, 0);    
    //创建一个数组,其中包含给定的对象    
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];    
    //获取所有已注册的类    
    Class *classes = (Class *)malloc(sizeof(Class) * count);    
    objc_getClassList(classes, count);    
    for (int i = 0; i < count; i++) {        
        if (cls == class_getSuperclass(classes[i])) {            
        [mArray addObject:classes[i]];        
        }    
    }    
    free(classes);   
    NSLog(@"classes = %@",mArray);
}

然后我们再调用前和调用后分别调用方法:

    [self printClasses:[Person class]];    
    NSLog(@"*********添加前*********");    
    //[self printClasses:NSClassFromString(@"NSKOVNotifing_Person")];        
    self.person = [[Person alloc]init];    
    self.person.name = @"zy";    
    //注册观察者    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];    
    NSLog(@"*********进去了*********");    
    [self printClasses:[Person class]];    
    NSLog(@"*********添加后:NSKVONotifying_Person*********");    
    [self printClasses:NSClassFromString(@"NSKVONotifying_Person")];

打印出来的值为

2019-04-14 20:27:48.150739+0800 KVODemo[1959:134800] classes = (

Person

)

**2019-04-14 20:27:48.150926+0800 KVODemo[1959:134800] *********添加前***********

**2019-04-14 20:27:48.151333+0800 KVODemo[1959:134800] *********进去了***********

2019-04-14 20:27:48.157911+0800 KVODemo[1959:134800] classes = (

Person,

"NSKVONotifying_Person"

)

**2019-04-14 20:27:48.158066+0800 KVODemo[1959:134800] *********添加后:NSKVONotifying_Person***********

2019-04-14 20:27:48.162250+0800 KVODemo[1959:134800] classes = (

"NSKVONotifying_Person"

)

那么我们可以明确的看到,NSKVONotifying_Person是继承与Person的,他是动态生成的Person的子类。

弄清楚了类的关系,我们再看看方法有什么变化。我们新增一个遍历所有方法的函数:

#pragma mark - ======== 遍历方法-ivar-property ========
- (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);
    }
    free(methodList);
}

然后在添加前后分别调用printClassAllMethod方法,打印的结果为:

2019-04-14 20:44:43.451245+0800 KVODemo[2131:144186] hello-0x10a759290

2019-04-14 20:44:43.451427+0800 KVODemo[2131:144186] world-0x10a7592c0

2019-04-14 20:44:43.451529+0800 KVODemo[2131:144186] nick-0x10a759320

2019-04-14 20:44:43.451633+0800 KVODemo[2131:144186] setNick:-0x10a759350

2019-04-14 20:44:43.451738+0800 KVODemo[2131:144186] .cxx_destruct-0x10a759390

2019-04-14 20:44:43.451879+0800 KVODemo[2131:144186] name-0x10a7592f0

2019-04-14 20:44:43.451963+0800 KVODemo[2131:144186] setName:-0x10a7591f0

**2019-04-14 20:44:43.452100+0800 KVODemo[2131:144186] *********添加前***********

**2019-04-14 20:44:43.452582+0800 KVODemo[2131:144186] *********进去了***********

**2019-04-14 20:44:43.452672+0800 KVODemo[2131:144186] *********添加后:NSKVONotifying_Person***********

2019-04-14 20:44:43.452786+0800 KVODemo[2131:144186] setName:-0x10aab263a

2019-04-14 20:44:43.452884+0800 KVODemo[2131:144186] class-0x10aab106e

2019-04-14 20:44:43.452968+0800 KVODemo[2131:144186] dealloc-0x10aab0e12

2019-04-14 20:44:43.453067+0800 KVODemo[2131:144186] _isKVOA-0x10aab0e0a

那么,通过对方法的地址分析,我们可以得到一个结论,NSKVONotifying_Person类重写了setName的方法,然后新增了class方法、dealloc方法和_isKVOA方法。

结论

综合上面的测试,我们可以总结出来KVO的原理:

  1. 验证是否存在setter方法,目的是为了不让实例进来
  2. 动态生成子类NSKVONotifying_Person:先开辟一个新的类,然后注册类,重写class的方法,讲class指向Person,接着重写setter方法,通过对setter赋值,实现父类的方法self.name = @"xlh",最后通过objc_getAssociatedObject关联住我们的观察者
  3. 讲isa的指针指向NSKVONotifying_Person
  4. 最后通过消息转发响应响应的回调
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • 删掉重新来一次吧,记得改那个脚本修改 /home/ubuntu/eos/scripts/install_depen...
    卢衍泓阅读 1,131评论 0 1
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • KVO是key-value observing的缩写 KVO是Objective-C对观察者设计模式的实现 App...
    pengmengli阅读 425评论 0 0
  • 对于开发人员来说,设计模式有时候就是一道坎,但是设计模式又非常有用,过了这道坎,它可以让你水平提高一个档次。而在a...
    WANKUN阅读 258评论 0 2