歡迎閱讀iOS探索系列(按序閱讀食用效果更加)html
在KVC(鍵值編碼)
到KVO(鍵值觀察)
,可能讀者老爺們都用的溜溜的,可是你真的瞭解它嗎?本文就將全方位分析KVO的原理git
KVO(Key-Value Observing)
是蘋果提供的一套事件通知機制,這種機制容許將其餘對象的特定屬性的更改通知給對象。iOS開發者可使用KVO
來檢測對象屬性的變化、快速作出響應,這可以爲咱們在開發強交互、響應式應用以及實現視圖和模型的雙向綁定時提供大量的幫助。github
在Documentation Archieve中提到一句想要理解KVO
,必須先理解KVC
,由於鍵值觀察
是創建在鍵值編碼
的基礎上面試
In order to understand key-value observing, you must first understand key-value coding.——Key-Value Observing Programming Guide數組
而KVO
和NSNotificatioCenter
都是iOS觀察者模式的一種實現,二者的區別在於:安全
KVO
是一對一的,NSNotificatioCenter
是一對多的KVO
對被監聽對象無侵入性,不須要修改其內部代碼便可實現監聽KVO使用三部曲:bash
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
複製代碼
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change);
}
複製代碼
[self.person removeObserver:self forKeyPath:@"name"];
複製代碼
Key-Value Observing Programming Guide
是這麼描述context
的多線程
消息中的上下文指針包含任意數據,這些數據將在相應的更改通知中傳遞迴觀察者;您能夠指定NULL並徹底依賴鍵路徑字符串來肯定更改通知的來源,可是這種方法可能會致使對象的父類因爲不一樣的緣由而觀察到相同的鍵路徑,所以可能會出現問題;一種更安全,更可擴展的方法是使用上下文確保您收到的通知是發給觀察者的,而不是超類的。
這裏提出一個假想,若是父類中有個name
屬性,子類中也有個name
屬性,二者都註冊對name
的觀察,那麼僅經過keyPath
已經區分不了是哪一個name
發生變化了,現有兩個解決辦法:app
object
,顯然爲了知足業務需求而去增長邏輯判斷是不可取的context
傳遞信息,更安全、更可擴展context
使用總結:ide
// context是 void * 類型,應該填 NULL 而不是 nil
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
複製代碼
static void *PersonNameContext = &PersonNameContext;
static void *ChildNameContext = &ChildNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:ChildNameContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == PersonNameContext) {
NSLog(@"%@", change);
} else if (context == ChildNameContext) {
NSLog(@"%@", change);
}
}
複製代碼
也許在平常開發中你以爲是否移除通知都無關痛癢,可是不移除會帶來潛在的隱患
如下是一段沒有移除觀察者的代碼,頁面push先後、鍵值改變先後都很正常
- (void)viewDidLoad {
[super viewDidLoad];
self.child = [FXChild new];
self.child.name = @"Feng";
[self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:ChildNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.child.name = [NSString stringWithFormat:@"%@+", self.child.name];
}
複製代碼
但當把FXChild
以單例
的形式建立後,pop回上一頁再次push進來程序就崩潰了
這是由於沒有移除觀察,單例對象
依舊存在,再次進來時就會報出野指針錯誤
了
移除了觀察者以後便不會發生這種狀況了——移除觀察者是必要的
蘋果官方推薦的方式是——在
init
的時候進行addObserver
,在dealloc
時removeObserver
,這樣能夠保證add
和remove
是成對出現的,這是一種比較理想的使用方式
有時候業務需求須要觀察某個屬性值,一下子要觀察了,一會又不要觀察了...若是把KVO三部曲
總體去掉、再總體添上,必然又是一頓繁瑣而又沒必要要的工做,好在KVO中有兩種辦法能夠手動觸發鍵值觀察:
automaticallyNotifiesObserversForKey
返回NO(能夠只對某個屬性設置)+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
複製代碼
使用willChangeValueForKey
、didChangeValueForKey
重寫被觀察者的屬性的setter
方法
這兩個方法用於通知系統該 key 的屬性值即將和已經變動了
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
複製代碼
兩種方式使用的排列組合以下,能夠自由組合如何使用
狀況 | 回調次數 |
---|---|
正常狀況 | 1 |
automaticallyNotifiesObserversForKey爲NO | 0 |
automaticallyNotifiesObserversForKey爲NO且添加willChangeValueForKey、didChangeValueForKey | 1 |
automaticallyNotifiesObserversForKey爲YES且添加willChangeValueForKey、didChangeValueForKey | 2 |
最近發現[self willChangeValueForKey:name]和[self willChangeValueForKey:"name"]兩種寫法是不一樣的結果:重寫setter方法取屬性值操做不會額外發送通知;而使用「name」會額外發送一次通知
好比有一個下載任務的需求,根據總下載量Total
和當前已下載量Current
來獲得當前下載進度Process
,這個需求就有兩種實現:
總下載量Total
和當前已下載量Current
兩個屬性,其中一個屬性發生變化時計算求值當前下載進度Process
keyPathsForValuesAffectingValueForKey
方法,並觀察process
屬性只要總下載量Total
或當前已下載量Current
任意發生變化,keyPaths=process
就能收到監聽回調
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"process"]) {
NSArray *affectingKeys = @[@"total", @"current"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
複製代碼
但僅僅是這樣還不夠——這樣只能監聽到回調,但尚未完成Process
賦值——須要重寫getter方法
- (NSString *)process {
if (self.total == 0) {
return @"0";
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.current/self.total];
}
複製代碼
如題:FXPerson
下有一個可變數組dataArray
,現觀察之,問點擊屏幕是否打印?
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [FXPerson new];
[self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"dataArray"]) NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person.dataArray addObject:@"Felix"];
}
複製代碼
答:不會
分析:
KVO
是創建在KVC
的基礎上的,而可變數組直接添加是不會調用Setter方法
可變數組dataArray
沒有初始化,直接添加會報錯// 初始化可變數組
self.person.dataArray = @[].mutableCopy;
// 調用setter方法
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"Felix"];
複製代碼
Key-Value Observing Programming Guide
中有一段底層實現原理的敘述
isa-swizzling
技術實現的這段話說的雲裏霧裏的,仍是敲代碼見真章吧
FXPerson
,實例對象isa指向FXPerson
FXPerson
,實例對象isa指向NSKVONotifying_FXPerson
從這兩圖中能夠得出一個結論:觀察者註冊先後FXPerson類
沒發生變化,但實例對象的isa
指向發生變化
那麼這個動態生成的中間類NSKVONotifying_FXPerson
和FXPerson
是什麼關係呢?
在註冊觀察者先後分別調用打印子類的方法——發現NSKVONotifying_FXPerson
是FXPerson
的子類
①首先得明白動態子類觀察的是什麼?下面觀察屬性變量name
和成員變量nickname
來找區別
兩個變量同時發生變化,但只有屬性變量監聽到回調——說明動態子類觀察的是setter
方法
②經過runtime-API
打印一下動態子類和觀察類的方法
- (void)printClassAllMethod:(Class)cls {
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);
}
複製代碼
經過打印能夠看出:
FXPerson類
中的方法沒有改變(imp實現地址沒有變化)NSKVONotifying_FXPerson類
中重寫了父類FXPerson
的dealloc
方法NSKVONotifying_FXPerson類
中重寫了基類NSObject
的class
方法和_isKVOA
方法
class
方法能夠指回FXPerson類
NSKVONotifying_FXPerson類
中重寫了父類FXPerson
的setName
方法
setName
的地址指針不同屬性變量
就重寫一個setter
方法(可自行論證)③dealloc
以後isa指向誰?——指回原類
④dealloc
以後動態子類會銷燬嗎?——不會
頁面pop後再次push進來打印FXPerson類
,子類NSKVONotifying_FXPerson類
依舊存在
⑤automaticallyNotifiesObserversForKey
是否會影響動態子類生成——會
動態子類會根據觀察屬性的automaticallyNotifiesObserversForKey
的布爾值來決定是否生成
automaticallyNotifiesObserversForKey
爲YES
時註冊觀察屬性會生成動態子類NSKVONotifying_XXX
setter
方法setter
方法、dealloc
、class
、_isKVOA
方法
setter
方法用於觀察鍵值dealloc
方法用於釋放時對isa指向進行操做class
方法用於指回動態子類的父類_isKVOA
用來標識是不是在觀察者狀態的一個標誌位dealloc
以後isa
指向元類dealloc
以後動態子類不會銷燬根據KVO的官方文檔和上述結論,咱們將自定義KVO——下面的自定義會有runtime-API的使用和接口設計思路的講解,最終的自定義KVO能知足基本使用的需求但仍不完善。系統的KVO回調和自動移除觀察者都與註冊邏輯分層,自定義的KVO將使用block回調和自動釋放來優化這一點不足
新建一個NSObject+FXKVO
的分類,開放註冊觀察者方法
-(void)fx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(FXKVOBlock)block;
當前觀察值keypath
是否存在/setter方法是否存在一開始想的是判斷屬性是否存在,雖然父類的屬性不會對子類形成影響,可是分類中的屬性雖然沒有setter方法,可是會添加到propertiList
中去——最終改成去判斷setter
方法
if (keyPath == nil || keyPath.length == 0) return;
// if (![self isContainProperty:keyPath]) return;
if (![self isContainSetterMethodFromKeyPath:keyPath]) return;
// 判斷屬性是否存在
- (BOOL)isContainProperty:(NSString *)keyPath {
unsigned int number;
objc_property_t *propertiList = class_copyPropertyList([self class], &number);
for (unsigned int i = 0; i < number; i++) {
const char *propertyName = property_getName(propertiList[i]);
NSString *propertyString = [NSString stringWithUTF8String:propertyName];
if ([keyPath isEqualToString:propertyString]) return YES;
}
free(propertiList);
return NO;
}
/// 判斷setter方法
- (BOOL)isContainSetterMethodFromKeyPath:(NSString *)keyPath {
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
NSLog(@"沒找到該屬性的setter方法%@", keyPath);
return NO;
}
return YES;
}
複製代碼
automaticallyNotifiesObserversForKey
方法返回的布爾值BOOL isAutomatically = [self fx_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
if (!isAutomatically) return;
// 動態調用類方法
- (BOOL)fx_performSelectorWithMethodName:(NSString *)methodName keyPath:(id)keyPath {
if ([[self class] respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
BOOL i = [[self class] performSelector:NSSelectorFromString(methodName) withObject:keyPath];
return i;
#pragma clang diagnostic pop
}
return NO;
}
複製代碼
class
方法指向原先的類// 動態生成子類
Class newClass = [self createChildClassWithKeyPath:keyPath];
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@", kFXKVOPrefix, oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重複建立生成新類
if (newClass) return newClass;
// 申請類
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 註冊類
objc_registerClassPair(newClass);
// class的指向是FXPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)fx_class, classTypes);
return newClass;
}
複製代碼
isa
的值指向動態子類object_setClass(self, newClass);
複製代碼
因爲可能會觀察多個屬性值,因此以屬性值-模型
的形式一一保存在數組中
typedef void(^FXKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface FXKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) FXKVOBlock handleBlock;
@end
@implementation FXKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(FXKVOBlock)block {
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
// 保存信息
FXKVOInfo *info = [[FXKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
複製代碼
往動態子類添加setter
方法
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
// 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)fx_setter, setterTypes);
return newClass;
}
複製代碼
setter方法的具體實現
static void fx_setter(id self,SEL _cmd,id newValue) {
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// 改變父類的值 --- 能夠強制類型轉換
void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
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)(kFXKVOAssiociateKey));
for (FXKVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
複製代碼
往動態子類添加dealloc
方法
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
// 添加dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)fx_dealloc, deallocTypes);
return newClass;
}
複製代碼
因爲頁面釋放時會釋放持有的對象,對象釋放時會調用dealloc,如今往動態子類的dealloc方法名中添加實現將isa指回去,從而在釋放時就不會去找父類要方法實現
static void fx_dealloc(id self, SEL _cmd) {
Class superClass = [self class];
object_setClass(self, superClass);
}
複製代碼
但僅僅是這樣仍是不夠的,只把isa指回去,但對象不會調用真正的dealloc方法,對象不會釋放
出於這種狀況,根據iOS探索 runtime面試題分析講過的方法交換進行一波操做
NSObject
的dealloc實現與fx_dealloc
進行方法交換dealloc
進行釋放+load
方法中進行交換,一是由於效率低,二是由於會影響到全部類- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
// 添加dealloc
// SEL deallocSEL = NSSelectorFromString(@"dealloc");
// Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
// const char *deallocTypes = method_getTypeEncoding(deallocMethod);
// class_addMethod(newClass, deallocSEL, (IMP)fx_dealloc, deallocTypes);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self FXMethodSwizzlingWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(fx_dealloc)];
});
return newClass;
}
- (void)fx_dealloc {
Class superClass = [self class];
object_setClass(self, superClass);
[self fx_dealloc];
}
複製代碼
就這樣自定義KVO將KVO三部曲用block形式合成一步
本文demo、J_Knight_寫的SJKVOController及FBKVO(建議看看這個成熟的自定義KVO)
最近在掘金上看到一個沸點——「不少人明白原理,但到了真正敲代碼的時候就不會了」
學習如同踩坑爬坑,有些坑看過別人踩過,本身不去嘗試過都不知道是怎麼回事。或許你會有抓耳撓腮迷惑的時候,可是你不去解決困難,困難永遠會擋在你成長的路上
你要悄悄拔尖,而後驚豔全部人🌺——————與君共勉