20.iOS底层学习之KVO 原理

本篇提纲
1、KVO简介;
2、KVO的使用;
3、KVO的一些细节;
4、KVO的底层原理;

KVO简介

KVO全称Key-Value Observing(键值观察),是允许对象在其他对象的属性发生更改是接到通知的一种途径。
想要去了解KVO,要先理解KVC。KVO是在KVC的基础上实现的。

KVO的使用

KVO的使用分为以下三步:

  • Registering as an Observer(注册一个观察者)
    一个观察者对象可以通过发送addObserver:forKeyPath:options:context:方法,把自己作为观察者传进去,key path是要被观察的属性,options和context参数是用来管理通知的。

Options:(指定为选项常量的按位或)会影响通知中提供的更改字典的内容以及生成通知的方式。
通过指定选项NSKeyValueObservingOptionOld,可以选择从更改之前接收观察到的属性的值。使用选项NSKeyValueObservingOptionNew请求属性的新值。通过这些选项中的按位或,可以同时接收旧值和新值。

Context:这个参数将在发生变化的通知调用时回传,可以是任意数据。也可以传NULL,这个时候接受到通知的时候只能通过key path这个参数来判断监听的是哪个监听生效了,但是也有可能两个类观察同一个属性,可能会导致区分不清楚。

使用示例:

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
 [account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld) context:PersonAccountBalanceContext];

这样接受到消息的时候,就可以通过context的值来判断是哪个观察者监听的生效了。

说明:键值观察方法observer:forKeyPath:options:context:method不会强持有观察的对象,被观察者,或者context,所以如果有需要你要强持有观察的对象,被观察者,或者context,避免被回收。

  • Receiving Notification of a Change(接收到发生变化的通知)
    当观察的属性发生了改变的时候,观察者会收到消息observeValueForKeyPath:ofObject:change:context:,所有的观察者,必须实现这个方法。(不实现会crash)

使用示例:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}
  • Removing an Object as an Observer (移除作为观察者的对象)
    通过方法removeObserver:forKeyPath:context:可以移除键值观察。
    使用示例:
[account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
 [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];

KVO的一些细节

  • KVO的手动实现
    在被观察者中实现automaticallyNotifiesObserversForKey方法,可以控制KVO是否自动通知。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    NSLog(@"%s key:%@",__func__,key);
    return NO;
}

返回NO的时候自动通知关闭,返回YES的时候开启。

再通过在set方法中实现willChangeValueForKey & didChangeValueForKey两个方法,完成手动通知。

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

注意手动通知不受自动开关状态的影响。如果开关打开,并且也手动实现,那么接受方法会触发两次。

  • keyPathsForValuesAffectingValueForKey

其值影响键控属性值的属性返回一组键路径。当key path的观察者向接收类的实例注册时,KVO本身会自动观察同一实例的所有key path路径,并在任何key path路径的值更改时向观察者发送key path更改通知。
这个方法会返回一个NSSet,里面的元素是可能影响到监听的属性的属性。也就是说NSSet返回的属性发生改变的时候,也会触发KVO的通知消息。

使用示例:
本类实现。

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

调用

//添加监听
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];

//触发
    self.person.writtenData += 10;
    self.person.totalData  += 1;

结果:


image.png

每点击一次屏幕,会调用两次,因为writtenData的值改变触发一次,totalData值改变再触发一次。

  • KVO对可变数组的观察
    按照属性的方法进行观察,示例:
//注册
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

//修改值
       [self.person.dateArray addObject:@"1"];

以上实现最终没有办法触发KVO,这是因为KVO是基于KVC的基础上进行实现的,数组的addObject的底层实现如下:

- (id)addObject:anObject{
    return [self insertObject:anObject at:numElements];
}

- (id)insertObject:anObject at:(unsigned)index
{
    register id *this, *last, *prev;
    if (! anObject) return nil;
    if (index > numElements)
        return nil;
    if ((numElements + 1) > maxElements) {
    volatile id *tempDataPtr;
    /* we double the capacity, also a good size for malloc */
    maxElements += maxElements + 1;
    tempDataPtr = (id *) realloc (dataPtr, DATASIZE(maxElements));
    dataPtr = (id*)tempDataPtr;
    }
    this = dataPtr + numElements;
    prev = this - 1;
    last = dataPtr + index;
    while (this > last) 
    *this-- = *prev--;
    *last = anObject;
    numElements++;
    return self;
}

