iOS中KVO使用及其原理

什么是KVO?

KVO(Key-value observing)是一種允許對象觀察(也可以叫監(jiān)聽)其他對象的指定屬性發(fā)生變化的機制。當被觀察對象的指定屬性發(fā)生變化時,它會向觀察者對象發(fā)送通知,告知變化的屬性及變化內(nèi)容,以便觀察者根據(jù)也無需求進行處理。KVO是建立在KVC的基礎之上的,所以要了解KVO,必須先了解KVC(點擊了解KVC的底層原理)。要了解更多內(nèi)容可以參考官方文檔。

KVO的使用

KVO的使用主要有三個部分組成,分別是注冊觀察者、接收觀察對象變化通知和移除觀察者。下面我們通過demo來演示KVO的使用及其過程。demo代碼結構如下:

@interface LNUser : NSObject<NSCopying>

@property (nonatomic, copy) NSString *nameAndAge;

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, strong) NSMutableArray <LNUser *> *friends;

+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age;

@end

@interface ViewController ()

@property (nonatomic, copy) LNUser *user;

@property (nonatomic) ThreeFloats threeFloats;

@end
注冊觀察者(Registering as an Observer)

KVO對象通過注冊一個觀察者(Observer)來負責觀察自己相關屬性變化的通知,通過如下方法來注冊觀察者:

/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
  • observer
    這里的observer就是觀察者,負責觀察對象的屬性變化,觀察者需要實現(xiàn)如下的回調(diào)方法來接收被觀察對象變化的通知。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
  • options
    這里的options是一個枚舉,可以選擇觀察屬性變化的新值還是舊值,也可以同時觀察兩個,其定義如下:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,
    NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
    NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
  • context
    這里的context顧名思義就是KVO觀察的上下文,用于標記每一次觀察的細節(jié), 觀察者在收到通知時可以根據(jù)context唯一確定是不是我們要觀察的對象和keyPath。它的作用比如說有多個對象、多個屬性時可以通過context快速區(qū)分,提高性能;而且有時候通過keyPath來判斷的話,有可能keyPath有重名的情況,比如父類和子類的屬性名稱一樣,這樣會對數(shù)據(jù)安全構成威脅,這時候就需要context唯一標記每一個觀察。context通常是一個靜態(tài)void *變量的指針,如demo中:
static void *kUserNameUpdateContext = &kUserNameUpdateContext;
static void *kUserNameAndAgeUpdateContext = &kUserNameAndAgeUpdateContext;
static void *kUserFriendsUpdateObserContext = &kUserFriendsUpdateObserContext;

通過KVO可以觀察對象的某個屬性,也可以觀察某個屬性的關聯(lián)屬性,也可以觀察集合屬性等。接下來我們展示幾種KVO常用方式。
1、觀察對象屬性
下面demo中self觀察self.user的name屬性,只要name屬性有變化,self就會接到通知:

    [self.user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kUserNameUpdateContext];

2、觀察多個相關的屬性
這里我們假設有一個屬性nameAndAge,它的值跟name和age都有關系,也就是只要name或者age的值變化nameAndAge也跟著變化,我們可以這樣做:先注冊self觀察nameAndAge,同時LNUser實現(xiàn)keyPathsForValuesAffectingValueForKey方法來設置name和age之間的關聯(lián),demo如下:

[self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];

