iOS-KVO原理

初探

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO, 即Key-value observing,也就是我们常说的键值观察,它是是一种机制,允许将其他对象的指定属性的更改通知给指定对象。

KVO机制对应用程序中模型层和控制器层之间的通信特别有用,控制器对象通常观察模型对象的属性,而视图对象通过控制器观察模型对象的属性,模型对象可能会观察其他模型对象(通常是确定从属值何时更改),甚至是自身(再次确定从属值何时更改)。

比如说,我们的视图控制器viewController上有一个label用来显示模型对象person的一个属性name,我们需要的实时显示,当name发生变化的时候,label的显示也会及时变化,那么我们就可以给person添加一个观察者viewController,用以观察name属性,当name发生变化的时候,viewController就可以及时刷新label,这样就做到了模型层和控制器层的通信。

KVO的使用

使用步骤

  • 使用addObserver:forKeyPath:options:context:方法将观察者注册到观察对象
  • observeValueForKeyPath:ofObject:change:context:在观察者内部实现以接受更改通知消息
  • 当观察者removeObserver:forKeyPath:不再应该接收消息时,使用该方法注销观察者。该方法至少在从内存释放观察者之前调用。

下面我们配合一个例子进行探究:

@interface TPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSMutableArray *hobbies;
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

@end

@implementation TPerson

@end

然后在ViewController里实现如下代码:

@interface ViewController ()

@property (nonatomic, strong) TPerson *person;
@property (nonatomic, assign) int pNumber;

@end

1. 注册键值观察

被观察对象首先通过调用下面方法向观察者注册自己,并且传入要观察的属性的keyPath,另外还指定了一个options参数和一个上下文指针context来管理通知的各个方面。

- (void)addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

该方法的参数如下:

  • (NSObject *)observer: 观察者
  • (NSString *)keyPath: 要观察的属性
  • (NSKeyValueObservingOptions)options: 设置回调的内容和时机
  • (nullable void *)context: 上下文指针用以区分被观察者和观察属性

我们再来看看options的取值有什么含义:

  • NSKeyValueObservingOptionNew = 0x01: 变化之后的新值
  • NSKeyValueObservingOptionOld = 0x02: 变化之前的旧值
  • NSKeyValueObservingOptionInitial = 0x04: 变化之前通知
  • NSKeyValueObservingOptionPrior = 0x08: 变化之后通知

注意,options可以填写多个。

我们可以使用context指针用来区分每一个监听,这样在通知回调的时候可以直接根据context进行区分处理,这样更简洁一些。使用方式如下:

static void *PersonNickNameContext = &PersonNickNameContext;

当不使用context指针的时候,需要传入NULL,而不能传入nil,因为不是id类型。

2. 接收变化回调

观察者必须实现下面方法,才能接收到观察对象的变化。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object 
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change 
                       context:(nullable void *)context;
  • (nullable NSString *)keyPath: 观察的属性
  • (nullable id)object: 观察对象
  • (nullable NSDictionary<NSKeyValueChangeKey, id> *)change: 返回的观察信息
  • (nullable void *)context: 上下文指针
  • NSKeyValueChangeKindKey: 返回的更改信息的时机,1是变化之前,2是变化之后
  • NSKeyValueChangeNewKey: 返回的值是新值
  • NSKeyValueChangeOldKey: 返回值是旧值
  • NSKeyValueChangeIndexesKey: 观察的属性是数组或者NSSet的下标变化
  • NSKeyValueChangeNotificationIsPriorKey: 观察通知的时机是否是Prior
  • NSKeyValueChangeSetting = 1: 数据的设置
  • NSKeyValueChangeInsertion = 2: 集合的插入
  • NSKeyValueChangeRemoval = 3: 集合的移除
  • NSKeyValueChangeReplacement = 4: 集合的替换

当被观察对象是集合对象,在NSKeyValueChangeKindKey字段中会包含NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement的信息,表示集合对象的操作方式。

3. 移除观察

当我们不需要观察者继续观察对象了,就需要移除观察者。在观察者被释放之前必须移除观察者,否则会出现BUG。移除之前必须注册,否则就会崩溃。