所以可变数组不是对元素操作的,而是对index和length的操作,当(numElements + 1) > maxElements会重新开辟新的空间。没有KVC相关方法流程的查找。所以调用addObject不会触发。

系统提供了调用方法:

    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

通过以上方法添加数组元素,KVO触发。

KVO的底层原理

根据官方文档的介绍可以了解到:

Automatic key-value observing is implemented using a technique called isa-swizzling.

自动键值观察的实现使用的技术叫做isa-swizzling

isa-swizzling

isa指针,是用来指向对象所属的包含一个派发表的类。这个表大体上有指向类的实现方法,还有其他的数据。
当一个观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
决不能依赖isa指针来确定类成员身份。相反,应该使用class方法来确定对象实例的类。

  • KVO使用isa-swizzling的验证
    image.png

    通过下断点发现isa的值已经不是LGPerson了,它变了🐳,变成了一个叫NSKVONotifying_LGPerson
    确实进行了isa指向的交换。

下面我们来进一步深入了解下具体的过程,分为以下几个部分:

  • NSKVONotifying_LGPerson是什么时候创建的?
  • NSKVONotifying_LGPerson和原来的类LGPerson有没有联系?
  • NSKVONotifying_LGPerson中都有什么内容?
  • NSKVONotifying_LGPerson什么时候销毁?
NSKVONotifying_LGPerson是什么时候创建的?

我们分别在addObserver的前后打印person对象的isa指向的类,以及通过API去获取NSKVONotifying_LGPerson类:

NSLog(@"addObserver之前:%s", object_getClassName(self.person));
    NSLog(@"addObserver之前:%s, %@", object_getClassName(self.person),objc_getClass("NSKVONotifying_LGPerson"));
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
    NSLog(@"addObserver之后:%s, %@", object_getClassName(self.person), objc_getClass("NSKVONotifying_LGPerson"));

打印结果如下:


由此可见在addObserver之后NSKVONotifying_LGPerson才被创建的,在这之前,这个类是为空的。

NSKVONotifying_LGPerson和原来的类LGPerson有没有联系

我们先来通过打印他的父类和子类来看看他和主类有没有关联。

image.png

通过打印可以了解到NSKVONotifying_LGPersonLGPerson的子类。

NSKVONotifying_LGPerson中都有什么内容

我们通过以下代码进行类NSKVONotifying_LGPerson的方法输出:

 unsigned int intCount;
    Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LGPerson"), &intCount);
    for (unsigned int intIndex=0; intIndex<intCount; intIndex++) {
        Method method = methodList[intIndex];
        NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method));
    }
image.png

可以看到 NSKVONotifying_LGPerson类重写了方法setNick:、class、dealloc、_isKVOA
我也分别打印了类NSKVONotifying_LGPerson的协议、属性还有成员变量,都是空的,所以这个类中主要含有方法。

NSKVONotifying_LGPerson什么时候销毁?

通过前面的探索我们了解到NSKVONotifying_LGPerson是在addObserve的时候动态创建的,那么会不会在关于KVO的api,在remove的时候销毁呢?我们来验证一下。

image.png

由此可见,self.person的isa指向在调用完remove之后指回了原来的类,但是此时获取NSKVONotifying_LGPerson还在,没有被销毁。

总结

  • 通过addObserve动态创建了一个子类NSKVONotifying_XXX
  • 这个子类重写了属性的set方法,还有系统的delloc,class,_isKVOA方法。
  • setter会调用父类原来的方法进行赋值,完成后进行回调通知。
  • 移除kvo时,属性的isa指向了原来的类,并且NSKVONotifying_XXX还存在没有被销毁。

遗留问题:KVO动态创建的子类不是在delloc中被销毁的,那么是在什么时候销毁的?

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

推荐阅读更多精彩内容