在LNUser.m中實現(xiàn)keyPathsForValuesAffectingValueForKey方法,如下:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"nameAndAge"]) {
        NSArray *affectingKeys = @[@"name", @"age"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

當我們給對象注冊觀察者時,被觀察對象會調(diào)用keyPathsForValuesAffectingValueForKey方法,我們重寫這個方法來達到我們自定義處理的目的。

3、觀察數(shù)組屬性
觀察數(shù)組屬性具體不只是屬性的set方法,還能觀察數(shù)組的insert、remove等操作。前面我們說過KVO是建立在KVC的基礎之上的(KVC的原理),實際上這里觀察的數(shù)組就是KVC中設計到的insert、remove相關的方法,接下來我們以demo來演示:

    [self.user addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew context:kUserFriendsUpdateObserContext];
    self.user.friends = [[NSMutableArray alloc] init];
    LNUser *user = [LNUser userWithName:@"WW" age:18];
    LNUser *user2 = [LNUser userWithName:@"YY" age:19];
    [[self.user mutableArrayValueForKey:@"friends"] addObject:user];
    [[self.user mutableArrayValueForKey:@"friends"] insertObject:user2 atIndex:0];
    [[self.user mutableArrayValueForKey:@"friends"] removeObject:user];
    [[self.user mutableArrayValueForKey:@"friends"] removeObjectAtIndex:0];

demo中如果想觀察到friends的insert和remove操作,必須通過KVC的方式來訪問數(shù)組(mutableArrayValueForKey:方法)。這里還有一個要注意的地方,friends使用前要初始化,如果friends為空的話mutableArrayValueForKey: 會返回空導致崩潰。

接收觀察對象變化通知(Receiving Notification of a Change)

當被觀察對象對應的keyPath的value發(fā)生變化時,觀察者會收到如下觀察方法,觀察方法會把變化的keyPath,變化的內(nèi)容change和觀察的上下文context回傳回來,我們可以根據(jù)特定的context和keyPath自行處理。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (context == kUserNameUpdateContext) {
        if ([keyPath isEqual:@"name"]) {
        }
    }else if(context == kUserNameAndAgeUpdateContext){
        if ([keyPath isEqual:@"nameAndAge"]) {
        }
    }else if(context == kUserFriendsUpdateObserContext){
        if ([keyPath isEqual:@"friends"]) {
        }
    }
    NSLog(@"change:%@", change);
}
移除觀察者

在觀察者銷毀前移除觀察者,不然會出現(xiàn)crash的問題。通常我們會在觀察者dealloc中移除觀察者:

- (void)dealloc
{
    [self.user removeObserver:self forKeyPath:@"name" context:kUserNameUpdateContext];
    [self.user removeObserver:self forKeyPath:@"nameAndAge" context:kUserNameAndAgeUpdateContext];
    [self.user removeObserver:self forKeyPath:@"friends" context:kUserFriendsUpdateObserContext];
}

完整demo:

static void *kUserNameUpdateContext = &kUserNameUpdateContext;
static void *kUserNameAndAgeUpdateContext = &kUserNameAndAgeUpdateContext;
static void *kUserFriendsUpdateObserContext = &kUserFriendsUpdateObserContext;

@interface LNUser : NSObject<NSCopying>

@property (nonatomic, copy) NSString *nameAndAge;

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, strong) NSMutableArray <LNUser *> *friends;

+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age;

@end

@implementation LNUser

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.friends = [[NSMutableArray alloc] init];
    }
    return self;
}

+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age
{
    LNUser *user = [[LNUser alloc] init];
    user.name = name;
    user.age = age;
    return user;
}

- (NSString *)nameAndAge
{
    return [NSString stringWithFormat:@"%@:%@",_name,@(_age)];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"nameAndAge"]) {
        NSArray *affectingKeys = @[@"name", @"age"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

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

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqual:@"name"]) {
        return NO;
    }
    return YES;
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    
    return self;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"LNUser:{name:%@, age:%@}", _name, @(_age)];
}

@end


@interface ViewController ()

@property (nonatomic, copy) LNUser *user;

