前言
進入 iOS 開發一年多,大部分時間都在寫業務程式碼,鮮有對優秀開原始碼的學習、總結。深知,是時候開始學習一些。萬事開頭難,所以我準備從比較簡短的開原始碼開始學習。第一篇準備寫寫 Facebook
這個極度熱愛開源的公司的一套關於 KVO
的開原始碼——FBKVOController
。閱讀本篇文章前,希望你對 KVO
已經有一定的瞭解。
正文
先說說本文主要想講一下哪些東西。
概述
- FBKVOController
做了什麼
- FBKVOController
使用姿勢
- FBKVOController
原始碼解析
- FBKVOController
設計思路總結
- FBKVOController
其它收穫
FBKVOController 做了什麼?
簡單來說,Facebook
開源的這套程式碼,主要是對我們經常使用的 KVO
機制進行了額外的一層封裝。其中最亮眼的特色是提供了一個 block 回撥讓我們進行處理,避免 KVO
的相關程式碼四處散落,不再需要使用下面這個方法:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
複製程式碼
使用姿勢
利用開源框架,我們這樣實現,其中第二種方法可以用一行程式碼實現 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];
//建立被觀察的 model 類
KVOModel *model = [[KVOModel alloc] init];
//初始化設定 model 的成員變數值
model.name = @"wo";
model.age = 5;
self.model = model;
//第一種方法:建立 FBKVOController 物件,並被 VC 強引用,否則出了當前作用域,就會被銷燬
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]);
}];
//第二種方法:無需主動建立 FBKVOController 物件,self.KVOController 直接懶載入建立FBKVOController 物件
//可以直接對某個物件的多個成員變數執行 KVO
//------真正實現一行程式碼搞定 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 優勢:
- 1 可以以
陣列形式
,同時對model
的多個 不同成員變數進行KVO
。 - 2 利用提供的
block
,將KVO
相關程式碼集中在一塊,而不是四處散落。比較清晰,一目瞭然。 - 3 不需要在
dealloc
方法裡取消對 object 的觀察,當FBKVOController
物件dealloc
,會自動取消觀察。
原始碼解析
這套原始碼主要包括了FBKVOController.h
、FBKVOController.m
、NSObject+FBKVOController.h
、NSObject+FBKVOController.m
四個檔案。
其中,NSObject+FBKVOController
這個分類比較簡單。它主要乾的事是通過 objc_setAssociatedObject
(關聯物件),以懶載入的形式給 NSObject
,建立並關聯一個 FBKVOController
的物件。
接下來,我會著重介紹一下今天的主角 FBKVOController
類。其檔案中還包含另外兩個類,_FBKVOInfo
、_FBKVOSharedController
。下面都會介紹到。
先來看看 FBKVOController
指定初始化函式:
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
//一般情況下 observer 會持有 FBKVOController 為了避免迴圈引用,此處的_observer 的記憶體管理語義是弱引用
_observer = observer;
//定義 NSMapTable key的記憶體管理策略,在預設情況,傳入的引數 retainObserved = YES
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
//建立 NSMapTable key 為 id 型別,value 為 NSMutableSet<_FBKVOInfo *> 型別
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
//初始化互斥鎖,避免多執行緒間的資料競爭
pthread_mutex_init(&_lock, NULL);
}
return self;
}
複製程式碼
以上初始化程式碼中,註釋都寫得比較清楚了。唯一比較陌生的是 NSMapTable
。簡單來說,它與 NSDictionary
類似。不同之處是 NSMapTable
可以自主控制 key
/ value
的記憶體管理策略。而 NSDictionary
的記憶體策略是固定為 copy
。當 key 為 object
時, copy
的開銷可能比較大!因此,在這裡只能使用相對比較靈活的 NSMapTable
。
執行 KVO
的相關方法程式碼解析
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//當 keyPath 字串長度為 0 或者 block 為空時,會產生斷言,程式會 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 (利用儲存的資訊對 “被觀察物件” 進行觀察!)
[self _observe:object info:info];
}
複製程式碼
上述程式碼中,出現了一個前面提及到的 _FBKVOInfo
類,其儲存的資訊包括了 FBKVOController
、keypath
、options
、block
。
接上段程式碼的最後一句 [self _observe:object info:info];
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock 互斥鎖加鎖
pthread_mutex_lock(&_lock);
//還記得初始化 FBKVOController 時建立的 NSMapTable 麼?
//其結構是以 被觀察者 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;
}
//如果沒有 關於這個 object(被觀察者)的相關資訊,則建立 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 是 幹嘛的? 將所有觀察資訊統一交由一個單例來完成
[[_FBKVOSharedController sharedController] observe:object info:info];
}
複製程式碼
總結一下上面一段的資料結構。FBKVOController
擁有成員變數 NSMapTable
,NSMapTable
以被觀察者
(object)為 key, NSMutableSet
為 value 。在 NSMutableSet
中,儲存了不同 info
。其關係圖如下圖:
[[_FBKVOSharedController sharedController] observe:object info:info];
複製程式碼
_FBKVOSharedController 是會在 app 生命週期一直存在的單例,其職責是:接收並轉發 KVO
通知。因此 app 當中所有 KVO
的通知都是由這個單例來完成的。
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info 向 NSHashTable 新增 info
//注意:在 _FBKVOController 類中的 NSMutableSet 已經強引用了 info
//這裡是為了弱引用 info,才使用 NSHashTable,當 info dealloc 時,同時會從容器中刪除
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
//_FBKVOSharedController 是實際的觀察者! 隨後會進行轉發 ,
//context 是 void * 無型別指標,是 info 的指標!
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
//如果 state 是原始狀態,則改為正在觀察的狀態,表明是在正在觀察的狀態
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
引數使用的是 (void *)info
的指標,這樣可以保證 context
的唯一性。
接收 KVO 通知,並做相應處理
- (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 * 轉換為 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 {
//預設情況 呼叫觀察者的原生函式!!
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
複製程式碼
設計思路總結
- 1
FBKVOController
持有NSMapTable
,以object
為key
得到相對應的NSMutableSet
。NSMutableSet
中儲存了不同的_FBKVOInfo
。這套資料結構的主要作用是防止開發人員重複新增相同的KVO
。當檢查到其中已存在相同的_FBKVOInfo
物件時,不再執行後面的程式碼。 - 2
_FBKVOSharedController
持有NSHashTable
。NSHashTable
以弱引用的方式持有不同的_FBKVOInfo
。此處實際執行KVO
程式碼。_FBKVOInfo
有一個重要的成員變數_FBKVOInfoState
,根據這個列舉值(_FBKVOInfoStateInitial
、_FBKVOInfoStateObserving
、_FBKVOInfoStateNotObserving
) 來決定新增或者刪除KVO
。
收穫(通讀、研究原始碼後)
- 1
NSSet
/NSHashTable
、NSDictionary
/NSMapTable
的學習NSSet
是過濾掉重複object
的集合類,NSHashTable
是NSSet
的升級版容器,並且只有可變版本,允許對新增到容器中的物件是弱引用的持有關係, 當NSHashTable
中的物件銷燬時,該物件也會從容器中移除。NSMapTable
同NSDictionary
類似,唯一區別是多了個功能:可以設定key
和value
的NSPointerFunctionsOptions
特性!NSDictionary
的key
策略固定是copy
,考慮到開銷問題,一般使用簡單的數字或者字串為key
。但是如果碰到需要用object
作為key
的應用場景呢?NSMapTable
就可以派上用場了!可以通過NSFunctionsPointer
來分別定義對key
和value
的記憶體管理策略,簡單可以分為strong
,weak
以及copy
。
- 2 幾個比較有用的巨集
NS_ASSUME_NONNULL_BEGIN
、NS_ASSUME_NONNULL_END
,如果需要每個屬性或每個方法都去指定nonnull
和nullable
,是一件非常繁瑣的事。蘋果為了減輕我們的工作量,專門提供了這兩個巨集。在這兩個巨集之間的程式碼,所有比較簡單指標物件都被假定為nonnull
,因此我們只需要去指定那些nullable
的指標。如果我們強行通過點語法將一個非空指標置空,編譯器會報warning
。NS_UNAVAILABLE
當我們不想要其他開發人員,用普通的 init 方法去初始化一個類,我們可以在.h 檔案裡這樣寫:- (instancetype)init NS_UNAVAILABLE;
編譯器不但不會提示補全init
方法,就算開發人員強制傳送 init 訊息,編譯器會直接報錯。NS_DESIGNATED_INITIALIZER
指定的初始化方法。當一個類提供多種初始化方法時,所有的初始化方法最終都會呼叫這個指定的初始化方法。比較常見的有:- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
- 3 斷言的使用
NSAssert(x,y);
:x
為BOOL
值,y
為 字串型別。當x
=YES
,則不產生斷言。當x
=NO
,則產生斷言,app 會 crash,並在控制檯中列印y
字串內容。合理利用斷言,可以保證 app 的健壯性。 - 4 互斥鎖的使用
pthread_mutex_init(&_lock, NULL);
(初始化)&_lock
是互斥鎖的指標,第二個引數是互斥鎖的屬性。預設值
是:當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平性。pthread_mutex_destroy(&_lock);
(銷燬)pthread_mutex_lock(&_lock);
(加鎖)pthread_mutex_unlock(&_lock);
(解鎖)- 涉及到資料的讀寫操作時,都需要加鎖來保證避免資料競爭。
- 順便複習一下
死鎖
的概念:如果執行緒A鎖住了記錄1並等待記錄2,而執行緒B鎖住了記錄2並等待記錄1,這樣兩個執行緒就發生了死鎖現象。
小尾巴
第一次寫原始碼解析,感覺思路都還比較混亂,認識也還比較淺薄,需要逐漸摸索一下。有什麼問題歡迎提給我!
一些相關知識的連結 NSHashTable的特性和使用 [互斥鎖](http://blog.csdn.net/yasi_xi/article/details/19112077)