上一章節咱們講了
KVC
的使用和底層原理,並本身簡單實現了一個簡單的KVC,對KVC已經有了基本的瞭解,那麼這一章節,就來說一下,基於KVC的KVO
是怎麼一回事html
傳送門☞iOS底層學習 - KVC探索之路編程
KVO:(Key-Value-Observer)
是一種機制,也叫觀察者模式,該機制容許將其餘對象的特定屬性的更改通知給對象。對於應用程序中模型層和控制器層之間的通訊特別有用c#
KVO的在平時的開發過程當中,使用也比較多。基本就是3個步驟:數組
下面逐步講解這3個步驟的使用安全
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
複製代碼
使用上述代碼來進行觀察者的註冊,首先來看一下各參數的意義bash
observer
:KVO
通知的對象,須要實現observeValueForKeyPath:ofObject:change:context:
代理方法keyPath
: 被觀察者的屬性的名稱options
: 枚舉類型,主要是觀察的屬性的變化類型context
: 上下文,主要是傳遞給代理使用。前兩個參數的意思比較好理解,對於後兩個參數,平時理解可能不那麼深入,咱們來重點解釋一下。多線程
1.options
app
NSKeyValueObservingOptionNew
:觀察屬性變化後的新值框架
NSKeyValueObservingOptionOld
:觀察屬性變化後的舊值函數式編程
NSKeyValueObservingOptionInitial
:在屬性發生變化後當即通知觀察者,這個過程甚至早於觀察者註冊(使用較少)。簡單來講就是這個枚舉值會在屬性變化前先觸發一次回調。
NSKeyValueObservingOptionPrior
:這個枚舉值會前後連續出發兩次 observeValueForKeyPath
回調。同時在回調中的可變字典中會有一個布爾值的 key - notificationIsPrior
來標識屬性值是變化前仍是變化後的。若是是變化後的回調,那麼可變字典中就只有 new
的值了
在平時的開發中,咱們最嘗試用的就是NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
,從而進行邏輯編寫
2.context
The context pointer in the addObserver:forKeyPath:options:context: message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
[譯]addObserver:forKeyPath:options:context:消息中的上下文指針包含任意數據,這些數據將在相應的更改通知中傳遞迴觀察者。您能夠指定NULL並徹底依賴鍵路徑字符串來肯定更改通知的來源,可是這種方法可能會給對象的父類帶來問題,該對象的超類也出於不一樣的緣由而觀察相同的鍵路徑。
一種更安全,更可擴展的方法是使用上下文確保您收到的通知是發給觀察者的,而不是超類的。
經過閱讀文檔,咱們知道,context
並非必需要傳遞的參數,若是不用時,咱們最好傳遞NULL
,這時候在回調中的判斷就須要徹底根據object
和keyPath
來進行判斷。可是不一樣若是有相同名稱keyPath
時,判斷起來就要嵌套多層判斷語句,使用context
能夠完美解決這個問題。
那麼怎麼使用比較好呢?
我推薦你們使用static void * XXContext = &XXContext;
這種方法,靜態變量存儲着它的指針地址。在回調中能夠直接使用context == XXContex
來進行判斷,方便快捷,並且更安全高效,擴展形強
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
複製代碼
接收觀察者的對象,必須實現以上代理,才能接收到變化先後的值和參數。
參數的含義都比較好理解,和註冊時填寫的參數是基本一致的。change
字典中就包含了觀察屬性變化先後的值,咱們所須要的數據也在裏面。object
是被觀察的對象,context
是註冊時傳遞的上下文,咱們通常用來作判斷使用.
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
複製代碼
註冊和刪除是一一對應的關係。若是註冊了觀察者,則必須進行移除。
一旦對某個對象上的屬性註冊了觀察者,能夠選擇在收到屬性值變化後取消註冊,也能夠在觀察者聲明週期結束以前(好比:dealloc
方法) 取消註冊,若是忘記調用取消註冊方法,那麼一旦觀察者被銷燬後,KVO
機制會給一個不存在的對象發送變化回調消息致使野指針錯誤。致使崩潰
經過上述的3大步驟註冊到一個觀察者後,當被觀察的keyPath
出現變化時,對應的回調就能收到相關的數據,這屬於系統給咱們實現好的自動擋KVO
。可是在平常開發中,咱們可能有一部分須要監聽,一部分不須要監聽,這時候咱們想要本身控制KVO變化,那咱們就須要實現手動擋KVO
了。
實現手動擋的KVO,須要修改下面的方法。系統默認爲YES
,若是咱們改成NO
,則說明被觀察者須要手動進行觀察,才能出發回調了。咱們能夠再此方法中經過判斷key
來進行自由的手動和自動的選擇
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
複製代碼
好比咱們的nick
屬性須要進行手動處理,咱們能夠再他的set
方法中添加willChangeValueForKey
和didChangeValueForKey
來標誌屬性即將發生變化和變化完成,這樣就實現了一個手動擋的KVO
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
複製代碼
當咱們觀察一個集合類型的屬性時,使用方法須要有細微的差異。是由於KVO是基於KVC的,因此必須有對應的set
或者insert
方法是才能夠。
好比咱們監聽dateArray
可變數組(須要初始化)
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
複製代碼
若是咱們直接調用add
方法,這個時候是沒有走KVC的,因此此時KVO是監聽不到的。
[self.person.dateArray addObject:@"1"];
複製代碼
這個時候咱們應該使用KVC的方式,來對數組進行賦值,此時,就能夠監聽到數組的變化了
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
複製代碼
所謂一對多,就是被觀察的屬性的變化,取決於其餘多個屬性的變化。最多見的例子就是下載進度。下載進度=如今進度/總進度
,因此當咱們觀察下載進度時,如今進度和總進度發生變化,都要觸發對應的回調。
好比,下載進入的get方法以下,能夠看到其收其餘兩個屬性影響
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
複製代碼
當咱們監聽了downloadProgress
屬性,writtenData
和totalData
發生變化時,都能在回調中收到對應值。
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.writtenData += 10;
self.person.totalData += 20;
複製代碼
Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
[譯]自動鍵值觀察是使用稱爲isa-swizzling的技術實現的。 該isa指針,顧名思義,指向對象的類,它保持一個調度表。該分派表實質上包含指向該類實現的方法的指針以及其餘數據。 在爲對象的屬性註冊觀察者時,將修改觀察對象的isa指針,指向中間類而不是真實類。結果,isa指針的值不必定反映實例的實際類。 您永遠不要依靠isa指針來肯定類成員。相反,您應該使用該class方法肯定對象實例的類。
經過閱讀文檔內容,咱們能夠發現,KVO
的實現原理很簡單,即isa-swizzling
,把對象指向類的isa
指向了一個中間類
。
經過下圖對象添加觀察者先後,isa
指向的變化,能夠看到。在添加了觀察者的註冊以後,isa
指向了一個名爲NSKVONotifying_XXX
的類
咱們能夠經過以下代碼,查看有關該類的全部相關繼承的類。
- (void)printClasses:(Class)cls{
// 註冊類的總數
int count = objc_getClassList(NULL, 0);
// 建立一個數組, 其中包含給定對象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 獲取全部已註冊的類
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
複製代碼
經過先後打印LGPerson
的相關類,能夠明顯的看出,生成的中間類,是繼承自原來的類的,是其子類
咱們知道,KVO
是基於KVC
的基礎上的,因此改變時必有get
和set
方法。那麼是否是KVO
是根據監聽其set
方法來達到目的的呢,咱們能夠經過打印中間類重寫的方法來獲得驗證。
#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"*********************");
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
複製代碼
經過在添加觀察者先後方法列表,咱們能夠發現,繼承自原來類的中間類,主要重寫了set<keyPath>
,class
,dealloc
,_isKVOA
方法,從而達到KVO
的實現。
set<keyPath>
KVO
是基於KVC
的,屬性發生變化時,必然要走進set
方法,因此重寫此方法是必然的。KVO
在重寫set
方法後,內部要調用willChangeValueForKey
和didChangeValueForKey
方法,並在中間觸發observeValueForKeyPath:ofObject:change:context:
回調,從而通知給觀察者進行操做。
class
重寫此方法是爲了對中間類進行假裝,經過對添加觀察先後,打印類的isa指向能夠得知,獲取到的元類仍是LGPerson
,說明系統內部對class
方法的重寫是對中間類的假裝,並在類調用calss
方法時,仍是獲取到的原來的類。
dealloc
在添加KVO
進行後,進行了isa_swizing
,可是什麼時候給交換回來呢。
經過對dealloc
方法打斷點,能夠得知,在觀察者銷燬後,對象的指向就會交換回來
那麼對象的isa
交換回來後,中間類是否銷燬了呢,咱們能夠再打印一下相關的類和子類列表看一下,由此能夠發現,中間類並不會銷燬
_isKVOA
該方法就是用來標識是不是在觀察者狀態的一個標誌位。
在瞭解了KVO
的底層原理後,咱們仍是和KVC
同樣,嘗試來簡單的來自定義一個KVO
來加深一下印象。
首先咱們仍是仍是新建一個NSObject
的分類用來處理KVO
相關的邏輯,並對系統的KVO
進行了函數式編程和自動銷燬觀察者等優化,主體思路以下:
setter
的方法isa-swizzling
到中間類Model
並保存在數組或者字典中,用來輸出變化先後的數據,經過關聯對象保存setter
並進行回調
set
方法獲取getter
方法的名稱 set<Key>:===> key
,從而獲取到key
keyPath
和KVC
的方式獲取到舊值objc_msgSendSuper
),歷來調用原來類的setter
方法block
回調,將保存的Model
信息傳遞給觀察者method-swizzling
交換dealloc
方法isa
指回給原來的類dealloc
原方法以上爲自定義KVO
的主體思路,下面咱們經過代碼來看一下。
首先咱們創建一個Model
,用來保存咱們每次觀察者註冊時候的信息,用來返回舊值,保存傳遞block
回調等。
static NSString *const kLGKVOPrefix = @"LGKVONotifying_";
static NSString *const kLGKVOAssiociateKey = @"kLGKVO_AssiociateKey";
@interface LGInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) LGKVOBlock handleBlock;
@end
@implementation LGInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
複製代碼
接着提供註冊的方法,和系統的基本同樣,咱們應該加上自定義的前綴和block回調
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block;
複製代碼
下面是實現註冊的主要代碼
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block{
✅// 1: 驗證是否存在setter方法 : 不讓實例進來
[self judgeSetterMethodFromKeyPath:keyPath];
✅// 2: 動態生成子類
Class newClass = [self createChildClassWithKeyPath:keyPath];
✅// 3: isa的指向 : LGKVONotifying_LGPerson
object_setClass(self, newClass);
✅// 4: 保存信息,使用關聯對象保存數組
LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
}
複製代碼
如下是各步驟的具體實現。
1.驗證是否存在setter
方法
#pragma mark - 驗證是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
✅// 獲取到當前的類
Class superClass = object_getClass(self);
✅// 獲取到當前的set方法IMP並進行判斷
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老鐵沒有當前%@的setter",keyPath] userInfo:nil];
}
}
複製代碼
2.動態生成子類。經過原理的探究,咱們知道,中間類主要是重寫了3個方法,因此咱們在建立類的時候,要把這3個方法也動態建立出來,對原來類的方法進行重寫
#pragma mark - 動態生成子類
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
NSString *oldClassName = NSStringFromClass([self class]);
✅// 在原來類的類名基礎上進行拼接
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
✅// 防止重複建立生成新類
if (newClass) return newClass;
/** * 若是內存不存在,建立生成 * 參數一: 父類 * 參數二: 新類的名字 * 參數三: 新類的開闢的額外空間 */
✅// 2.1 : 申請類
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
✅// 2.2 : 註冊類
objc_registerClassPair(newClass);
✅// 2.3.1 : 添加class : class的指向是LGPerson,即原來類,進行假裝
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
✅// 2.3.2 : 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
✅// 2.3.3 : 添加dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes);
return newClass;
}
複製代碼
setter
並進行回調因爲咱們重寫了原來類的setter
方法,因此新的setter
方法要咱們本身來實現,道理也很簡單,就是獲取到新值後,發送給父類,即原來類,而後將存儲的Model
取出,進行回調
tatic void lg_setter(id self,SEL _cmd,id newValue){
✅// 根據setter方法,獲取到key,並取出舊值
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
✅// 消息轉發 : 轉發給父類
// 改變父類的值 --- 能夠強制類型轉換
void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// void /* struct objc_super *super, SEL op, ... */
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
lg_msgSendSuper(&superStruct,_cmd,newValue);
✅// 信息數據獲取並回調
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
for (LGInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
複製代碼
#pragma mark - 從get方法獲取set方法的名稱 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragma mark - 從set方法獲取getter方法的名稱 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
複製代碼
銷燬觀察者的這一步,咱們可使用常規的方法,即和系統實現同樣,提供一個移除觀察者的方法,在裏面進行isa
的指回和關聯對象接觸等操做,可是這樣是比較麻煩的,每次註冊後,都須要記得手動去移除,代碼魯棒性差。
因此咱們想到,若是當前對象要進行銷燬了,那麼其中間類天然沒必要存在,這時候就能夠移除觀察者了。因此咱們hook
原來類的dealloc方法,和咱們本身寫的方法進行交換,這樣就能夠添加上咱們本身的邏輯了。
具體代碼以下:
首先在load
方法裏面進行交換,不要等到註冊的時候才進行交換
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self lg_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)];
});
}
+ (BOOL)lg_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
Class cls = self;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!swiMethod) {
return NO;
}
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
return YES;
}
複製代碼
- (void)myDealloc{
✅// 從關聯對象中刪除現有的keyPtah的Model
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}
for (LGInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observerArr.count<=0) {
✅// isa指回給父類
Class superClass = [self class];
object_setClass(self, superClass);
}
[self myDealloc];
}
複製代碼
至此,一個簡單的KVO咱們已經實現好了。可是這個並不完美,由於沒有考慮到多線程等狀況。你們能夠查看臉書的`FBKVOController`開源框架等進行加深學習
KVO
監聽時,系統會修改這個對象的isa
指針指向NSKVONotifying_XXX
類,將對象的isa
指針指向這個子類set
方法;set
方法實現內部會順序調用willChangeValueForKey
方法、原來的setter
方法實現、didChangeValueForKey
方法,而didChangeValueForKey
方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:
監聽方法class
方法,對中間類進行假裝,返回原來類的class
isa
指回原來的類