@property (nonatomic) ThreeFloats threeFloats;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.user = [LNUser userWithName:@"ZZ" age:19];
    [self.user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kUserNameUpdateContext];
    self.user.name = @"TTT";
    
    [self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
    self.user.name = [self.user.name stringByAppendingString:@"8"];
    self.user.age += 1;
    
    [self.user addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew context:kUserFriendsUpdateObserContext];
    LNUser *user = [LNUser userWithName:@"WW" age:18];
    LNUser *user2 = [LNUser userWithName:@"YY" age:19];
    [[self.user mutableArrayValueForKey:@"friends"] addObject:user];
    [[self.user mutableArrayValueForKey:@"friends"] insertObject:user2 atIndex:0];
    [[self.user mutableArrayValueForKey:@"friends"] removeObject:user];
    [[self.user mutableArrayValueForKey:@"friends"] removeObjectAtIndex:0];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (context == kUserNameUpdateContext) {
        if ([keyPath isEqual:@"name"]) {
        }
    }else if(context == kUserNameAndAgeUpdateContext){
        if ([keyPath isEqual:@"nameAndAge"]) {
        }
    }else if(context == kUserFriendsUpdateObserContext){
        if ([keyPath isEqual:@"friends"]) {
        }
    }
    NSLog(@"change:%@", change);
}

- (void)dealloc
{
    [self.user removeObserver:self forKeyPath:@"name" context:kUserNameUpdateContext];
    [self.user removeObserver:self forKeyPath:@"nameAndAge" context:kUserNameAndAgeUpdateContext];
    [self.user removeObserver:self forKeyPath:@"friends" context:kUserFriendsUpdateObserContext];
}

自動觀察和手動觀察

KVO觀察分為自動觀察和手動觀察。被觀察對象通過automaticallyNotifiesObserversForKey方法判斷是否是自動觀察。默認情況下返回YES,即是自動觀察,比如我們前面的代碼。那什么情況下是手動觀察呢?為了實現(xiàn)手動觀察我們得讓被觀察對象重寫automaticallyNotifiesObserversForKey:,這里可以指定某個Key為手動觀察,其他為自動:

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

但是光實現(xiàn)這個方法還不夠,我們還要手動調(diào)用willChangeValueForKey和didChangeValueForKey方法以通知觀察者屬性的變化。比如這里我們重寫setName:方法:

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

備注:這里willChangeValueForKey和didChangeValueForKey必須成對出現(xiàn)才有效。

KVO的底層原理

KVO自動觀察是通過isa-swizzling技術實現(xiàn)的。isa-swizzling大概意思就是isa指針交換。isa指針是指向類的指針,它的底層結構比較復雜(點擊了解更多關于isa指針的信息)。當一個對象注冊一個觀察者觀察它的屬性時,它的isa指針就會被修改,這時的指針已經(jīng)不是指向它原來的類,而是一個中間類,這個中間類是在KVO注冊觀察者時創(chuàng)建的,它是以對象原有的類為父類,新建一個子類,用于處理KVO。這樣做的目的是為了不改變原來的類結構,因為要觀察對象的屬性變化,就必須要去修改類的結構,因為屬性列表等信息是存儲在類結構中的(點擊了解更多類結構相關的信息)。但是如果是手動觀察,就不會改變isa指針。接下來我們通過上面的代碼運行調(diào)試,以驗證結論。

驗證isa指針變化及中間類的存在

