讀源碼漲姿勢之優(yōu)雅KVO實(shí)現(xiàn)

如果說書籍是人類進(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), xBOOL值, y為字符串類型, 當(dāng)xNO時產(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就是在FBKVOControllerdealloc里面做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ù)hashisEqual來對元素進(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:

  1. 判斷對象是否為空
  2. 判斷是否同一對象(內(nèi)存地址是否相等)
  3. 如果不是同一個class那肯定不是同一對象
  4. 判斷對象的各屬性值是否相等
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ù)添加,這個判等的操作包括兩步:

  1. 兩個成員的hash值是否相等,如不相等則立即判斷為不同元素
  2. 若hash值相等,則再判斷isEqual是否返回一致

只有兩個元素的hashisEqual都為一致的情況下才判斷為相同對象。好了,明白了這個原則,我們來重寫下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)存地址,所以上面的person1person2它們的[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)行元素比較的原因是單例_FBKVOSharedControllerinfos里面所存放的_FBKVOInfo都是從FBKVOController傳入過來的,已經(jīng)經(jīng)過了判等操作,不會出現(xiàn)相同的對象,所以_infos處理這些_FBKVOInfo元素直接用指針比較就好了,沒必要再去調(diào)用hashisEqual方法。另外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的博文)。

KVOController

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

FBKVOInfo

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

調(diào)用流程

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é)和提煉的各種姿勢可以讓大家有一些收獲,也歡迎大家留言討論。完。

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

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