一般,我们在观察者初始化期间(例如在init中或viewDidLoad中)注册为观察者,在释放过程中(通常在中dealloc)注销,确保在观察者从内存中释放之前将其注销。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

接着上面的例子,我们在viewDidLoad进行注册KVO,回调通知的策略给name属性使用改变之前的,hobbies使用改变之后的。:

static void *PersonNameContext = &PersonNameContext;
static void *PersonHobbyContext = &PersonHobbyContext;

- (void)viewDidLoad {
    self.person = [[TPerson alloc] init];
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial context:PersonNameContext];
    
    self.person.hobbies = [NSMutableArray array];
    [self.person addObserver:self forKeyPath:@"hobbies" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:PersonHobbyContext];
    
    [self.person addObserver:self forKeyPath:@"downloadProgress" options: NSKeyValueObservingOptionNew context:NULL];
}

监听回调:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonNameContext) {
        NSLog(@"===name===change==%@=====", change);
    } else if (context == PersonHobbyContext) {
        NSLog(@"===hobbies===change==%@=====", change);
    } else {
        NSLog(@"===downloadProgress===change==%@=====", change);
    }
}

然后在下面方法中改变观察的属性的值:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.pNumber++;
    self.person.name = [NSString stringWithFormat:@"%@-%d-%@", @"Number", self.pNumber, @"Person"];
    
    switch (self.pNumber % 4) {
        case 1:
        case 3:
        {
            [[self.person mutableArrayValueForKeyPath:@"hobbies"] addObject:@"add"];
        }
            break;
        case 2:
        {
            [[self.person mutableArrayValueForKeyPath:@"hobbies"] removeObject:@"add"];
        }
            break;
            
        case 0:
        {
            [[self.person mutableArrayValueForKeyPath:@"hobbies"] replaceObjectAtIndex:0 withObject:@"replace"];
        }
            break;
        default:
            break;
    }
    
    NSLog(@"===%@===", self.person.hobbies);
    
    self.person.writtenData += 10;
    self.person.totalData += 20;
}

然后在dealloc方法中移除观察:

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

运行程序,控制台输出:

===name===change=={
    kind = 1;
    new = "<null>";
}=====

因为name属性的监听策略使用了NSKeyValueObservingOptionInitial,所以进入页面就会响应。点击一下页面:

===name===change=={
    kind = 1; // NSKeyValueChangeSetting
    new = "Number-1-Person"; // NSKeyValueObservingOptionNew
    old = "<null>"; // NSKeyValueObservingOptionOld
}=====
// NSKeyValueObservingOptionPrior
===hobbies===change=={
    indexes = "<_NSCachedIndexSet: 0x600002949ac0>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; // NSKeyValueChangeIndexesKey
    kind = 2; // NSKeyValueChangeInsertion
    notificationIsPrior = 1; // NSKeyValueObservingOptionPrior
}=====
===hobbies===change=={
    indexes = "<_NSCachedIndexSet: 0x600002949ac0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        add
    );
}=====

继续点击页面,控制台会输出hobbiesremoveObjectreplaceObjectAtIndex的监听,kind也会有相应的变化,此处就不一一演示了。

注意,观察的属性是数组的时候不能直接是add等方法:

[self.person.hobbies addObject:@"addd"]

由于KVO是基于KVC,而直接使用add等方法是不会触发KVC的,所以我们要使用

[[self.person mutableArrayValueForKeyPath:@"hobbies"] addObject:@"add"]

符合KVO的使用标准

当我们要使用KVO观察某个对象的时候,必须得确保该对象符合KVO的使用标准才可以。

  • 该类和属性必须要符合KVC,因为KVO的实现依托于KVCKVO支持的数据类型与KVC相同,包括Objective-C对象以及基本数据类型和结构体。
  • 该类能为该属性发出KVO更改的通知。
  • 当有依赖关系的时候,注册合适的依赖key

发出KVO通知

发出KVO通知有两种方式,一种是自动通知,另外一种是手动通知。

自动通知

自动通知需要该类和属性必须要符合KVC,使用KVC的方式对属性进行赋值。如下面方法:

- (void)setValue:(nullable id)value forKey:(NSString *)key

手动通知

在某些情况下,我们希望控制通知过程。例如,最大程度地减少出于应用程序特定原因而不必要的触发通知,或将多个更改分组为一个通知。这是就可以使用手动更改通知。

手动和自动通知不是互斥的。当我们使用手动通知的时候,第一步需要重写automaticallyNotifiesObserversForKey:,并且返回NO;第二步在在更改值之前调用willChangeValueForKey:、在更改值之后调用didChangeValueForKey:。当一个类里既存在需要手动通知的属性,又存在需要自动通知的属性,在这个方法里就需要进行判断,分别处理。

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

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

如果操作的属性是集合类型,需要使用以下方法:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

注册合适的依赖key

在许多情况下,一个属性的值取决于另一个或多个其他属性的值。如果一个属性的值发生更改,则派生属性的值也应标记为更改。我们需要确保当一个附属的属性发生变化时,其父属性也能收到改变通知。

一对一关系

要自动触发一对一关系的通知,需要重写 keyPathsForValuesAffectingValueForKey:或实现遵循其定义的用于注册从属key的式的方法。

  1. 使用keyPathsForValuesAffectingValueForKey方法
+ (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];
}

运行程序,控制台输出:

===downloadProgress===change=={
    kind = 1;
    new = "0.100000";
}=====
===downloadProgress===change=={
    kind = 1;
    new = "0.083333";
}=====
  1. 使用keyPathsForValuesAffectingDownloadProgress代替keyPathsForValuesAffectingValueForKey方法也可以达到同样的效果。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress {
    return [NSSet setWithObjects:@"totalData", @"writtenData", nil];
}

一对多关系

假设有一个Department对象,Department又包含多个employees,所以该对象与Employee形成了一对多关系,而Employee具有薪金属性。当Department对象具有一个totalSalary属性,该属性取决于所有雇员的薪水,可以在observeValueForKeyPath进行处理。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath: @"employees.@ sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
    if (totalSalary!= newTotalSalary) {
        [self willChangeValueForKey:@“totalSalary”];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@“totalSalary”];
    }
}
 
-(NSNumber *)totalSalary {
    return _totalSalary;
}

KVO原理

看完KVO的使用,我们来看一下KVO的实现原理,官方文档这么说:

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

KVO是使用isa-swizzling技术实现的。这个isa指针指向对象的类,它维护一个派发表。该派发表实质上包含该类实现的方法的指针以及其他数据。

在为对象的属性注册观察者时,将修改观察对象的isa指针,让其指向一个中间类而不是原来的类。表现出来的现象就是,isa指针的值不一定指向正确类,因为它可能指向一个中间类。所以不要依靠isa指针来确定类,相反,应该使用类方法确定实例对象的类。

我们可以使用上述demo来验证一下,在注册观察者的前后设置log,运行程序:

image

从控制台我们可以看出,此时生成了一个中间类NSKVONotifying_TPerson

当观察一个对象A时,KVO机制动态创建一个对象A当前类的中间类,其类名为NSKVONotifying_A,该类继承自对象A的本类,并为这个新的类重写了被观察属性keyPathsetter方法。setter方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。

被观察对象Aisa指针从指向原来的A类,被KVO机制修改为指向NSKVONotifying_A类,来实现当前类属性值改变的监听。

需要注意的是,此处生成的是一个类,而不是对象,修改的是原对象的isa指向,让其指向中间类

下面我们来验证一下NSKVONotifying_AA类的关系到底是不是继承?我们在注册前后使用下面打印一下TPerson类及其子类的名称:

- (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);
}

控制台输出:

image

可以看出,生成的中间类是被观察对象的类的子类

接着我们给TPerson添加一个实例变量,看看属性和实例变量由于setter方法的差异会不会有不同的结果。

@public
NSString *ivarName;

touchesBegan事件,给ivarName赋值:

self.person->ivarName = @"ivarName";

运行程序,控制台输出:

==change=={
    kind = 1;
    new = "Number-1-Person";
    old = "<null>";
}==