以下通過讀取注冊觀察前后self.user的類的變化來判斷isa指針變化。

    NSLog(@"注冊手動觀察前:%@", object_getClass(self.user));
    [self.user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kUserNameUpdateContext];
    NSLog(@"注冊手動觀察后:%@", object_getClass(self.user));

    Class startClass = object_getClass(self.user);
    NSLog(@"注冊自動觀察前:%@", startClass);
    
    [self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
    Class endCls = object_getClass(self.user);
    NSLog(@"注冊自動觀察后:%@", endCls);

打印結果:

2021-08-17 09:58:37.713304+0800 KVC&KVODemo[63178:16132888] 注冊手動觀察前:LNUser
2021-08-17 09:58:37.713608+0800 KVC&KVODemo[63178:16132888] 注冊手動觀察后:LNUser
2021-08-17 09:58:37.713752+0800 KVC&KVODemo[63178:16132888] 注冊自動觀察前:LNUser
2021-08-17 09:58:37.714362+0800 KVC&KVODemo[63178:16132888] 注冊自動觀察后:NSKVONotifying_LNUser

這其中name屬性是手動通知,所以self.user對象LNUser在注冊觀察前后它的類沒有變化,但是nameAndAge屬性是自動觀察的,所以注冊觀察者之后類發(fā)生了改變,由原來的LNUser變成了NSKVONotifying_LNUser,也就是isa指針發(fā)生了改變。

備注:著這里要特別注意的是,獲取當前對象的類不能通過class方法獲取,比如[self.user class],而是要通過object_getClass或者object_getClassName函數(shù)獲取,因為這兩個函數(shù)都是直接訪問的isa指針,而class方法有可能會被重寫(后面分析KVO對象的結構會有解釋)。以下是object_getClass方法和object_getClassName方法實現(xiàn):
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
const char *object_getClassName(id obj)
{
return class_getName(obj ? obj->getIsa() : nil);
}
所以后面demo我們都是用object_getClass函數(shù)來獲取對應的類class的。

這個isa指針是動態(tài)變化的,當對象的觀察者移除完畢之后就會,isa又改回為指向原來的類,以下是demo演示:

- (void)dealloc
{
    NSLog(@"移除觀察者前:%@", object_getClass(self.user));
    [self.user removeObserver:self forKeyPath:@"name" context:kUserNameUpdateContext];
    [self.user removeObserver:self forKeyPath:@"nameAndAge" context:kUserNameAndAgeUpdateContext];
    NSLog(@"移除部分觀察者后:%@", object_getClass(self.user));
    [self.user removeObserver:self forKeyPath:@"friends" context:kUserFriendsUpdateObserContext];
    NSLog(@"移除所有觀察者后:%@", object_getClass(self.user));
}

打印結果:

2021-08-17 10:35:39.776155+0800 KVC&KVODemo[63872:16170818] 移除觀察者前:NSKVONotifying_LNUser
2021-08-17 10:35:42.325530+0800 KVC&KVODemo[63872:16170818] 移除部分觀察者后:NSKVONotifying_LNUser
2021-08-17 10:35:45.085174+0800 KVC&KVODemo[63872:16170818] 移除所有觀察者后:LNUser

中間類會緩存

備注:這個中間類NSKVONotifying_LNUser創(chuàng)建之后會被緩存,即使我們把所有觀察都移除了,被觀察對象的isa已改回為指向初始的類,但是中間類會被緩存下來,下次程序再次注冊觀察者的時候會判斷有沒有中間類緩存,有緩存就是用,沒有才會新建一個中間類。驗證過程如下:

驗證緩存.jpg

demo中第一次注冊的觀察者已經(jīng)移除,對象的類由LNUser變成了NSKVONotifying_LNUser,cls1表示第一次注冊時生成的NSKVONotifying_LNUser,然后第二次注冊時cls3也是一個NSKVONotifying_LNUser類,我們通過x/4gx讀取cls1和cls3的內(nèi)存,發(fā)現(xiàn)他們是完全一致的,cls3復用了cls1,證明了緩存的存在。

驗證中間類是LNUser的子類

觀察注冊自動觀察前后的self.user的class和superClass,看看對應的類和父類:

    Class startClass = object_getClass(self.user);
    NSLog(@"注冊自動觀察前:%@", startClass);
    NSLog(@"注冊自動觀察前superClass:%@", [startClass superclass]);
    
    [self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
    Class endCls = object_getClass(self.user);
    NSLog(@"注冊自動觀察后:%@", endCls);
    NSLog(@"注冊自動觀察后superClass:%@", [endCls superclass]);

打印結果:

2021-08-17 10:55:36.264796+0800 KVC&KVODemo[64316:16193280] 注冊自動觀察前:LNUser
2021-08-17 10:55:38.071268+0800 KVC&KVODemo[64316:16193280] 注冊自動觀察前superClass:NSObject
2021-08-17 10:55:38.071953+0800 KVC&KVODemo[64316:16193280] 注冊自動觀察后:NSKVONotifying_LNUser
2021-08-17 10:55:38.072126+0800 KVC&KVODemo[64316:16193280] 注冊自動觀察后superClass:LNUser

注冊觀察者前,對象的isa指向LNUser,父類是NSObject;注冊觀察者后,對象的isa變成指向NSKVONotifying_LNUser類,父類是LNUser。證明了中間類是LNUser的子類。

驗證中間類和LNUser結構上的區(qū)別

經(jīng)過比較,NSKVONotifying_LNUser相對父類出現(xiàn)的變化主要在方法列表上,屬性和成員變量并沒有變化。下面專門針對方法列表作對比分析:

    NSLog(@"開始打印LNUser的方法列表:");
    [self printClassAllMethod:startClass];

    [self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];

    Class endCls = object_getClass(self.user);
    NSLog(@"開始打印NSKVONotifying_LNUser的方法列表:");
    [self printClassAllMethod:endCls];
    

打印結果:

2021-08-17 11:19:12.992325+0800 KVC&KVODemo[64972:16228215] 開始打印LNUser的方法列表:
2021-08-17 11:19:12.992521+0800 KVC&KVODemo[64972:16228215] nameAndAge-0x10f13b220
2021-08-17 11:19:12.992651+0800 KVC&KVODemo[64972:16228215] setNameAndAge:-0x10f13b5b0
2021-08-17 11:19:12.992761+0800 KVC&KVODemo[64972:16228215] init-0x10f13b0a0
2021-08-17 11:19:12.992856+0800 KVC&KVODemo[64972:16228215] copyWithZone:-0x10f13b590
2021-08-17 11:19:12.992953+0800 KVC&KVODemo[64972:16228215] name-0x10f13b5f0
2021-08-17 11:19:12.993161+0800 KVC&KVODemo[64972:16228215] .cxx_destruct-0x10f13b6c0
2021-08-17 11:19:12.993527+0800 KVC&KVODemo[64972:16228215] setName:-0x10f13b2d0
2021-08-17 11:19:12.993831+0800 KVC&KVODemo[64972:16228215] setAge:-0x10f13b640
2021-08-17 11:19:12.994116+0800 KVC&KVODemo[64972:16228215] age-0x10f13b620
2021-08-17 11:19:12.994381+0800 KVC&KVODemo[64972:16228215] friends-0x10f13b660
2021-08-17 11:19:12.994664+0800 KVC&KVODemo[64972:16228215] setFriends:-0x10f13b680
2021-08-17 11:19:12.994936+0800

KVC&KVODemo[64972:16228215] 開始打印NSKVONotifying_LNUser的方法列表:
2021-08-17 11:20:35.099860+0800 KVC&KVODemo[64972:16228215] setAge:-0x7fff207bfc81
2021-08-17 11:20:35.099980+0800 KVC&KVODemo[64972:16228215] setNameAndAge:-0x7fff207bf03f
2021-08-17 11:20:35.100102+0800 KVC&KVODemo[64972:16228215] class-0x7fff207bdb49
2021-08-17 11:20:35.100204+0800 KVC&KVODemo[64972:16228215] dealloc-0x7fff207bd8f7
2021-08-17 11:20:35.100305+0800 KVC&KVODemo[64972:16228215] _isKVOA-0x7fff207bd8ef
2021-08-17 11:20:35.100407+0800

根據(jù)打印結果,NSKVONotifying_LNUser相對于LNUser類多了一個_isKVOA方法,用于判斷是否是KVO對象;另外還重寫了幾個方法setAge:、setNameAndAge:、class和dealloc。這其中setAge:和setNameAndAge是分別對應被觀察的屬性name和nameAndAge的set方法,重寫這兩個方法是為了在發(fā)生set操作時能給觀察者發(fā)送通知;而重寫class方法則是為了返回初始的類LNUser,避免返回中間類型NSKVONotifying_LNUser,這那樣做的目的是為了不讓開發(fā)者感知到當前對象的類的變化,因為這是一個中間類,開發(fā)者并不知道具體類名,如果開發(fā)者之前使用了class方法來獲取類來判斷是不是LNUser等操作,就可能會有問題,因此才會重寫, 所以我們前面在獲取觀察類的變化時沒有用class方法,而是用runtime的object_getClass函數(shù);而重寫dealloc則是為了在被觀察對象銷毀的時候作自己相應的處理。

觀察者如何接受到通知

有上面的比較結果可知,屬性變化通知主要應該是在set方法發(fā)起的。我們之前在做手動觀察demo時其實也是在重寫setName方法,然后手動調(diào)用相關方法進行通知觀察者:

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

但是自動觀察是不是這樣的呢?由于蘋果不開放KVO源碼,我們沒知道知道具體實現(xiàn),但是我們可以利用斷點調(diào)試查看大致流程。前面我們知道nameAndAge是被自定觀察的屬性,因此我們在nameAndAge的set方法打個斷點,看看KVO是如何給觀察者發(fā)送通知的:

- (void)setNameAndAge:(NSString *)nameAndAge
{
    _nameAndAge = nameAndAge;
}

在這個方法中打個斷點,然后進入?yún)R編調(diào)試模式:

set方法斷點.jpeg

通過斷點可知,在調(diào)用set方法前會先調(diào)用[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]方法,我們進入這個方法的匯編代碼,滑到斷點的位置,查看調(diào)用set方法前后KVO都做了那些操作。通過Xcode -> Debug -> Debug Workflow -> Always show disassambly 開啟匯編調(diào)試模式:

進入?yún)R編調(diào)試.png

