感覺(jué)很久都沒(méi)有寫新文章了,還是寫一些小文章吧,記錄一下也好!
這一次和大家講解的是KVO -- key value observing,想必大家也不陌生,項(xiàng)目中也會(huì)有用到的地方,網(wǎng)上相關(guān)的文章也不少,我在這里的目的就是為了班門弄斧??,哈哈哈...。但是大家有沒(méi)有研究過(guò)KVO是怎么實(shí)現(xiàn)的呢?我本著厚顏無(wú)恥的心態(tài)為大家講解一下。
前言
KVO的監(jiān)聽,實(shí)際上是通過(guò)運(yùn)行時(shí)runtime實(shí)現(xiàn)的,并且對(duì)被監(jiān)聽的對(duì)象,動(dòng)態(tài)創(chuàng)建其子類,重寫setter、class、等方法,改變其isa指針的指向,企圖瞞騙我們。想一想都覺(jué)得好過(guò)分??
雖然我們不知道其內(nèi)部的實(shí)現(xiàn),但是通過(guò)猜想,說(shuō)錯(cuò)了,不是猜想,是實(shí)踐。首先我們來(lái)看看添加觀察后,過(guò)程發(fā)生了什么。這里已經(jīng)有大神做過(guò)分析了,可以先看看這里
我也簡(jiǎn)單的給大家講解下
@interface TestObject : NSObject
@property (nonatomic,assign) int z; // 被觀察的屬性z
@end
@implementation TestObject
@end
@interface ViewController ()
@end
//MARK: - 獲取方法列表
static NSArray *ClassMethodNames(Class c)
{
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++)
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
free(methodList);
return array;
}
//MARK: - 打印對(duì)象信息
static void PrintDescription(NSString *name, id obj)
{
NSString *str = [NSString stringWithFormat:
@"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 創(chuàng)建 x、y對(duì)象
TestObject *x = [TestObject new];
TestObject *y = [TestObject new];
// 觀察 y 對(duì)象
[y addObserver:y forKeyPath:@"z" options:0 context:nil];
// 打印x、y對(duì)象有什么不同
PrintDescription(@"x", x);
PrintDescription(@"y", y);
}
以下為打印信息
x: <TestObject: 0x604000017b90>
NSObject class TestObject
libobjc class TestObject
implements methods <z, setZ:>
y: <TestObject: 0x604000017c50>
NSObject class TestObject
libobjc class NSKVONotifying_TestObject
implements methods <setZ:, class, dealloc, _isKVOA>
從結(jié)果我們可以看出,被觀察的對(duì)象重寫了 setter class dealloc 還有一個(gè)神秘的_isKVOA方法,原來(lái)的TestObject類也發(fā)生了改變,變成了NSKVONotifying_TestObject,這樣子我們就可以斷定對(duì)象方法class被覆蓋了override,但我們可以通過(guò)runtime的object_getClass()獲取其真正的類名,而且[obj class]其內(nèi)部實(shí)現(xiàn)就是下面的樣子??
- (Class)class {
return object_getClass(self);
}
你還會(huì)發(fā)現(xiàn),當(dāng)你重寫TestObject的class方法,這方法已經(jīng)不走了,原因就是被重寫了。來(lái),讓我們?cè)僦貙懸恍┓椒?,看?code>Apple還做了什么事情。
@interface TestObject : NSObject
@property (nonatomic,assign) int z;
@end
@implementation TestObject
- (void)setZ:(int)z {
_z = z;
NSLog(@"setter z:%d",z);
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey:%@",key);
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey:%@",key);
}
- (Class)class {
NSLog(@"not override:%@",self);
return object_getClass(self);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
TestObject *x = [TestObject new];
TestObject *y = [TestObject new];
[y addObserver:y forKeyPath:@"z" options:0 context:nil];
x.z = 1;
NSLog(@"separation line");
y.z = 1;
[x class];
NSLog(@"separation line1");
[y class];
}
}
2018-07-19 11:17:04.989201+0800 KVO_demo[79056:7685866] setter z:1
2018-07-19 11:17:04.989368+0800 KVO_demo[79056:7685866] separation line
2018-07-19 11:17:04.989613+0800 KVO_demo[79056:7685866] willChangeValueForKey:z
2018-07-19 11:17:04.989775+0800 KVO_demo[79056:7685866] setter z:1
2018-07-19 11:17:04.990139+0800 KVO_demo[79056:7685866] didChangeValueForKey:z
2018-07-19 11:17:04.990517+0800 KVO_demo[79056:7685866] not override:<TestObject: 0x60000001acf0>
2018-07-19 11:17:05.007865+0800 KVO_demo[79056:7685866] separation line1
這次我們重寫了4個(gè)方法
- (void)setZ:(int)z
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
- (Class)class
通過(guò)觀察打印信息,我們可以發(fā)現(xiàn)被觀察的對(duì)象,在賦值之前會(huì)先走- (void)willChangeValueForKey:(NSString *)key,然后是- (void)setZ:(int)z,最后走到- (void)didChangeValueForKey:(NSString *)key,但并沒(méi)有走- (Class)class,而 x 對(duì)象只走了 setter方法和class方法。這樣子我們就可以看出差別了,y 對(duì)象的class方法確實(shí)被覆蓋了。不過(guò)細(xì)心的你是不是發(fā)現(xiàn)了一些問(wèn)題,上面不是說(shuō)過(guò)setter方法也被覆蓋重寫了嗎?那為什么 y 對(duì)象的setter方法還會(huì)走,有貓膩啊??,這個(gè)我們下面會(huì)說(shuō)到,看官別急。
在這里還要提醒一下大家,當(dāng)你重寫override了 -(void)willChangeValueForKey:(NSString *)key ;- (void)didChangeValueForKey:(NSString *)key其中的一個(gè)方法時(shí),下面監(jiān)聽的回調(diào)方法就不會(huì)再調(diào)用的了。
@interface NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end
而且此回調(diào)方法是根據(jù)添加的觀察者是誰(shuí)addObserver:observer,誰(shuí)就會(huì)接收到通知。
小結(jié)
根據(jù)以上的分析和實(shí)踐,我們大致了解KVO一個(gè)看得見(jiàn)的實(shí)現(xiàn)流程,下面我們來(lái)模擬一下KVO的實(shí)現(xiàn)。還原案發(fā)現(xiàn)場(chǎng)罒ω罒。
先上代碼
#import <Foundation/Foundation.h>
typedef NSString *CCKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeNewKey;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeOldKey;
@interface NSObject (Override_KVO)
/**
對(duì)象方法,誰(shuí)調(diào)用 表明 誰(shuí)就是被觀察的對(duì)象
@param observer 觀察者
@param key 被觀察對(duì)象的屬性
@param handler block回調(diào)
*/
- (void)cc_addObserver:(id)observer
key:(NSString *)key
handler:(void(^)(id observer, NSString *key, NSDictionary<CCKeyValueChangeKey, id> *dic))handler;
/**
回調(diào)方法
@param key 被觀察對(duì)象的屬性
@param object 觀察者
@param change 變化前后的值
*/
- (void)cc_observeValueForKey:(NSString *)key object:(id)object change:(NSDictionary<CCKeyValueChangeKey,id> *)change;
/**
移除觀察者
@param observer 觀察者
@param key 被觀察對(duì)象的屬性
*/
- (void)removeObserver:(id)observer key:(NSString *)key;
@end
我們先來(lái)看一下頭文件的一些屬性和方法
typedef NSString *CCKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeNewKey;
FOUNDATION_EXPORT CCKeyValueChangeKey const CCKeyValueChangeOldKey;
這里是參考Apple的實(shí)現(xiàn)
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
至于這個(gè)typedef NSString *CCKeyValueChangeKey和這個(gè)FOUNDATION_EXPORT有什么用,請(qǐng)百度一下吧,這里就不細(xì)說(shuō)??,往下還有很多知識(shí)點(diǎn),根本說(shuō)不完。簡(jiǎn)單理解就是自定義字符串。
我們先看一下第一個(gè)方法的實(shí)現(xiàn)
- (void)cc_addObserver:(id)observer key:(NSString *)key handler:(void (^)(id, NSString *, NSDictionary<CCKeyValueChangeKey,id> *))handler {
SEL setterMethod = NSSelectorFromString(setterNameFunc(key));
Class observerClass = object_getClass(self); // 獲取類名
NSString *classString = NSStringFromClass(observerClass); // 轉(zhuǎn)字符串
// 判斷是否已創(chuàng)建子類
if (![classString hasPrefix:cc_keyValueNotifiy]) {
Class subClass = [self createSubClass:observerClass];
object_setClass(self, subClass); // 原類轉(zhuǎn)換為子類
}
// 沒(méi)有 setter 方法 就添加
if (![self isExistSetterFunc:setterMethod]) {
Method method = class_getInstanceMethod(object_getClass(self), setterMethod);
const char *type = method_getTypeEncoding(method);
class_addMethod(object_getClass(self), setterMethod, (IMP)cc_setter, type);
}
// 創(chuàng)建回調(diào)對(duì)象
CCObserver *temp = [[CCObserver alloc] initObserver:observer key:key handler:handler];
// 關(guān)聯(lián)數(shù)組,存儲(chǔ)對(duì)象
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
if (!array) {
array = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge void *)cc_associationKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[array addObject:temp];
}
在添加觀察者的時(shí)候,我們先判斷一下是否已經(jīng)新建了子類,沒(méi)有的話我們就創(chuàng)建其子類,改變其isa的指向,下面我們看一看代碼
Class subClass = [self createSubClass:observerClass];
//MARK: - 創(chuàng)建子類
- (Class)createSubClass:(Class)obsererClass {
NSString *classString = NSStringFromClass(obsererClass);
NSString *subString = [NSString stringWithFormat:@"%@%@",cc_keyValueNotifiy,classString]; // 拼接
Class subClass = NSClassFromString(subString); // 轉(zhuǎn)換為 class
// 不是nil 證明已存在該類,直接返回
if (subClass) {
return subClass;
}
subClass = objc_allocateClassPair(obsererClass, subString.UTF8String, 0); // 創(chuàng)建新的類
Method method = class_getInstanceMethod(obsererClass, @selector(class));
const char * type = method_getTypeEncoding(method);
class_addMethod(subClass, @selector(class), (IMP)override_classFunc, type); // 添加新的class方法 以重載原class方法
objc_registerClassPair(subClass); // 創(chuàng)建新的類后,注冊(cè)該類,使添加的方法 屬性生效
return subClass;
}
我們通過(guò)runtime的objc_allocateClassPair動(dòng)態(tài)的創(chuàng)建一個(gè)新的類,順便看一下官方文檔的說(shuō)明,需要什么參數(shù),每個(gè)參數(shù)的作用。
superclass: 這個(gè)參數(shù)將作為新類的父類,如果是nil,那么會(huì)創(chuàng)建一個(gè)新的根類。
name:新類的名字
extraBytes分配字節(jié)數(shù),通常為 0
如果已經(jīng)存在該類,那么新建的類就會(huì)失敗,返回nil,不過(guò)上面的代碼已經(jīng)做了判斷了。
/**
* Creates a new class and metaclass.
*
* @param superclass The class to use as the new class's superclass, or \c Nil to create a new root class.
* @param name The string to use as the new class's name. The string will be copied.
* @param extraBytes The number of bytes to allocate for indexed ivars at the end of
* the class and metaclass objects. This should usually be \c 0.
*
* @return The new class, or Nil if the class could not be created (for example, the desired name is already in use).
*
* @note You can get a pointer to the new metaclass by calling \c object_getClass(newClass).
* @note To create a new class, start by calling \c objc_allocateClassPair.
* Then set the class's attributes with functions like \c class_addMethod and \c class_addIvar.
* When you are done building the class, call \c objc_registerClassPair. The new class is now ready for use.
* @note Instance methods and instance variables should be added to the class itself.
* Class methods should be added to the metaclass.
*/
OBJC_EXPORT Class _Nullable
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,
size_t extraBytes)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
新類已經(jīng)創(chuàng)建完了,我們繼續(xù)通過(guò)runtime的class_addMethod,動(dòng)態(tài)添加新的方法,以覆蓋原有的class方法
Method method = class_getInstanceMethod(obsererClass, @selector(class));
const char * type = method_getTypeEncoding(method);
class_addMethod(subClass, @selector(class), (IMP)override_classFunc, type);
最后注冊(cè)新的類,上面的文檔也提到了,當(dāng)創(chuàng)建完新的類,為其設(shè)置類的屬性和函數(shù),最后調(diào)用objc_registerClassPair進(jìn)行注冊(cè),以告訴編譯器新的類已經(jīng)準(zhǔn)備好使用。
有getClass自然就有setClass,創(chuàng)建完新的類后,沒(méi)錯(cuò),要變身了,請(qǐng)后退。object_setClass可以把對(duì)象設(shè)置為任何類,很好很強(qiáng)大的。而且你會(huì)發(fā)現(xiàn)我們很多的類都是這樣子設(shè)計(jì)的,這里先舉個(gè)??NSNumber,先說(shuō)這么多,我們繼續(xù)吧。
object_setClass(self, subClass); // 改變`isa`的指向
新類創(chuàng)建完了,外衣也做好了,接下來(lái)我們?yōu)樾骂愒賱?dòng)態(tài)添加一個(gè)方法,以重寫父類的setter方法,并且把相關(guān)信息存儲(chǔ)到數(shù)組里面,繼續(xù)以runtime的 objc_setAssociatedObject關(guān)聯(lián)起來(lái),又是一個(gè)知識(shí)點(diǎn)。還有我們?yōu)槭裁匆陆ㄒ粋€(gè)對(duì)象來(lái)存儲(chǔ)信息呢,這里是為了后續(xù)的回調(diào)使用的,而且這樣子包一層,也為了防止強(qiáng)引用observer,而造成內(nèi)存無(wú)法釋放。數(shù)組會(huì)強(qiáng)引用其對(duì)象,所以不宜直接使用。
if (![self isExistSetterFunc:setterMethod]) {
Method method = class_getInstanceMethod(object_getClass(self), setterMethod);
const char *type = method_getTypeEncoding(method);
class_addMethod(object_getClass(self), setterMethod, (IMP)cc_setter, type);
}
// 創(chuàng)建回調(diào)對(duì)象
CCObserver *temp = [[CCObserver alloc] initObserver:observer key:key handler:handler];
// 關(guān)聯(lián)數(shù)組,存儲(chǔ)對(duì)象
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
if (!array) {
array = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge void *)cc_associationKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[array addObject:temp];
我們繼續(xù)看一下重寫的setter 又做了什么,來(lái)看下面的代碼。我們?cè)谫x值前先調(diào)用其NSObject分類方法willChangeValueForKey:,之后獲取原來(lái)的值,創(chuàng)建一個(gè)結(jié)構(gòu)體struct objc_super,又是一個(gè)知識(shí)點(diǎn),這里最好能理解super跟self之間的關(guān)系,例如:[[super alloc] init]這段代碼,意思是調(diào)用父類的方法分配內(nèi)存、初始化,但是其接收接者是self。
((void (*)(id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&superClass), _cmd, (id)newValue);沒(méi)錯(cuò),這里的做法是為了讓原類TestObject的setter方法生效,而這里為什么用objc_msgSendSuper而不是objc_msgSend,我想大家應(yīng)該明白了吧,這里也很好的解釋了上文提到的 setter方法為什么重寫了還會(huì)走,因?yàn)橛酶割?code>[super setXXX:]了。
//MARK: - 重載 setter 方法
static void cc_setter(id self, SEL _cmd, id newValue) {
NSString *setterName = NSStringFromSelector(_cmd);
NSString *key = getterName(setterName);
[self willChangeValueForKey:key]; // 賦值前 調(diào)用 willChangeValueForKey
id oldValue = [self valueForKey:key]; // 獲取 舊值
// 創(chuàng)建結(jié)構(gòu)體
struct objc_super superClass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
// 使原類 setter 方法生效
((void (*)(id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&superClass), _cmd, (id)newValue);
// 獲取存儲(chǔ)的數(shù)組
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
for (CCObserver *observer in array) {
NSDictionary *dic = @{CCKeyValueChangeOldKey:oldValue, CCKeyValueChangeNewKey:newValue};
// [observer.observer cc_observeValueForKey:key object:observer.observer change:dic];.
!observer.handler ? : observer.handler(observer.observer, key, dic); // 執(zhí)行回調(diào)
}
[self didChangeValueForKey:key]; // 賦值后 調(diào)用 didChangeValueForKey
}
謎底基本都差不多揭開了,最后我們來(lái)把結(jié)果回調(diào)一下,把存儲(chǔ)起來(lái)的對(duì)象進(jìn)行回調(diào)。注釋掉的那段代碼是模仿原有的回調(diào),這里我們以block形式回調(diào)。
// 獲取存儲(chǔ)的數(shù)組
NSMutableArray *array = objc_getAssociatedObject(self, (__bridge void *)cc_associationKey);
for (CCObserver *observer in array) {
NSDictionary *dic = @{CCKeyValueChangeOldKey:oldValue, CCKeyValueChangeNewKey:newValue};
// [observer.observer cc_observeValueForKey:key object:observer.observer change:dic];.
!observer.handler ? : observer.handler(observer.observer, key, dic); // 執(zhí)行回調(diào)
}
總結(jié)
KVO涉及到知識(shí)點(diǎn)非常多,而且很多是runtime,對(duì)這方面的薄弱的同學(xué)很可能看不懂,需要多看多讀多寫,反復(fù)消化,這段代碼為什么這么寫,而不這么寫,都需要認(rèn)真理解后才知道。這里還可以延伸很多其他的知識(shí)。消息的轉(zhuǎn)發(fā)、通過(guò)關(guān)聯(lián)值為分類添加屬性、動(dòng)態(tài)添加方法、類的指向及其結(jié)構(gòu)、繼承、分類的作用...,多得不要不要的。而我們這次的實(shí)踐也很好的證明了KVO確實(shí)是通過(guò)runtime,動(dòng)態(tài)創(chuàng)建一個(gè)新類作為原對(duì)象的子類,把isa指針指向新類,并對(duì)其重寫setter方法,以監(jiān)聽屬性值的變化來(lái)通知外界。
下一次分享什么好呢,我自己都很期待??
本次的demo
示例及demo的參考來(lái)源及文章
iOS開發(fā)·KVO用法,原理與底層實(shí)現(xiàn)
How Key-Value Observing (KVO) is actually implemented at the runtime level