iOS底層探索 -- KVO探索
前言
上一篇學習了KVC鍵值編碼
的查詢原理,而KVO(Key-Value Observing)
在開發中也是用的比較多。本篇我們深入底層探索一下KVO
的底層原理。
1. KVO初探
首先,先看一下,平常我們是怎麼寫KVO
進行鍵值觀察的
如在某個類中,有一個 LGPerson
型別的屬性person
,在這個類中對person
的name
屬性進行觀察,程式碼如下:
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@" ,change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
}
複製程式碼
這些都是我很熟悉的,但是要注意的是:新增的觀察者,一定要及時移除,否則,當物件釋放後,會造成野指標等問題。
接下來看一下KVO
的一些細節問題。
1.1 context 的作用
檢視新增觀察者
的API
,
addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
前面三個引數,我們都很熟悉,那麼最後一個void *
型別的上下文context
,有什麼用呢?
在平常的開發中,我們習慣的給傳NULL
,那麼我們思考一個問題,
假如在一個類中,要對多個物件的同名屬性進行觀察,
比如:LGStudent
繼承自LGPerson
,而我們要在同一個類中對這個兩個屬性的name
進行觀察,我們會怎麼做呢?
// ✅ 新增觀察者
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self for KeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
// ✅ 監聽變化
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@",change);
if (object == self.person) {
if ([keyPath isEqualToString:@"name"]) {
// 邏輯
}
} else if(object == self.student){
if ([keyPath isEqualToString:@"name"]) {
// 邏輯
}
}
}
// ✅ 移除
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.student removeObserver:self forKeyPath:@"name"];
}
複製程式碼
這時我們就需要在監聽變化的方法中,寫很多判斷條件,然後來處理邏輯,這樣會很繁瑣。
通過檢視檔案,其實我們可以在新增觀察者
的時候,為每個觀察到的鍵路徑建立一個不同的上下文,從而完全不需要進行字串比較,從而可以更有效地進行通知解析,這是一個更加安全、更加便利的方式
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
複製程式碼
1.2 自動觀察
在開發中,我們也會經常遇到一種情況,比如:需求頻繁改動,導致我們對某個屬性的觀察頻繁的刪除,然後重新,很是繁瑣。
其實,我們可以在被觀察的類中(比如上面對self.person
的name
觀察,可以寫在LGPerson
中),寫下面的程式碼,
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
複製程式碼
這個方法預設為YES
,我們可以設定為NO
,此時新增的觀察者會失效,在被觀察屬性發生變化是,需要手動通過兩個方法(willChangeValueForKey:
和didChangeValueForKey:
)進行觀察
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
// 對 person 的 name 進行觀察
...
// 當 name 發生改變時
[self.person willChangeValueForKey:@"name"];
self.person.name = @"null";
[self.person didChangeValueForKey:@"name"];
複製程式碼
我們還可以通過下面的方式,對某個Key
判斷,進而設定自動還是手動,設定為NO
的將不會被觀察。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
複製程式碼
也可以在被觀察屬性的setter方法
中,呼叫這兩個方法,進行手動觀察
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
複製程式碼
1.3 多個因素影響
在開發中,也會遇到進度條的應用場景,而當前進度的佔比,是受兩個因素控制(當前下載量和總下載量)的,
比如下面的示例:
@interface LGPerson : NSObject
@property (nonatomic,copy) NSString *downloadProgress;
@property (nonatomic,assign) double writtenData;
@property (nonatomic,assign) double totalData;
@end
#import "LGPerson.h"
@implementation LGPerson
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData",@"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (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];
}
@end
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
}
複製程式碼
在新增觀察者時,不光有正常的三部,還需要新增一個keyPathsForValuesAffectingValueForKey
方法。
1.4 可變陣列的觀察
在對可變陣列進行觀察時,對陣列進行修改時,不能通過呼叫addObject
方法新增元素,應該通過下面的方式。
// 陣列變化不能通過這種方式
// [self.person.dateArray addObject:@"1"];
// KVO 建立在 KVC
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
複製程式碼
因為KVO
是基於KVC
的,而KVO
的觀察是通過setter
,可變陣列的的獲取是通過mutableArrayValueForKey
方法,不是像其他的setValeu:forKey
方法。
2. KVO 原理分析
首先,我們定義一個LGPerson
,LGPerson
中定義一個nickName
的屬性。
然後對其觀察。
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic,copy) NSString *nickName;
- (void)sayHello;
- (void)sayLove;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)setNickName:(NSString *)nickName{
_nickName = nickName;
}
- (void)sayHello{
}
- (void)sayLove{
}
@end
複製程式碼
在新增觀察者前後斷點除錯,
分別列印self.person
的類,如下:
發現在新增之後,self.person
的類發生了變化,變成了NSKVONotifying_LGPerson
檢視官方檔案,KVO
底層,是對ias
進行了swizzling
。使物件的isa
由原來的類指向了派生出來的NSKVONotifying_xxx
類
Automatic key-value observing is implemented using a technique called isa-swizzling.
複製程式碼
那麼LGPerson
和NSKVONotifying_LGPerson
是什麼關係呢?
我們通過RunTime API
列印LGPerson
父類的所有子類。
- (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);
}
複製程式碼
在新增觀察前後,列印結果如下:
由此可見,NSKVONotifying_LGPerson
同樣繼承自LGPerson
的元類。
我們知道,對一般型別的屬性進行觀察時,是觀察的這個屬性的setter
,當新增觀察者時,會動態建立一個NSKVONotifying_xxx
的類,那麼這個類中是否會對元類中的方法進行重寫呢?
列印方法列表如下:
從列印結果可以看出,動態建立的類中,重寫了setNickName:
、class
、
delloc
和_isKVOA
然後在觀察者銷燬時,將isa
指向原來的類,在delloc
方法中,列印self.person
類:
那麼,動態生成的NSKVONotifying_xxx
是否釋放了呢?
答案是否定的,因為動態子類,建立成本太高,銷燬了會很浪費,而不銷燬方便下次快捷的使用
小結:
1: 動態生成子類 : NSKVONotifying_xxx
2: 觀察的是 setter
3: 動態子類重寫了很多方法 setNickName (setter) class dealloc _isKVOA
4: 移除觀察的時候 isa 指向回來
5: 動態子類不會銷燬(建立成本太高,不釋放,方便下次使用)
複製程式碼
3. 自定義 KVO 思路
系統的KVO
是NSObject
的一個分類NSObject(NSKeyValueObserving)
,凡是繼承自NSObject
的類,都可以使用KVO
。
那麼接下來,嘗試自定義一個簡單的KVO
。
首先,可以自定義一點新增觀察者的方法,在這個方法中
1. 動態建立 NSKVONotifying_xxx 類。為了防止錯誤,我們可以先檢測被觀察的 keyPath 是否有 setter
2. 交換 isa 的指向,指向 NSKVONotifying_xxx
複製程式碼
- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
// ✅ 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];
}
複製程式碼
動態建立子類
- (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;
}
static void lg_dealloc(id self,SEL _cmd){
}
static void lg_setter(id self,SEL _cmd,id newValue){
NSLog(@"來了:%@",newValue);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// ✅4: 訊息轉發 : 轉發給父類
// 改變父類的值 --- 可以強制型別轉換
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)),};
//objc_msgSendSuper(&superStruct,_cmd,newValue)
lg_msgSendSuper(&superStruct,newValue);
// ✅ 5: 資訊資料回撥
// ✅ 拿到觀察者
NSMutableArray *mArray = objc_getAssociatedObject(self,(__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
for (LGKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0,0),^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
// 對新舊值進行處理
if (info.options & LGKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & LGKeyValueObservingOptionOld) {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
}
}
// ✅ 2: 訊息傳送給觀察者
SEL observerSEL = @selector(lg_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
});
}
}
}
Class lg_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
複製程式碼
自定義移除觀察者這方法
- (void)ll_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
NSMutableArray *observerArr = objc_getAssociatedObject(self,(__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}
for (LGKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self,observerArr,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
// ✅ 將指標指回
if (observerArr.count<=0) {
// 指回給父類
Class superClass = [self class];
object_setClass(self,superClass);
}
}
複製程式碼
自定義監聽方法
- (void)ll_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey,id> *)change context:(nullable void *)context{
}
複製程式碼
4. KVO 函式語言程式設計
在自定義新增KVO
時,我們可以加入函式語言程式設計的思想,在新增觀察者的時候,定義一個回撥block
,直接在被觀察屬性發生變化時,呼叫block
,將其傳回。這樣就不用在寫監聽方法了。
對上面的lg_setter
修改,將傳送訊息,改成回撥block
- (void)ll_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,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
}
static void lg_setter(id self,newValue);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// 4: 訊息轉發 : 轉發給父類
// 改變父類的值 --- 可以強制型別轉換
void (*lg_msgSendSuper)(void *,newValue);
// 5: 資訊資料回撥
NSMutableArray *mArray = objc_getAssociatedObject(self,(__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
for (LGInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
// 回撥block
info.handleBlock(info.observer,oldValue,newValue);
}
}
}
// 新增觀察者
[self.person ll_addObserver:self forKeyPath:@"nickName" block:^(id _Nonnull observer,NSString * _Nonnull keyPath,id _Nonnull oldValue,id _Nonnull newValue) {
NSLog(@"%@-%@",newValue);
}];
複製程式碼
其實,還可以將觀察者的指標指向原來的類的操作放到手動新增的delloc
的實現中,這樣就可以自動釋放,不需要再呼叫移除方法
static void lg_dealloc(id self,SEL _cmd){
Class superClass = [self class];
object_setClass(self,superClass);
}
複製程式碼
小結
1. 驗證是否存在 setter ,不讓例項變數儘量
2. 動態生成子類
2.1 動態開闢一個新類 (NSKVONotifying_xxx)
2.2 註冊類
2.3 新增 class 方法,setter 方法,dealloc 方法
2.4 使用關聯物件,儲存觀察者
3. 修改 ISA 是指向,指向動態生成的類
4. 在重寫的 setter 中
4.1 訊息轉發給父類,呼叫父類的 setter,給一種什麼也沒幹的假象
4.2 通過傳送訊息,呼叫監聽方法(observeValueForKeyPath:)
或者通過響應式程式設計,回撥block
複製程式碼
5. FBKVO 簡單分析
上面我們通過自定義KVO
對KVO
底層原理有了一個系統的瞭解,其中肯定存在問題,而在一些開源網站上,有很多大牛自己封裝的KVO
。
接下來,簡單的瞭解一下FBKVO
FBKVO
封裝了一個FBKVOController
的中間層,添加了函式語言程式設計的思想,可以通過下面的形式呼叫。
[self.kvoCtrl observe:self.person keyPath:@"age" options:0 action:@selector(lg_observerAge)];
[self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer,id _Nonnull object,NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"****%@****",change);
}];
[self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer,change);
}];
複製程式碼
在FBKVOController
中,統一新增觀察者,統一處理,統一銷燬,如下圖,解決了迴圈引用的問題