可以发现,并没有ivarName的改变通知,这也说明了ivarName没有setter方法,所以并不能发出改变通知。

同样,我们可以在注册前后打印类的方法来分析前后的差异:

- (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(@"--sel--%@--imp--%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

- (void)viewDidLoad {
    [self printClassAllMethod:[TPerson class]];
    NSLog(@"--注册之前--");
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial context:PersonNameContext];
    NSLog(@"--注册之后--");
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_TPerson")];
}

由于注册之后就生成了中间类NSKVONotifying_TPerson,所以注册后我们直接打印中间类的方法。控制台输出结果如下:

 --sel--setHobbies:--imp--0x10f10ef70
 --sel--hobbies--imp--0x10f10ef50
 --sel--writtenData--imp--0x10f10eff0
 --sel--setWrittenData:--imp--0x10f10f010
 --sel--totalData--imp--0x10f10f040
 --sel--setTotalData:--imp--0x10f10f060
 --sel--.cxx_destruct--imp--0x10f10f090
 --sel--name--imp--0x10f10ef20
 --sel--setName:--imp--0x10f10eb90
 --sel--downloadProgress--imp--0x10f10ee10
 --sel--setDownloadProgress:--imp--0x10f10efb0
 --注册之前--
 --注册之后--
 --sel--setName:--imp--0x7fff25721c7a
 --sel--class--imp--0x7fff2572073d
 --sel--dealloc--imp--0x7fff257204a2
 --sel--_isKVOA--imp--0x7fff2572049a

从以上结果可以看出,在注册之前,属性name和实例变量ivarName的区别就是没有setter/getter。另外,在注册之后,又打印出来了setName方法,NSKVONotifying_TPerson作为TPerson的子类,如果还有setName方法的话,说明NSKVONotifying_TPerson,重写了父类的setName的方法,也就是中间类重写了原来类的setter

既然中间类NSKVONotifying_TPerson重写了dealloc方法,那么它肯定是在该方法里做了某些处理,我们在观察者的dealloc方法里,移除观察者的之后,加入以下方法:

NSLog(@"--移除之后--%s--", object_getClassName(self.person));

运行程序,控制台输出:

--移除之后--TPerson--

可以看出,移除观察者之后,被观察对象的isa指针会指回到原来的A类。

总结

KVO是基于KVC的一种机制,通过观察回调,可用于不同层面的通信。其使用步骤如下:

    1. 注册观察者
    1. 接受更改回调
    1. 移除观察者

KVO原理

  1. 动态生成了一个中间类,该类是被观察对象的类的子类,类名是NSKVONotifying_A,被观察对象Aisa指针从指向原来的A类,被KVO机制修改为指向NSKVONotifying_A
  2. 观察的是中间类NSKVONotifying_A重写的setter方法
  3. 中间类NSKVONotifying_A重写了很多方法:
    • setter: 发送改变通知
    • class: 返回被观察对象的类
    • dealloc: 释放相关方法
    • _isKVOA: 标志是生成的中间类
  4. 移除观察的之后, 被观察对象的isa指针会指回到原来的A
  5. 移除观察之后中间类NSKVONotifying_A不会销毁

参考文献: Aplle官方文档

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

推荐阅读更多精彩内容

  • kvo全称Key-Value-Observing,俗称“键值监听”,可以用于对对象的某个属性值的监听。kvo是Ob...
    Devbrave阅读 434评论 0 0
  • 导语: KVO全称Key Value Observing,直译为键值观察。KVO 作为 iOS 中一种强大并且有效...
    xianminxiao阅读 1,111评论 0 2
  • 1、KVO的基本使用 定义:KVO的全称是Key-Value-Observing,俗称“键值监听”,可以用于监听某...
    Jerky_Guo阅读 1,725评论 0 5
  • KVO是iOS开发中常用的不同类之间通信的技术,叫做键值观察,跟通知NSNotifacation一样是,可以一对多...
    huxinwen阅读 1,233评论 0 2
  • 感觉很久都没有写新文章了,还是写一些小文章吧,记录一下也好!这一次和大家讲解的是KVO -- key value ...
    chouson_chan阅读 356评论 0 1