前言
進(jìn)入 iOS 開發(fā)一年多拉背,大部分時間都在寫業(yè)務(wù)代碼师崎,鮮有對優(yōu)秀開源代碼的學(xué)習(xí)、總結(jié)椅棺。深知犁罩,是時候開始學(xué)習(xí)一些。萬事開頭難两疚,所以我準(zhǔn)備從比較簡短的開源代碼開始學(xué)習(xí)床估。第一篇準(zhǔn)備寫寫 Facebook
這個極度熱愛開源的公司的一套關(guān)于 KVO
的開源代碼——FBKVOController
。閱讀本篇文章前诱渤,希望你對 KVO
已經(jīng)有一定的了解丐巫。
正文
先說說本文主要想講一下哪些東西。
概述
- FBKVOController
做了什么
- FBKVOController
使用姿勢
- FBKVOController
源碼解析
- FBKVOController
設(shè)計思路總結(jié)
- FBKVOController
其它收獲
FBKVOController 做了什么勺美?
簡單來說递胧,Facebook
開源的這套代碼,主要是對我們經(jīng)常使用的 KVO
機(jī)制進(jìn)行了額外的一層封裝赡茸。其中最亮眼的特色是提供了一個 block 回調(diào)讓我們進(jìn)行處理缎脾,避免 KVO
的相關(guān)代碼四處散落,不再需要使用下面這個方法:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
使用姿勢
利用開源框架占卧,我們這樣實現(xiàn)遗菠,其中第二種方法可以用一行代碼實現(xiàn) KVO
:
#import "ViewController.h"
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"
@interface KVOModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation KVOModel
@end
NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (nonatomic, strong) KVOModel *model;
@property (nonatomic, strong) FBKVOController *kvoController;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建被觀察的 model 類
KVOModel *model = [[KVOModel alloc] init];
//初始化設(shè)置 model 的成員變量值
model.name = @"wo";
model.age = 5;
self.model = model;
//第一種方法:創(chuàng)建 FBKVOController 對象联喘,并被 VC 強(qiáng)引用,否則出了當(dāng)前作用域辙纬,就會被銷毀
FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self];
_kvoController = kvoController;
//添加 觀察
[kvoController observe:model keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"我的舊名字是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"我的新名字是:%@", change[NSKeyValueChangeNewKey]);
}];
//第二種方法:無需主動創(chuàng)建 FBKVOController 對象豁遭,self.KVOController 直接懶加載創(chuàng)建FBKVOController 對象
//可以直接對某個對象的多個成員變量執(zhí)行 KVO
//------真正實現(xiàn)一行代碼搞定 KVO------
[self.KVOController observe:model keyPaths:@[@"name", @"age"] options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];
if ([changedKeyPath isEqualToString:@"name"]) {
NSLog(@"修改了名字");
} else if ([changedKeyPath isEqualToString:@"age"]) {
NSLog(@"修改了年齡");
}
NSLog(@"舊值是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
}];
//修改 model 的 name 成員變量
model.name = @"ni";
}
@end
NS_ASSUME_NONNULL_END
相比于原生 API 優(yōu)勢:
- 1 可以以
數(shù)組形式
,同時對model
的多個 不同成員變量進(jìn)行KVO
贺拣。 - 2 利用提供的
block
蓖谢,將KVO
相關(guān)代碼集中在一塊,而不是四處散落纵柿。比較清晰蜈抓,一目了然。 - 3 不需要在
dealloc
方法里取消對 object 的觀察昂儒,當(dāng)FBKVOController
對象dealloc
沟使,會自動取消觀察。
源碼解析
這套源代碼主要包括了FBKVOController.h
渊跋、FBKVOController.m
腊嗡、NSObject+FBKVOController.h
、NSObject+FBKVOController.m
四個文件拾酝。
其中燕少,NSObject+FBKVOController
這個分類比較簡單。它主要干的事是通過 objc_setAssociatedObject
(關(guān)聯(lián)對象)蒿囤,以懶加載的形式給 NSObject
客们,創(chuàng)建并關(guān)聯(lián)一個 FBKVOController
的對象。
接下來材诽,我會著重介紹一下今天的主角 FBKVOController
類底挫。其文件中還包含另外兩個類,_FBKVOInfo
脸侥、_FBKVOSharedController
建邓。下面都會介紹到。
先來看看 FBKVOController
指定初始化函數(shù):
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
//一般情況下 observer 會持有 FBKVOController 為了避免循環(huán)引用睁枕,此處的_observer 的內(nèi)存管理語義是弱引用
_observer = observer;
//定義 NSMapTable key的內(nèi)存管理策略官边,在默認(rèn)情況,傳入的參數(shù) retainObserved = YES
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
//創(chuàng)建 NSMapTable key 為 id 類型外遇,value 為 NSMutableSet<_FBKVOInfo *> 類型
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
//初始化互斥鎖注簿,避免多線程間的數(shù)據(jù)競爭
pthread_mutex_init(&_lock, NULL);
}
return self;
}
以上初始化代碼中,注釋都寫得比較清楚了跳仿。唯一比較陌生的是 NSMapTable
诡渴。簡單來說,它與 NSDictionary
類似塔嬉。不同之處是 NSMapTable
可以自主控制 key
/ value
的內(nèi)存管理策略玩徊。而 NSDictionary
的內(nèi)存策略是固定為 copy
。當(dāng) key 為 object
時谨究, copy
的開銷可能比較大恩袱!因此,在這里只能使用相對比較靈活的 NSMapTable
胶哲。
執(zhí)行 KVO
的相關(guān)方法代碼解析
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//當(dāng) keyPath 字符串長度為 0 或者 block 為空時畔塔,會產(chǎn)生斷言,程序會 crash
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
//如果 “被觀察對象” 為 nil鸯屿,同樣會直接返回
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// create info _FBKVOInfo
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// observe object with info (利用存儲的信息對 “被觀察對象” 進(jìn)行觀察3憾帧)
[self _observe:object info:info];
}
上述代碼中,出現(xiàn)了一個前面提及到的 _FBKVOInfo
類寄摆,其存儲的信息包括了 FBKVOController
谅辣、keypath
、options
婶恼、block
桑阶。
接上段代碼的最后一句 [self _observe:object info:info];
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock 互斥鎖加鎖
pthread_mutex_lock(&_lock);
//還記得初始化 FBKVOController 時創(chuàng)建的 NSMapTable 么?
//其結(jié)構(gòu)是以 被觀察者 object 為 key勾邦。并不像我們常用的 NSDictionary 那樣是以 NSString 為 key
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
// 必須重寫 _FBKVOInfo hash 以及 isEqual 方法蚣录,這樣才能使用 NSSet 的 member 方法。
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
// observation info already exists; do not observe it again
// unlock and return
pthread_mutex_unlock(&_lock);
return;
}
//如果沒有 關(guān)于這個 object(被觀察者)的相關(guān)信息眷篇,則創(chuàng)建 NSMutableSet萎河,并添加到 NSMapTable 中
// lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
// add info and oberve -- NSMutableSet 加 info
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
//sharedController 是 干嘛的? 將所有觀察信息統(tǒng)一交由一個單例來完成
[[_FBKVOSharedController sharedController] observe:object info:info];
}
總結(jié)一下上面一段的數(shù)據(jù)結(jié)構(gòu)蕉饼。FBKVOController
擁有成員變量 NSMapTable
虐杯,NSMapTable
以被觀察者
(object)為 key, NSMutableSet
為 value 椎椰。在 NSMutableSet
中厦幅,存儲了不同 info
。其關(guān)系圖如下圖:
追蹤一下這句代碼
[[_FBKVOSharedController sharedController] observe:object info:info];
_FBKVOSharedController 是會在 app 生命周期一直存在的單例慨飘,其職責(zé)是:接收并轉(zhuǎn)發(fā) KVO
通知确憨。因此 app 當(dāng)中所有 KVO
的通知都是由這個單例來完成的。
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info 向 NSHashTable 添加 info
//注意:在 _FBKVOController 類中的 NSMutableSet 已經(jīng)強(qiáng)引用了 info
//這里是為了弱引用 info瓤的,才使用 NSHashTable休弃,當(dāng) info dealloc 時,同時會從容器中刪除
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
//_FBKVOSharedController 是實際的觀察者圈膏! 隨后會進(jìn)行轉(zhuǎn)發(fā) 塔猾,
//context 是 void * 無類型指針,是 info 的指針稽坤!
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
//如果 state 是原始狀態(tài)丈甸,則改為正在觀察的狀態(tài)糯俗,表明是在正在觀察的狀態(tài)
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
以上代碼中想單獨說一下下面的代碼,其中的 context
參數(shù)使用的是 (void *)info
的指針睦擂,這樣可以保證 context
的唯一性得湘。
接收 KVO 通知,并做相應(yīng)處理
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// lookup context in registered infos, taking out a strong reference only if it exists
// 利用 context 查找 info顿仇,其中用到了 void * 轉(zhuǎn)換為 id 型變量 (__bridge id)
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
id observer = controller.observer;
if (nil != observer) {
// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
//字典合并淘正,并重新拷貝一份,
//包含信息有:1臼闻、改變了哪個值 mChange 2鸿吆、 原先的 change 字典
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
//忽略警告!
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
//默認(rèn)情況 調(diào)用觀察者的原生函數(shù)J瞿拧惩淳!
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
設(shè)計思路總結(jié)
- 1
FBKVOController
持有NSMapTable
,以object
為key
得到相對應(yīng)的NSMutableSet
乓搬。NSMutableSet
中存儲了不同的_FBKVOInfo
黎泣。這套數(shù)據(jù)結(jié)構(gòu)的主要作用是防止開發(fā)人員重復(fù)添加相同的KVO
。當(dāng)檢查到其中已存在相同的_FBKVOInfo
對象時缤谎,不再執(zhí)行后面的代碼抒倚。 - 2
_FBKVOSharedController
持有NSHashTable
。NSHashTable
以弱引用的方式持有不同的_FBKVOInfo
坷澡。此處實際執(zhí)行KVO
代碼托呕。_FBKVOInfo
有一個重要的成員變量_FBKVOInfoState
,根據(jù)這個枚舉值(_FBKVOInfoStateInitial
频敛、_FBKVOInfoStateObserving
项郊、_FBKVOInfoStateNotObserving
) 來決定新增或者刪除KVO
。
收獲(通讀斟赚、研究源代碼后)
- 1
NSSet
/NSHashTable
着降、NSDictionary
/NSMapTable
的學(xué)習(xí)-
NSSet
是過濾掉重復(fù)object
的集合類,NSHashTable
是NSSet
的升級版容器拗军,并且只有可變版本任洞,允許對添加到容器中的對象是弱引用的持有關(guān)系, 當(dāng)NSHashTable
中的對象銷毀時发侵,該對象也會從容器中移除交掏。 -
NSMapTable
同NSDictionary
類似,唯一區(qū)別是多了個功能:可以設(shè)置key
和value
的NSPointerFunctionsOptions
特性!NSDictionary
的key
策略固定是copy
刃鳄,考慮到開銷問題盅弛,一般使用簡單的數(shù)字或者字符串為key
。但是如果碰到需要用object
作為key
的應(yīng)用場景呢?NSMapTable
就可以派上用場了挪鹏!可以通過NSFunctionsPointer
來分別定義對key
和value
的內(nèi)存管理策略见秽,簡單可以分為strong
,weak
以及copy
。
-
- 2 幾個比較有用的宏
-
NS_ASSUME_NONNULL_BEGIN
讨盒、NS_ASSUME_NONNULL_END
张吉,如果需要每個屬性或每個方法都去指定nonnull
和nullable
,是一件非常繁瑣的事催植。蘋果為了減輕我們的工作量,專門提供了這兩個宏勺择。在這兩個宏之間的代碼创南,所有比較簡單指針對象都被假定為nonnull
,因此我們只需要去指定那些nullable
的指針省核。如果我們強(qiáng)行通過點語法將一個非空指針置空稿辙,編譯器會報warning
。 -
NS_UNAVAILABLE
當(dāng)我們不想要其他開發(fā)人員气忠,用普通的 init 方法去初始化一個類邻储,我們可以在.h 文件里這樣寫:
- (instancetype)init NS_UNAVAILABLE;
編譯器不但不會提示補(bǔ)全init
方法,就算開發(fā)人員強(qiáng)制發(fā)送 init 消息旧噪,編譯器會直接報錯吨娜。 -
NS_DESIGNATED_INITIALIZER
指定的初始化方法。當(dāng)一個類提供多種初始化方法時淘钟,所有的初始化方法最終都會調(diào)用這個指定的初始化方法宦赠。比較常見的有:
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
-
- 3 斷言的使用
NSAssert(x,y);
:x
為BOOL
值,y
為 字符串類型米母。當(dāng)x
=YES
勾扭,則不產(chǎn)生斷言。當(dāng)x
=NO
铁瞒,則產(chǎn)生斷言妙色,app 會 crash,并在控制臺中打印y
字符串內(nèi)容慧耍。合理利用斷言身辨,可以保證 app 的健壯性。 - 4 互斥鎖的使用
-
pthread_mutex_init(&_lock, NULL);
(初始化)&_lock
是互斥鎖的指針芍碧,第二個參數(shù)是互斥鎖的屬性栅表。缺省值
是:當(dāng)一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列师枣,并在解鎖后按優(yōu)先級獲得鎖怪瓶。這種鎖策略保證了資源分配的公平性。 -
pthread_mutex_destroy(&_lock);
(銷毀) -
pthread_mutex_lock(&_lock);
(加鎖) -
pthread_mutex_unlock(&_lock);
(解鎖) - 涉及到數(shù)據(jù)的讀寫操作時,都需要加鎖來保證避免數(shù)據(jù)競爭洗贰。
- 順便復(fù)習(xí)一下
死鎖
的概念:如果線程A鎖住了記錄1并等待記錄2找岖,而線程B鎖住了記錄2并等待記錄1,這樣兩個線程就發(fā)生了死鎖現(xiàn)象敛滋。
-
小尾巴
第一次寫源碼解析许布,感覺思路都還比較混亂,認(rèn)識也還比較淺薄绎晃,需要逐漸摸索一下蜜唾。有什么問題歡迎提給我!
一些相關(guān)知識的鏈接
NSHashTable的特性和使用
互斥鎖