如果說書籍是人類進(jìn)步的階梯,那么優(yōu)秀的開源代碼就是程序員提升的橋梁。研讀源碼可以學(xué)習(xí)其中的框架和模式, 代碼技巧, 算法等,然后不斷總結(jié)運(yùn)用,最終這些會變成自己的東西,編程水平自然也提高了。
FBKVOController是Facebook開源的接口設(shè)計(jì)優(yōu)雅的KVO框架。筆者研讀之后確實(shí)受益匪淺,本著學(xué)以致用的原則,筆者借鑒其接口設(shè)計(jì)的方式實(shí)現(xiàn)了一套完整的小紅點(diǎn)(推送消息)解決方案RJBadgeKit, 有興趣的同學(xué)可以參考一下。
鑒于目前已經(jīng)有很多關(guān)于FBKVOController源碼分析的博文,本文會嘗試從另外一個角度,以提煉和分析具體知識點(diǎn)的方式來總結(jié)FBKVOController中我們可以借鑒和學(xué)習(xí)的地方。
宏定義
通常在添加觀察者的時候都需要指定一個觀察路徑(keyPath), 這個路徑是直接以字符串的方式提供的,比如我們有個類RJPhoto的對象photo, 需要觀察它的name路徑:
[self.KVOController observe:photo keyPath:@"name"];
如果字符串拼寫錯誤,或者被observe的對象沒有name這個屬性,編譯器并不會報(bào)錯,只有等到運(yùn)行時才會發(fā)現(xiàn)問題。我們來看下FBKVOController是怎么通過宏定義來解決這個問題的:
#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))
#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))
有了這兩個宏,被觀察者的keyPath可以通過宏傳入,其好處在于該宏會進(jìn)行編譯檢查和代碼提示,如果keyPath不存在或者拼寫錯誤,會提示錯誤。
[self.KVOController observe:photo keyPath:FBKVOKeyPath(photo.name)];
[self.KVOController observe:photo keyPath:FBKVOClassKeyPath(RJPhoto, name)];
上面的宏是怎么做到編譯檢查和代碼提示的呢?我們先分析第二個相對比較復(fù)雜的宏FBKVOClassKeyPath, 其整體是一個C語言的逗號表達(dá)式,逗號表達(dá)式的格式: e.g. int a = (b, c);逗號表達(dá)式取后面的值,故而a將被賦值成c, 此時b在賦值運(yùn)算中就被忽略了,沒有被使用,所以編譯器會給出警告,為了消除這個warning我們需要在b前面加上(void)做個類型強(qiáng)轉(zhuǎn)操作。
逗號表達(dá)式的前項(xiàng)和NO進(jìn)行了與操作,這個主要是為了讓編譯器忽略第一個值,因?yàn)槲覀冋嬲x值的是表達(dá)式后面的值。預(yù)編譯的時候看見了NO, 就會很快的跳過判斷條件。我猜你看到這兒肯定會奇怪了,既然要忽略,那為啥還要用個逗號表達(dá)式呢,直接賦值不就好了?
這里主要是對傳入的第一個參數(shù)CLASS的對象(CLASS *)(nil)和第二個正要輸入的KEYPATH做了.操作,這也正是為什么輸入第二個參數(shù)時編輯器會給出正確的代碼提示(只要是作為表達(dá)式的一部分, Xcode自動會提示)。如果傳入的KEYPATH不是CLASS對象的屬性,那么(CLASS *)(nil).KEYPATH就不是一個合法的表達(dá)式,所以自然編譯就不會通過了。
宏FBKVOKeyPath接受一個參數(shù),前半段和上面是一樣的,不同的是逗號表達(dá)式的后一段strchr(# photo.name, '.') + 1, 函數(shù)strchar是C語言中的函數(shù),用來查找某字符在字符串中首次出現(xiàn)的位置,這里用來在photo.name(注意前面加了#字符串化)中查找.出現(xiàn)的位置,再加上1就是返回.后面keyPath的地址了。也就是strchr('photo.name', '.')返回的是一個C字符串,這個字符串從找到'photo.name'中為'.'的字符開始往后,即'name'.
這邊還用到了斷言宏
NSCAssert(x, y),x為BOOL值,y為字符串類型, 當(dāng)x為NO時產(chǎn)生斷言退出并打印y字符串內(nèi)容. 需要注意的是NSCAssert在C語言函數(shù)下使用, 而NSAssert則是Objective-C函數(shù)下使用
關(guān)于宏定義的詳細(xì)解釋以及上面所述的類似宏定義的應(yīng)用和分析,可以參考筆者的博文Hello, 宏定義魔法世界。
自釋放
FBKVOController通過自釋放的機(jī)制來實(shí)現(xiàn)observer的自動移除,具體來說就是給observer添加一個FBKVOController的成員變量,比如:
#import "RJViewController.h"
#import "KVOController.h"
@interface RJViewController ()
@property (nonatomic, strong) FBKVOController *kvoController;
@end
@implementation RJViewController
- (instancetype)init
{
self = [super init];
if (nil != self) {
_kvoController = [FBKVOController controllerWithObserver:self];
}
return self;
}
觀察者RJViewController定義了一個FBKVOController的成員變量kvoController, 當(dāng)RJViewController釋放后,其成員變量kvoController也會相應(yīng)釋放,F(xiàn)BKVO自動移除觀察者的trick就是在FBKVOController的dealloc里面做remove observer的操作。
初始化
我們先來看看FBKVOController是如何提供初始化接口的:
+ (instancetype)controllerWithObserver:(nullable id)observer;
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObserver:(nullable id)observer;
一共有3個初始化函數(shù),其中第一個和第三個為便利初始化函數(shù)(convenience initializer), 第二個添加了NS_DESIGNATED_INITIALIZER宏,為指定初始化函數(shù)(designated initializer).
初始化接口的規(guī)則:
a) 指定初始化方法必須調(diào)用父類的指定初始化方法
b) 便利初始化方法必須調(diào)用其他的初始化方法,直到最后指向指定初始化方法
c) 具有指定初始化方法的子類必須實(shí)現(xiàn)所有父類的指定初始化方法
理解了自釋放的原理,初始化的規(guī)則就很明顯了,F(xiàn)BKVOController必須作為observer的成員變量存在。那如果使用方忽視了這個規(guī)則或者不想如此繁瑣,有沒有更簡單的方式呢?有!我們來看下FBKVO是怎么提供最簡化convenience initializer的:
@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
FBKVOController創(chuàng)建了NSObject的Category, 通過AssociateObject給NSObject提供一個Retain和nonRetain的KVOController(這里實(shí)際也是在成員變量KVOController的Get函數(shù)里面調(diào)用了+ controllerWithObserver方法)。所以任意observer都可以直接調(diào)用observer.KVOController來動態(tài)生成一個FBKVOController對象,非常方便!
到這兒看起來初始化接口已經(jīng)很完整了,但好像還有個問題,萬一使用方不按套路直接來個系統(tǒng)默認(rèn)的初始化函數(shù)[[FBKVOController alloc] init]或者[FBKVOController new]那Observer豈不是就沒有了。怎么做才能提醒使用方不要調(diào)用系統(tǒng)的初始化函數(shù)呢?
/**
@abstract Allocates memory and initializes a new instance into it.
@warning This method is unavaialble. Please use `controllerWithObserver:` instead.
*/
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
答案就是在這兩個默認(rèn)初始化函數(shù)后面加上NS_UNAVAILABLE宏,這樣如果使用方誤用了系統(tǒng)默認(rèn)的初始化函數(shù)時會給出警告,提醒他應(yīng)該使用模塊指定的初始化接口方法。
NSHashTable & NSMapTable
NSHashTable可以理解為更廣泛意義上的NSMutableSet, 與后者相比NSMapTable主要有如下特性:
- NSHashTable是可變的, 沒有不可變版本
- 可以弱引用所存儲的元素, 當(dāng)元素釋放后會自動被移除
- 可以在添加元素的時候復(fù)制元素后再存放
與NSMutableSet相同之處(與NSMutableArray不同之處)則是:
- 元素都是無序存放的
- 根據(jù)
hash和isEqual來對元素進(jìn)行比較 - 不會存放相同的元素
關(guān)于對比,我們要先區(qū)分==運(yùn)算符和isEqual方法:
UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
在上面的示例中color1 == color2返回false, 但[color1 isEqual:color2]卻是返回true, 原因在于==是直接的指針比較,顯然color1和color2的地址是不同的,而isEqual則是判斷其顏色內(nèi)容是否相同。
類似的還包括NSString isEqualToString / NSDate isEqualToDate
回到NSHashTable中來, NSHashTable可以隨意的存儲指針并且利用指針的唯一性來進(jìn)行hash同一性檢查(檢查成員元素是否有重復(fù))和對比操作(isEqual), 當(dāng)然我們也可以重寫hash/isEqual方法來設(shè)定元素對比和相等的規(guī)則(其實(shí)isEqual是NSObject定義的Protocol). 我們來看下面這個示例:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
+ (instancetype)personWithName:(NSString *)name birthday:(NSDate *)date;
@end
我們定義一個Person類,其包括name和birthday兩個屬性,在我們的正常認(rèn)知下,如果兩個Persion對象的這兩個屬性是一致的,那么他們就是同一個人,所以類似上面UIColor的例子,我們需要重寫下Person的isEqual函數(shù):
- (BOOL)isEqual:(id)object
{
if (nil == object) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[Person class]]) {
return NO;
}
return [self isEqualToPerson:(Person *)object];
}
- (BOOL)isEqualToPerson:(Person *)person
{
if (!person) return NO;
BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}
這里的isEqual函數(shù)的實(shí)現(xiàn)分為四步,也是我們推薦的best pratice:
- 判斷對象是否為空
- 判斷是否同一對象(內(nèi)存地址是否相等)
- 如果不是同一個class那肯定不是同一對象
- 判斷對象的各屬性值是否相等
Person *person1 = [Person personWithName:@"Ryan Jin" birthday:self.date];
Person *person2 = [Person personWithName:@"Ryan Jin" birthday:self.date];
現(xiàn)在如果判斷[person1 isEqual person2]就是返回true了,不過怎么感覺缺了點(diǎn)什么, hash好像還沒用到呀,難道不需要重寫hash方法嗎?
答案當(dāng)然是需要,當(dāng)成員被加入到NSHashTable(也包括NSSet)中時,會被分配一個hash值,以標(biāo)識該成員在集合中的位置,通過這個位置標(biāo)識可以極大的提升成員查找的效率(這也是為什么NSHashTable 查找元素的速度會快于NSArray).
由于NSHashTable/NSSet在添加元素的時候會就行判等操作,當(dāng)某個元素已經(jīng)存在時不會重復(fù)添加,這個判等的操作包括兩步:
- 兩個成員的hash值是否相等,如不相等則立即判斷為不同元素
- 若hash值相等,則再判斷isEqual是否返回一致
只有兩個元素的hash和isEqual都為一致的情況下才判斷為相同對象。好了,明白了這個原則,我們來重寫下Person的hash方法:
- (NSUInteger)hash {
return [self.name hash] ^ [self.birthday hash]; // best practice
}
由于系統(tǒng)的NSString和NSDate在內(nèi)容相同的情況下會返回相同的hash值,所以這邊的最佳實(shí)踐是返回各屬性的位或運(yùn)算。這邊需要注意的是不能簡單的返回[super hash], 因?yàn)槟J(rèn)的hash值為該對象的內(nèi)存地址,所以上面的person1和person2它們的[super hash]是不同的,所以會被判定為不同的元素,而我們想要實(shí)現(xiàn)的是當(dāng)Person的各屬性一致的時候它們即為同一元素。
NSHashTable/NSSet在添加元素和判斷某個元素是否存在(member:/containsObject:)時會調(diào)用hash方法, 另外NSDictionary在查找key時(key為非字符串對象), 也會利用hash值來提高查找效率
我們來看下FBKVO里面用到NSHashTable地方:
NSHashTable *infos = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
這里初始化了一個NSHashTable, 存放類型為NSPointerFunctionsWeakMemory即弱持有成員元素且當(dāng)元素釋放后自動從NSHashTable移除。另外,判等類型為NSPointerFunctionsObjectPointerPersonality即直接使用指針地址是否相等來判斷。如果類型設(shè)置為NSPointerFunctionsObjectPersonality則采用上面所描述hash和isEqual來判斷。
FBKVO這邊采用直接指針地址進(jìn)行元素比較的原因是單例_FBKVOSharedController的infos里面所存放的_FBKVOInfo都是從FBKVOController傳入過來的,已經(jīng)經(jīng)過了判等操作,不會出現(xiàn)相同的對象,所以_infos處理這些_FBKVOInfo元素直接用指針比較就好了,沒必要再去調(diào)用hash和isEqual方法。另外NSPointerFunctionsWeakMemory設(shè)置是為了在_FBKVOInfo釋放后自動從_infos里面移除它, _FBKVOInfo都不存在了,放在里面也沒意義了。從這邊可以看到FBKVO設(shè)計(jì)的確實(shí)很細(xì)致。
NSMapTable可以理解為更廣泛意義上的NSMutableDictionary, 其各項(xiàng)特性和NSHashTable基本相同:
- NSMapTable是可變的, 沒有不可變版本
- 可以弱引用持有keys和values, 當(dāng)key或value釋放后存儲的實(shí)體會被移除
- NSMapTable可以在添加value的時候?qū)alue進(jìn)行復(fù)制
NSMapTable *keyToObjectMapping = [NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn
valueOptions:NSMapTableStrongMemory];
如果按上面這么設(shè)置NSMapTable會和NSMutableDictionary用起來完全一樣: 復(fù)制 key, 并對它的object引用計(jì)數(shù)加一。同樣,我們也來看下FBKVO使用NSMapTable的地方:
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
_observer = observer;
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
}
return self;
}
這邊定義了一個_objectInfosMap: key為被觀察的對象, value則為存放著_FBKVOInfo的NSMutableSet, 這邊在初始化的時候增加了retainObserved變量用來標(biāo)記是否將key強(qiáng)引用,具體調(diào)用示例如下:
[self.KVOController observe:self.photos
keyPath:@"count"
options:NSKeyValueObservingOptionNew
block:^(id observer, id object, NSDictionary *change) {
// observer -> RJViewController -> __weak self
// object -> self.photos
// change -> NSKeyValueChangeKey + FBKVONotificationKeyPathKey
}];
默認(rèn)情況下對key(被觀察的對象)也就是這邊的self.photos做強(qiáng)引用,但是如果我們observe的對象為self本身,那么是不能做強(qiáng)引用持有的,否則就循環(huán)引用了。所以我們在這個情況下需要retainObserved傳入NO, 這也是為什么NSObject+FBKVOController會有一個KVOController和KVOControllerNonRetaining了。
至于_objectInfoMap的value, 因?yàn)槭荖SMutableSet所以直接采用默認(rèn)的強(qiáng)引用持有,這邊大家或許有疑問,為什么value值又采用了NSMutableSet而不用NSHashTable呢?原因是這里并不需要弱引用持有各個_FBKVOInfo對象,而且大部分情況下使用NSMutableSet更加方便,比如NSHashTable就沒有enumerateObjectsUsingBlock的枚舉方法。而NSSet相比較NSArray的區(qū)別是前者無序存放,且使用hash查找元素效率快,但是后者比前者添加元素的速度快很多,所以在選擇使用哪個容器的時候需要根據(jù)具體情況來選擇。
FBKVO對_FBKVOInfo的hash和isEqual進(jìn)行了重寫,以keyPath來進(jìn)行判等。所以這邊對于value設(shè)置為NSPointerFunctionsObjectPersonality以hash/isEqual進(jìn)行判斷,而key值(被觀察者)則設(shè)置為NSPointerFunctionsObjectPointerPersonality直接以指針地址做判斷。
最后我們直接引用Mattt大神對于什么時候用NSMapTable什么時候用NSDictionary的說明來結(jié)束這一小節(jié):
As always, it's important to remember that programming is not about being clever: always approach a problem from the highest viable level of abstraction. NSSet and NSDictionary are great classes. For 99% of problems, they are undoubtedly the correct tool for the job. If, however, your problem has any of the particular memory management constraints described above, then NSHashTable & NSMapTable may be worth a look.
循環(huán)引用
我們知道,使用block的時候極易出現(xiàn)循環(huán)引用,通常使用方需要在block內(nèi)部自己聲明一個weak化的self來避免這個問題。那有沒有辦法省去這一步呢?是的,F(xiàn)BKVO在接口設(shè)計(jì)的時候也考慮到了這個問題,解決方法是在block回調(diào)接口增加一個observer參數(shù),而這個observer在FBKVOController內(nèi)部做了weak化處理,在上面示例中,id observer就是觀察者(即RJViewController *observer),也就是初始化接口時傳入的self, 所以在block內(nèi)部直接使用[observer doSomething]來代替[self doSomething]即可避免循環(huán)引用的問題。
FBKVO避免循環(huán)引用的設(shè)計(jì)確實(shí)很精致,我們來接著看下面這個情況:
[self.KVOController observe:self.photos
keyPath:@"count"
options:NSKeyValueObservingOptionNew
block:^(id observer, id object, NSDictionary *change) {
// observer -> RJViewController -> __weak self
// object -> self.photos
// [self doSomething] -> [observer doSomething]
[self.KVOController unobserve:self.photos keyPath:@"count"]
}];
這里在block里面用self調(diào)用了unobserve方法,按照我們之前的理解,那這邊肯定會出現(xiàn)循環(huán)引用了,應(yīng)該改成:
[observer.KVOController unobserve:observer.photos keyPath:@"count"]
但事實(shí)是在這個情況下,即使用self也不會引起循環(huán)引用,這是為什么呢?原因是做了unobserve操作后,存儲KVO信息的_FBKVOInfo會被釋放掉,這樣它所指向的當(dāng)前這個block也會被置為nil, 這樣block引用self這個鏈端就被打破了,也就不會出現(xiàn)循環(huán)引用的問題了。所以打破循環(huán)引用除了在block內(nèi)使用weakSelf外,也可以在事件處理完后將當(dāng)前的block置為nil來實(shí)現(xiàn)。
線程鎖
FBKVO使用pthread_mutex_t作為線程鎖,關(guān)于iOS各種線程鎖的介紹可以參看這篇博文。我們直接來看下FBKVO使用的其中一個地方:
- (void)_unobserveAll
{
// lock
pthread_mutex_lock(&_lock);
NSMapTable *objectInfoMaps = [_objectInfosMap copy];
// clear table and map
[_objectInfosMap removeAllObjects];
// unlock
pthread_mutex_unlock(&_lock);
_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
for (id object in objectInfoMaps) {
// unobserve each registered object and infos
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}
鎖的基本原則是所有對公共數(shù)據(jù)處理的地方都需要加鎖,上面的代碼中_objectInfosMap為全局的NSMapTable, 對其修改操作(Add/Remove)都需要加鎖, FBKVO這邊的操作很值得借鑒,先拷貝一份臨時變量,然后將_objectInfosMap清空,這一步在鎖里面操作,之后被拷貝出的那份非全局或者說非共享變量再去做相應(yīng)的后續(xù)操作。
這樣的話即使有多個線程訪問_unobserveAll也不會有任何問題,因?yàn)橹挥械谝粋€線程會訪問到_objectInfosMap, 第二個線程等解鎖后再去訪問時_objectInfosMap已經(jīng)為空了,拷貝的對象objectInfoMaps也自然為空,所以鎖并不需要加滿整個_unobserveAll函數(shù)范圍。
如果需要使用互斥類型的pthread_mutex_t鎖,比如在遞歸函數(shù)中加鎖,那需要將pthread_mutex_t初始化為互斥類型:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_mutex, &attr);
pthread_mutexattr_destroy(&attr);
鎖使用完后記得要在dealloc里面銷毀掉:
- (void)dealloc {
pthread_mutex_destroy(&_mutex);
}
DEBUG描述
DEBUG描述(debugDescription)其實(shí)和description是一樣的效果,只是debugDescription是在Xcode控制臺里使用po命令的時候調(diào)用顯示的。如果沒有實(shí)現(xiàn)debugDescription方法,那么打印該對象的時候僅僅顯示內(nèi)存地址,而不會顯示該對象的各個屬性值。
- (NSString *)debugDescription
{
NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
if (0 != _options) {
[s appendFormat:@" options:%@", describe_options(_options)];
}
if (NULL != _action) {
[s appendFormat:@" action:%@", NSStringFromSelector(_action)];
}
if (NULL != _context) {
[s appendFormat:@" context:%p", _context];
}
if (NULL != _block) {
[s appendFormat:@" block:%p", _block];
}
[s appendString:@">"];
return s;
}
上面是FBKVO實(shí)現(xiàn)的_FBKVOInfo的debugDescription, 將各個屬性值拼接成字符串顯示出來。那如果某個對象有N多個屬性,這樣一個個拼接會非常繁瑣,這種情況下可以采用runtime來動態(tài)獲取屬性并返回:
- (NSString *)debugDescription // prefer super class
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
// fetch class's all properties
uint count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
// loop to get each property via KVC
for (int i = 0; i<count; i++) {
objc_property_t property = properties[I];
NSString *name = @(property_getName(property));
id value = [self valueForKey:name]?:@"nil"; // default nil string
[dictionary setObject:value forKey:name]; // add to dicionary
}
free(properties);
return [NSString stringWithFormat:@"<%@-%p> -- %@",[self class],self,dictionary];
}
數(shù)據(jù)結(jié)構(gòu)
KVOController是框架的對外接口類,作為KVO的管理者,其持有了當(dāng)前觀察者對象和被觀察者的KVO信息。 觀察者對象以weak屬性存儲在_observer中,而_objectInfosMap中將被觀察者以key進(jìn)行存儲, value則存儲了對應(yīng)的_ FBKVOInfo集合(圖片引用自Draveness的博文)。

FBKVO為每一個被observe的對象都生成了一個_ FBKVOInfo對象,該對象存儲了所有與KVO相關(guān)的信息,包括路徑,回調(diào)等等。

FBKVO的調(diào)用流程如下圖所示, FBKVOController的作用只是添加相應(yīng)的被觀察者記錄,以及生成相應(yīng)的FBKVOInfo信息,最終會由FBKVOSharedController 這個單例來調(diào)用系統(tǒng)KVO方法實(shí)現(xiàn)對屬性的監(jiān)聽,并且在回調(diào)方法中將事件分發(fā)給 KVO 的觀察者。

FBKVO中還有一個比較有意思的地方是用_來區(qū)分內(nèi)部接口和外部接口:
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info // -> private method
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath // -> public method
包括類名也是如此:
@interface FBKVOController : NSObject // -> public
@interface _FBKVOInfo : NSObject // -> internal
@interface _FBKVOSharedController : NSObject // -> internal
FBKVO的代碼量雖然不多,但其框架流程,接口設(shè)計(jì)和代碼中使用到的細(xì)微技術(shù)點(diǎn)確實(shí)極具水平,希望本文總結(jié)和提煉的各種姿勢可以讓大家有一些收獲,也歡迎大家留言討論。完。