定位到斷點的位置,查看set前后的代碼流程:

KVO流程.png

雖然沒有看到手動觀察時顯式調(diào)用的方法willChangeValueForKey和didChangeValueForKey,但是我們看到類似的操作NSKeyValueWillChange和NSKeyValueDidChangeBySetting,原理上講應該都是一樣的。在set前后調(diào)用相關方法以達到通知觀察者相關Key的value變化的目的。
上面的調(diào)試有一點需要注意的是,setNameAndAge方法調(diào)用進入的是LNUser的setNameAndAge方法,而不是我們之前我們分析的中間類NSKVONotifying_LNUser里面的,這是為什么呢?我們前面不是說了NSKVONotifying_LNUser會重寫相應的被被觀察屬性的set方法嗎?

重載父類的set方法

根據(jù)流程推測,應該是KVO子類重寫的set方法里重載了父類的set方法。上面的setNameAndAge:最終是通過父類LNUser的實現(xiàn)來賦值的,這里面NSKVONotifying_LNUser雖然重寫了set方法,但是他還是會重載父類的setNameAndAge:,只是在調(diào)用父類方法前后分別加了willChange和didChange相關的操作。這其實也很好理解,畢竟父類的set方法實現(xiàn)也比較完整,包括內(nèi)存管理操作都已經(jīng)實現(xiàn)了,沒必要全部重寫,重載父類方法只是進行必要的復用而已。

總結

本文主要參考蘋果官方文檔的介紹,并根據(jù)自己的理解進行分析。如果需要了解KVO更加詳細的內(nèi)容,可以參考官方文檔。同時網(wǎng)上也有很多自定義KVO Demo,關于自定義KVO日后有空再說。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • #pragma clang diagnostic push #pragma clang diagnostic ig...
    戀空K閱讀 327評論 0 1
  • KVC KVC定義 KVC(Key-value coding)鍵值編碼,就是指iOS的開發(fā)中,可以允許開發(fā)者通過K...
    暮年古稀ZC閱讀 2,293評論 2 9
  • KVC定義 KVC(Key-value coding)鍵值編碼,就是指iOS的開發(fā)中,可以允許開發(fā)者通過Key名直...
    SheIsMySin_72e7閱讀 423評論 0 0
  • KVC KVC定義 KVC(Key-value coding)鍵值編碼,就是指iOS的開發(fā)中,可以允許開發(fā)者通過K...
    jackyshan閱讀 52,318評論 9 198
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 7,974評論 0 4

友情鏈接更多精彩內(nèi)容