上一篇文章iOS-底層原理20:KVC底層原理中了解了KVC
底層原理筋蓖,本文將講解KVO
底層原理蒸播。
結(jié)合上一篇文章有個(gè)問題
問題:
通過KVC修改屬性會(huì)觸發(fā)KVO嗎
帶著問題開始探索
1. 定義
KVO
迫皱,全稱為Key-Value observing
创橄,中文名為鍵值觀察
询刹,KVO是一種機(jī)制棋恼,它允許將其他對象的指定屬性的更改通知給對象
。官方文檔
2. KVO 使用注意事項(xiàng)
2.1 基本使用
KVO的基本使用主要分為3步:
step1:
向觀察對象注冊觀察者 addObserver:forKeyPath:options:context:
- Options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01, //提供更改后的值
NSKeyValueObservingOptionOld = 0x02, //提供更改前的值
NSKeyValueObservingOptionInitial, //初始值脂崔,在注冊觀察服務(wù)時(shí)會(huì)調(diào)用一次觸發(fā)方法
NSKeyValueObservingOptionPrior //分別在值修改前后觸發(fā)方法滤淳,NSKeyValueChangeNotificationIsPriorKey可以來判斷在前調(diào)用還是在后調(diào)用
}
- Context
翻譯成中文
大致含義就是:addObserver:forKeyPath:options:context:
方法中的上下文context指針包含任意數(shù)據(jù)
,這些數(shù)據(jù)將在相應(yīng)的更改通知中傳遞回觀察者砌左〔备溃可以通過指定context為NULL
,從而依靠keyPath
即鍵路徑字符串傳來確定更改通知的來源
汇歹,但是這種方法可能會(huì)導(dǎo)致對象的父類由于不同的原因也觀察到相同的鍵路徑而導(dǎo)致問題
屁擅。所以可以為每個(gè)觀察到的keyPath創(chuàng)建一個(gè)不同的context,從而完全不需要進(jìn)行字符串比較产弹,從而可以更有效地進(jìn)行通知解析
通俗的講派歌,context上下文主要是用于區(qū)分不同對象的同名屬性
,從而在KVO回調(diào)方法中可以直接使用context進(jìn)行區(qū)分痰哨,可以大大提升性能胶果,以及代碼的可讀性
context使用總結(jié)
- 不使用context,使用keyPath區(qū)分通知來源
//context的類型是 nullable void *斤斧,應(yīng)該是NULL早抠,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
- 使用context區(qū)分通知來源
//定義context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
//注冊觀察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
//KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}
step2:
接收更改通知消息 observeValueForKeyPath:ofObject:change:context:
- change
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, //賦值
NSKeyValueChangeInsertion = 2, //添加值
NSKeyValueChangeRemoval = 3, //移除值
NSKeyValueChangeReplacement = 4, //替換值
};
//kind,表示動(dòng)作的種類撬讽,值為上面這個(gè)枚舉
NSKeyValueChangeKey const NSKeyValueChangeKindKey;
//new, 更改后的值
NSKeyValueChangeKey const NSKeyValueChangeNewKey;
//old, 更改前的值
NSKeyValueChangeKey const NSKeyValueChangeOldKey;
//indexes蕊连,數(shù)組等改變的index信息
NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
//notificationIsPrior,這個(gè)用于NSKeyValueObservingOptionPrior用于區(qū)分是更改前的觸發(fā)通知還是更改后的觸發(fā)通知
NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
step3:
注銷觀察者 removeObserver:forKeyPath:context:
刪除觀察者時(shí)游昼,請記住以下幾點(diǎn):
如果觀察者未注冊調(diào)用移除將會(huì)導(dǎo)致
NSRangeException
甘苍。 成對調(diào)用addObserver:forKeyPath:options:context:
與removeObserver:forKeyPath:context:
,或?qū)?code>removeObserver:forKeyPath:context:放入try/catch塊
內(nèi)處理潛在的異常烘豌。釋放后羊赵,
觀察者不會(huì)自動(dòng)將其自身移除
。被觀察對象繼續(xù)發(fā)送通知,而忽略了觀察者的狀態(tài)昧捷。但是闲昭,與發(fā)送到已釋放對象的任何其他消息一樣,更改通知會(huì)觸發(fā)內(nèi)存訪問異常
靡挥。因此序矩,你可以確保觀察者在從內(nèi)存中消失之前將自己刪除
。該協(xié)議無法詢問對象是觀察者還是被觀察者跋破。構(gòu)造代碼以避免發(fā)布相關(guān)的錯(cuò)誤簸淀。一種典型的模式是在觀察者初始化期間(例如,
在init或viewDidLoad中
)注冊為觀察者
毒返,并在釋放過程中(通常在dealloc中
)注銷
租幕,以確保成對和有序地添加和刪除消息
,并確保觀察者在注冊之前被取消注冊拧簸,從內(nèi)存中釋放出來
劲绪。
移除觀察者Demo
step1:
新建一個(gè)LBHPerson
類和一個(gè)LBHStudent
類
/********LBHPerson********/
//.h
@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@end
//.m
@implementation LBHPerson
@end
/********LBHStudent********/
//.h
@interface LBHStudent : LBHPerson
+ (instancetype)shareInstance;
@end
//.m
@implementation LBHStudent
static LBHStudent* _instance = nil;
+ (instancetype)shareInstance{
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
_instance = [[super allocWithZone:NULL] init] ;
}) ;
return _instance ;
}
@end
step2:
有一個(gè)嵌套在UINavigationController中的ViewController類,有一個(gè)導(dǎo)航按鈕可以push到下一個(gè)類LBHViewController
@interface ViewController ()
@property (nonatomic, strong) LBHStudent *student;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [LBHStudent shareInstance];
}
step3:
LBHViewController
類
@interface LBHViewController ()
@property (nonatomic, strong) LBHStudent *student;
@end
@implementation LBHViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [LBHStudent shareInstance];
[self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.student.name = [NSString stringWithFormat:@"%@+",self.student.name];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
// fastpath
// 性能 + 代碼可讀性
NSLog(@"%@",change);
}
@end
step4:
運(yùn)行盆赤,跳轉(zhuǎn)到LBHViewController
贾富,返回,繼續(xù)跳轉(zhuǎn)到LBHViewController
牺六,點(diǎn)擊屏幕颤枪,崩潰了
問題
: 為什么會(huì)崩潰?
解答
: 第一次進(jìn)入LBHViewController注冊了KVO觀察者
,返回上一個(gè)頁面時(shí)沒有移除監(jiān)聽
淑际,再次進(jìn)入會(huì)重新注冊KVO觀察者
胶征,而且第一次的通知對象還在內(nèi)存中磷瘤,沒有進(jìn)行釋放缝其,點(diǎn)擊屏幕屬性發(fā)生變化涝缝,此時(shí)接收到屬性值變化的通知,會(huì)出現(xiàn)找不到原有的通知對象淡溯,只能找到現(xiàn)有的通知對象
读整,即第二次KVO注冊的觀察者簿训,所以導(dǎo)致了類似野指針的崩潰
咱娶,即一直保持著一個(gè)野通知,且一直在監(jiān)聽
【注】
:這里的崩潰案例是通過單例對象實(shí)現(xiàn)强品,因?yàn)閱卫龑ο笤趦?nèi)存是常駐的膘侮,針對一般的類對象,貌似不移除也是可以的的榛,但是為了防止線上意外琼了,建議還是移除比較好
解決方法
在LBHViewController
的dealloc
方法中加上移除監(jiān)聽
- (void)dealloc{
[self.student removeObserver:self forKeyPath:@"name" context:NULL];
}
2.2 KVO的自動(dòng)觸發(fā)與手動(dòng)觸發(fā)
KVO觀察的開啟
和關(guān)閉
有兩種方式,自動(dòng)
和手動(dòng)
- 自動(dòng)開關(guān),返回NO雕薪,就監(jiān)聽不到昧诱,返回YES,表示監(jiān)聽
// 自動(dòng)開關(guān)
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
- 自動(dòng)開關(guān)關(guān)閉的時(shí)候所袁,可以通過手動(dòng)開關(guān)監(jiān)聽盏档,重寫監(jiān)聽屬性的
set
方法
- (void)setName:(NSString *)name{
//手動(dòng)開關(guān)
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
測試
step1:
在上面demo中,LBHPerson
中添加automaticallyNotifiesObserversForKey
方法返回一個(gè)NO
// 自動(dòng)開關(guān)
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
step2:
運(yùn)行
沒有任何打印燥爷,說明并沒有監(jiān)聽
step3:
在LBHPerson
中添加如下方法
- (void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"]; //即將改變
_name = name;
[self didChangeValueForKey:@"name"]; //已改變
}
step4:
打印正常
2.3 KVO觀察:一對多
KVO觀察中的一對多蜈亩,意思是通過注冊一個(gè)KVO觀察者,可以監(jiān)聽多個(gè)屬性的變化
以下載進(jìn)度為例前翎,比如目前有一個(gè)需求稚配,需要根據(jù)總的下載量totalData
和當(dāng)前下載量writtenData
來計(jì)算當(dāng)前的下載進(jìn)度downloadProgress
,實(shí)現(xiàn)有兩種方式:
1港华、
分別觀察 總的下載量totalData
和當(dāng)前下載量writtenData
兩個(gè)屬性道川,當(dāng)其中一個(gè)發(fā)生變化
重新計(jì)算當(dāng)前下載進(jìn)度currentProcess
2、
實(shí)現(xiàn)keyPathsForValuesAffectingValueForKey
方法苹丸,將兩個(gè)觀察合為一個(gè)觀察
愤惰,即觀察當(dāng)前下載進(jìn)度currentProcess
// LBHPerson
@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end
@implementation LBHPerson
- (NSString *)downloadProgress {
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [NSString stringWithFormat:@"%.2f", 1.0f * self.writtenData / self.totalData];
}
// 下載進(jìn)入 writtenData / totalData
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray * affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) LBHPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LBHPerson new];
// 添加監(jiān)聽
[self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
}
// 處理監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@: %@", keyPath, change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.writtenData += 20;
self.person.totalData +=10;
}
-(void)dealloc {
// 移除監(jiān)聽
[self.person removeObserver:self forKeyPath:@"downloadProgress" context: NULL];
}
@end
運(yùn)行結(jié)果
兩個(gè)屬性在點(diǎn)擊時(shí)都有賦值,所以每次打印兩條
2.4 KVO 觀察可變數(shù)組
可變數(shù)組如果直接添加數(shù)據(jù)赘理,是不會(huì)調(diào)用setter
方法的宦言,所有對可變數(shù)組的KVO觀察下面這種方式不生效的,即直接通過[self.person.dateArray addObject:@"1"];向數(shù)組添加元素,是不會(huì)觸發(fā)kvo通知回調(diào)的
2.4.1 官方文檔說明
翻譯成中文
2.4.2 測試代碼
// LBHPerson
@interface LBHPerson : NSObject
@property (nonatomic, strong) NSMutableArray *dateArray;
@end
@implementation LBHPerson
- (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
[self.dateArray insertObject:object atIndex:index];
}
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) LBHPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LBHPerson new];
self.person.dateArray = [NSMutableArray new];
// 添加監(jiān)聽
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
}
// 處理監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@: %@", keyPath, change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
//集合相關(guān)API
[[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"2" atIndex:0];
[[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"3" atIndexedSubscript:0];
[[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
}
-(void)dealloc {
// 移除監(jiān)聽
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
[self.person removeObserver:self forKeyPath:@"dateArray" context: NULL];
}
@end
運(yùn)行
其中的kind
表示鍵值變化的類型
商模,是一個(gè)枚舉奠旺,主要有以下4種
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//設(shè)值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替換
};
3 KVO 底層原理探索
KVO
是使用isa-swizzling
的技術(shù)實(shí)現(xiàn)的。顧名思義施流,
isa指針
指向維護(hù)分配表的對象的類
响疚。該分派表實(shí)質(zhì)上包含指向該類實(shí)現(xiàn)的方法的指針以及其他數(shù)據(jù)。當(dāng)為
對象的屬性注冊觀察者
時(shí)瞪醋,將修改觀察對象的isa指針
忿晕,指向中間類
而不是真實(shí)類。結(jié)果银受,isa指針的值不一定反映實(shí)例的實(shí)際類
践盼。您永遠(yuǎn)不應(yīng)依靠isa指針來確定類成員身份。相反宾巍,您應(yīng)該使用class方法來確定對象實(shí)例的類咕幻。
3.1 KVO 只對屬性觀察
3.1.1 測試
在LBHPerson
中創(chuàng)建一個(gè)成員變量nickName
和一個(gè)屬性name
,然后分別注冊KVO
// LBHPerson
@interface LBHPerson : NSObject
{
@public NSString * nickName;
}
@property (nonatomic, copy) NSString *name;
@end
@implementation LBHPerson
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) LBHPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LBHPerson new];
self.person.name = @"liu";
// 添加監(jiān)聽
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
}
// 處理監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@: %@", keyPath, change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@name+", self.person.name];
self.person->nickName = [NSString stringWithFormat:@"%@nickName+", self.person->nickName];
}
-(void)dealloc {
// 移除監(jiān)聽
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
[self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
}
@end
運(yùn)行
多次打印結(jié)果都只有屬性顶霞,并沒有出現(xiàn)成員變量的打印肄程,說明成員變量并沒有監(jiān)聽 。
結(jié)論:KVO對成員變量不觀察,只對屬性觀察
蓝厌,屬性和成員變量的區(qū)別在于屬性多一個(gè) setter 方法
玄叠,KVO觀察的就是setter 方法
。
3.2 中間類
根據(jù)官方文檔所述拓提,在注冊KVO觀察者后
诸典,觀察對象的isa指針指向會(huì)發(fā)生改變
注冊觀察者之前:
注冊觀察者之后:
在注冊觀察者之后,對象的isa指針發(fā)生變化
崎苗,指向的類也發(fā)生了改變
p self.person.class
得到的卻是LBHPerson
狐粱,NSKVONotifying_LBHPerson
和LBHPerson
是什么關(guān)系?
3.2.1 NSKVONotifying_LBHPerson
和LBHPerson
類 的關(guān)系
可以通過下面封裝的方法胆数,獲取LBHPerson的相關(guān)類
#import <objc/runtime.h>
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
// 注冊類的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個(gè)數(shù)組肌蜻, 其中包含給定對象
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);
}
//********調(diào)用********
[self printClasses:[LBHPerson class]];
從結(jié)果中可以說明NSKVONotifying_LBHPerson
是LBHPerson
的子類
3.2.2 中間類有些什么?
我們添加遍歷Ivars
必尼、Property
蒋搜、Method
的函數(shù):
/// 遍歷Ivars
-(void)printIvars: (Class)cls {
// 仿寫Ivar結(jié)構(gòu)
typedef struct LBH_ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
}LBH_ivar_t;
// 記錄函數(shù)個(gè)數(shù)
unsigned int count = 0;
// 讀取函數(shù)列表
Ivar * ivars = class_copyIvarList(cls, &count);
for (int i = 0; i < count; i++) {
LBH_ivar_t * ivar = (LBH_ivar_t *) ivars[i];
NSLog(@"ivar: %@", [NSString stringWithUTF8String: ivar->name]);
}
free(ivars);
}
/// 遍歷屬性
-(void) printProperties: (Class)cls {
// 仿寫objc_property_t結(jié)構(gòu)
typedef struct LBH_property_t{
const char *name;
const char *attributes;
}LBH_property_t;
// 記錄函數(shù)個(gè)數(shù)
unsigned int count = 0;
// 讀取函數(shù)列表
objc_property_t * props = class_copyPropertyList(cls, &count);
for (int i = 0; i < count; i++) {
LBH_property_t * prop = (LBH_property_t *)props[i];
NSLog(@"property: %@", [NSString stringWithUTF8String:prop->name]);
}
free(props);
}
/// 遍歷方法
-(void) printMethodes: (Class)cls {
// 記錄函數(shù)個(gè)數(shù)
unsigned int count = 0;
// 讀取函數(shù)列表
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(@"method: %@-%p", NSStringFromSelector(sel), imp);
}
free(methodList);
}
在注冊KVO觀察者
后,分別對 LBHPerson本類
和 NSKVONotifying_LBHPerson派生類
的Ivars
判莉、Property
和Method
進(jìn)行打印
NSLog(@"------- NSKVONotifying_LBHPerson --------");
[self printMethodes: objc_getClass("NSKVONotifying_LBHPerson")];
[self printIvars: objc_getClass("NSKVONotifying_LBHPerson")];
[self printProperties: objc_getClass("NSKVONotifying_LBHPerson")];
NSLog(@"------- LBHPerson --------");
[self printMethodes: self.person.class];
[self printIvars: self.person.class];
[self printProperties: self.person.class];
打印結(jié)果
NSKVONotifying_LBHPerson
的方法是繼承還是重寫豆挽?
新建一個(gè)LBHPerson
的子類LBHStudent
,
NSLog(@"------- NSKVONotifying_LBHPerson --------");
[self printMethodes: objc_getClass("NSKVONotifying_LBHPerson")];
[self printIvars: objc_getClass("NSKVONotifying_LBHPerson")];
[self printProperties: objc_getClass("NSKVONotifying_LBHPerson")];
NSLog(@"------- LBHPerson --------");
[self printMethodes: self.person.class];
[self printIvars: self.person.class];
[self printProperties: self.person.class];
NSLog(@"------- LBHStudent --------");
[self printMethodes: LBHStudent.class];
[self printIvars: LBHStudent.class];
[self printProperties: LBHStudent.class];
打印
【結(jié)論
】
- 直接繼承的子類券盅,沒有任何方法和屬性帮哈。
- KVO派生類繼承自LBHPerosn,重寫了
setName
锰镀、class
娘侍、dealloc
方法,新增了_isKVOA
方法
3.2.3 KVO派生類給父類屬性賦值
step1:
在addObserver
處添加斷點(diǎn)泳炉,運(yùn)行代碼到此處時(shí)憾筏,lldb輸入:watchpoint set variable self->_person->_name
:
step2:
設(shè)置成功后,繼續(xù)運(yùn)行代碼花鹅,點(diǎn)擊屏幕觸發(fā)touchesBegan
事件氧腰,會(huì)進(jìn)入?yún)R編頁面(觀察到設(shè)置屬性斷點(diǎn)處)
查看堆棧,會(huì)有些發(fā)現(xiàn)
可以觀察到刨肃,當(dāng)派生類在調(diào)用willChange
和didChange
中間古拴,調(diào)用了[LBHPerson setName]
方法,完成了給父類LBHPerson的name屬性賦值
之景。(此時(shí)的willChange和didChange方法是繼承自NSObject的)
3.2.4 KVO派生類何時(shí)移除斤富,是否真移除膏潮?
step1:
在dealloc
函數(shù)中锻狗,移除KVO
處添加一個(gè)斷點(diǎn)
移除觀察者之前:實(shí)例對象的isa指向仍是NSKVONotifying_LBHPerson
中間類
step2:
執(zhí)行一步,繼續(xù)查看實(shí)例對象的isa指向
移除觀察者之后:實(shí)例對象的isa指向更改為LBHPerson
類
問題
: 那中間類是否被刪除?
解答
: 我們打印LBHPerson
及其子類打印
LBHPerosn類和子類
的信息轻纪,發(fā)現(xiàn)NSKVONotifying_LBHPerson派生類
并沒有移除油额。
中間類一旦生成,沒有移除刻帚,沒有銷毀
潦嘶,這樣可以減少頻繁的添加操作。
【總結(jié)】
1崇众、添加
:addObserver
時(shí)掂僵,創(chuàng)建了派生類
,派生類是當(dāng)前類的子類顷歌,重寫了被監(jiān)聽屬性的setter方法锰蓬,并將當(dāng)前類的isa指向了派生類
。
2眯漩、賦值
: 派生類重寫了被監(jiān)聽屬性的setter方法
芹扭,在派生類的setter方法觸發(fā)時(shí):在willChange之后
,didChange之前
,調(diào)用父類屬性settter方法赦抖,完成父類屬性的賦值舱卡。
3、移除
: 在removeObserver
后队萤,isa從派生類指回本類
轮锥, 但創(chuàng)建過的派生類,會(huì)一直在內(nèi)存中不會(huì)銷毀要尔。
4 自定義 KVO
自定KVO的目的
1交胚、模擬系統(tǒng)實(shí)現(xiàn)KVO原理
2、自動(dòng)移除觀察者
3盈电、實(shí)現(xiàn)響應(yīng)式+函數(shù)式
核心流程:
- 1蝴簇、
addObserver
時(shí):- 1.1 驗(yàn)證
setter
方法是否存在
- 1.2 注冊
KVO派生類
- 1.3 派生類
添加setter、class匆帚、dealloc方法
- 1.4
isa指向派生類
- 1.5 保存信息
- 1.1 驗(yàn)證
- 2熬词、
觸發(fā)setter方法
時(shí):- 2.1
willChange
- 2.1 消息轉(zhuǎn)發(fā)(設(shè)置原類的屬性值)
- 2.2
didChange
- 2.1
- 3、
removeObserver
:- 3.1 手動(dòng)移除
- 3.2 自動(dòng)移除
示例相關(guān)文件說明
本示例中:
ViewController
:有導(dǎo)航控制器的根視圖吸重,點(diǎn)擊Push按鈕可跳轉(zhuǎn)PushViewController
互拾;LGViewController
:測試控制器,實(shí)現(xiàn)LBHPerson屬性
的添加觀察者
嚎幸、觸發(fā)屬性變化
颜矿、移除觀察者
等功能;LBHPerosn
:繼承自NSObject嫉晶,具備name和nickName屬性
的類NSObject+LBHKVO
:重寫KVO的相關(guān)功能
1 添加addObserver
// 添加觀察者
- (void)lbh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LBHKVOBlock)block {
// 1.1 驗(yàn)證setter方法是否存在
[self judgeSetterMethodFromKeyPath:keyPath];
// 1.2 + 1.3 注冊KVO派生類(動(dòng)態(tài)生成子類) 添加方法
Class newClass = [self creatChildClassWithKeyPath:keyPath];
// 1.4 isa的指向: LBHKVONotifying_LBHPerosn
object_setClass(self, newClass);
// 1.5. 保存信息
LBHInfo * info = [[LBHInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
[self associatedObjectAddObject:info];
}
1.1 驗(yàn)證setter
方法是否存在
//MARK: - 驗(yàn)證是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *) keyPath {
Class class = object_getClass(self);
SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(class, setterSelector);
if (!setterMethod) {
@throw [NSException exceptionWithName: NSInvalidArgumentException
reason:[NSString stringWithFormat:@"當(dāng)前%@沒有setter方法", keyPath]
userInfo:nil];
}
}
因?yàn)槲覀儽O(jiān)聽的是setter方法骑疆,所以當(dāng)前被監(jiān)聽屬性必須具備setter方法
1.1.1 setterForGetter
從getter
名稱中讀取setter
田篇, key
=> setKey
static NSString * setterForGetter(NSString * getter) {
if (getter.length <= 0) return nil;
NSString * setterFirstChar = [getter substringToIndex:1].uppercaseString;
return [NSString stringWithFormat:@"set%@%@:", setterFirstChar, [getter substringFromIndex:1]];
}
1.1.2 getterForSetter
從set
方法獲取getter
方法, setKey
=> key
#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];
}
1.2 注冊KVO派生類
- (Class)creatChildClassWithKeyPath: (NSString *) keyPath {
// 1. 類名
NSString * oldClassName = NSStringFromClass([self class]);
NSString * newClassName = [NSString stringWithFormat:@"%@%@",LBHKVOPrefix,oldClassName];
// 2. 生成類
Class newClass = NSClassFromString(newClassName);
// 2.1 不存在箍铭,創(chuàng)建類
if (!newClass) {
// 2.2.1 申請內(nèi)存空間 (參數(shù)1:父類泊柬,參數(shù)2:類名,參數(shù)3:額外大姓┗稹)
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2.2 注冊類
objc_registerClassPair(newClass);
}
// 2.2.3 動(dòng)態(tài)添加set函數(shù)
SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSel); //為了保證types和原來的類的Imp保持一致兽赁,所以從[self class]提取
const char * setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSel, (IMP)ht_setter, setterTypes);
// 2.2.4 動(dòng)態(tài)添加class函數(shù) (為了讓外界調(diào)用class時(shí),不看到中間類冷守,看到的時(shí)原來的類刀崖,isa需要指向原來的類)
SEL classSel = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSel);
const char * classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSel, (IMP)ht_class, classTypes);
// 2.2.5 動(dòng)態(tài)添加dealloc函數(shù)
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
const char * deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSel, (IMP)ht_dealloc, deallocTypes);
return newClass;
}
1.2.1 LBHKVO類的命名前綴,關(guān)聯(lián)屬性的key:
static NSString *const kLBHKVOPrefix = @"LBHKVONotifying_";
static NSString *const LBHKVOAssiociakey = @"kLBHKVO_AssiociateKey";
1.3 派生類添加setter
拍摇、class
蒲跨、dealloc方法
1.3.1 setter
方法
static void lbh_setter(id self, SEL _cmd, id newValue) {
NSLog(@"新值:%@", newValue);
// 讀取getter方法(屬性名)
NSString * keyPath = getterForSetter(NSStringFromSelector(_cmd));
// 獲取舊值
id oldValue = [self valueForKey:keyPath];
// 1. willChange在此處觸發(fā)(本示例省略)
// 2. 調(diào)用父類的setter方法(消息轉(zhuǎn)發(fā))
// 修改objc_super的值,強(qiáng)制將super_class設(shè)置為父類
void(* lbh_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
// 創(chuàng)建并賦值
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
lbh_msgSendSuper(&superStruct, _cmd, newValue);
// objc_msgSendSuper(&superStruct, _cmd, newValue);
// 3. didChange在此處觸發(fā)(本示例省略)
NSMutableArray * array = objc_getAssociatedObject(self, (__bridge const void * _Nonnull) LBHKVOAssiociakey);
for (LBHKVOInfo * info in array) {
if([info.keyPath isEqualToString:keyPath] && info.observer){
// 3.1 block回調(diào)的方式
if (info.hanldBlock) {
info.hanldBlock(info.observer, keyPath, oldValue, newValue);
}
// // 3.2 調(diào)用方法的方式
if([info.observer respondsToSelector:@selector(lbh_observeValueForKeyPath: ofObject: change: context:)]) {
[info.observer lbh_observeValueForKeyPath:keyPath ofObject:self change:@{keyPath: newValue} context:NULL];
}
}
}
}
給屬性賦值時(shí)會(huì)觸發(fā)setter授翻,有3個(gè)需要注意的點(diǎn):
1或悲、
賦值前
: 本案例沒實(shí)現(xiàn)賦值前的willChange
事件。因?yàn)榕c下面的didChange
方式一樣堪唐,只是狀態(tài)不同巡语;2、
賦值
: 調(diào)用父類的setter
方法淮菠,我們是通過objc_msgSendSuper
進(jìn)行調(diào)用男公。我們重寫objc_super
的結(jié)構(gòu)體并完成receiver
和super_class的賦值
。
直接使用
objc_msgSendSuper
調(diào)用合陵,會(huì)報(bào)參數(shù)錯(cuò)誤
有兩種解決方法
1枢赔、
objc_msgSend
的檢查關(guān)閉:target
--> Build Setting
--> Enable Strict Checking of objc_msgSend Calls
設(shè)置為NO
2、
新創(chuàng)建一個(gè)lbh_msgSendSuper
引用objc_msgSendSuper
拥知,這樣編譯就不會(huì)報(bào)錯(cuò)踏拜,不需要關(guān)閉編譯檢查
- 3、
賦值后
:我們有2種方法可以實(shí)現(xiàn)didChange事件低剔,告知外部
1速梗、
和蘋果官方一樣,NSObject+LBHKVO.h
文件中對外公開lbh_observeValueForKeyPath
函數(shù)
在調(diào)用的地方實(shí)現(xiàn)這個(gè)方法即可
但是此方法方式讓代碼很分散襟齿,開發(fā)者需要在2個(gè)地方同時(shí)實(shí)現(xiàn)lbh_addObserver
和lbh_observeValueForKeyPath
兩個(gè)函數(shù)
2姻锁、
響應(yīng)式 + 函數(shù)式
,直接在lbh_addObserver
中添加Block
回調(diào)代碼塊猜欺,需要響應(yīng)的時(shí)候位隶,我們直接響應(yīng)block即可
.h 文件
.m 文件
在調(diào)用lbh_addObserver
函數(shù)時(shí),直接實(shí)現(xiàn)block
響應(yīng)就行开皿。這樣完成了代碼的內(nèi)聚涧黄。
1.3.2 class
方法
重寫class
方法篮昧,主要是讓外界讀取時(shí),看不到KVO派生類
弓熏,輸出的是原來的類
Class lbh_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self)); // 返回當(dāng)前類的父類(原來的類)
}
1.3.3 dealloc
方法
重寫了dealloc
方法,并將isa從KVO衍生類指回了原來的類
// 重寫dealloc方法
void lbh_dealloc(id self, SEL _cmd) {
NSLog(@"%s KVO派生類移除了",__func__);
Class superClass = [self class];
object_setClass(self, superClass);
}
1.4 isa
指向派生類
// 1.4 isa的指向: LBHKVONotifying_LBHPerosn
object_setClass(self, newClass);
1.5 保存信息
創(chuàng)建Info實(shí)例保存觀察數(shù)據(jù)
讀取關(guān)聯(lián)屬性數(shù)組
(當(dāng)前所有觀察對象) --->
如果關(guān)聯(lián)屬性數(shù)組不存在糠睡,就創(chuàng)建一個(gè)
(使用OBJC_ASSOCIATION_RETAIN_NONATOMIC沒關(guān)系挽鞠,因?yàn)殛P(guān)聯(lián)屬性不存在強(qiáng)引用,只是記錄類名和屬性名)
case1
如果被監(jiān)聽對象已存在狈孔,直接跳出
case2
添加監(jiān)聽對象
LBHKVOInfo * info = [[LBHKVOInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
[self associatedObjectAddObject:info];
//MARK: - 關(guān)聯(lián)屬性添加對象
- (void)associatedObjectAddObject:(LBHKVOInfo *)info {
NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey);
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey, mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
for (LBHKVOInfo * tempInfo in mArray) {
if ([tempInfo isEqual:info]) return;
}
[mArray addObject:info];
}
2. 觸發(fā)setter
方法時(shí)
在1.3.1 setter
方法中已描述清晰信认。
主要是三步:willChange
--> 設(shè)置原類屬性
--> didChange
3. removeObserver
移除觀察者
3.1 手動(dòng)移除
移除指定被監(jiān)聽屬性,如果都被移除了均抽,就將isa指回父類
- (void)lbh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
NSMutableArray * observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey);
if (observerArr.count <= 0) return;
for (LBHKVOInfo * info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
// 移除當(dāng)前info
[observerArr removeObject:info];
// 重新設(shè)置關(guān)聯(lián)對象的值
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey, observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
// 全部移除后嫁赏,isa指回父類
if (observerArr.count <= 0) {
Class superClass = [self class];
object_setClass(self, superClass);
}
}
問題
:手動(dòng)把所有被監(jiān)聽屬性都移除,觸發(fā)isa指回本類
油挥,那dealloc觸發(fā)lbh_dealloc觸發(fā)時(shí)潦蝇,isa會(huì)不會(huì)指向父類的父類了?
解答
:不會(huì)深寥。因?yàn)閕sa指回本類后攘乒,KVO派生類對象已被釋放。不會(huì)再進(jìn)入ht_dealloc惋鹅。
這也是為什么將isa指回本類,會(huì)自動(dòng)移除觀察者闰集。因?yàn)榕缮悓ο笠驯会尫殴炼铮涗浀年P(guān)聯(lián)屬性也自動(dòng)被釋放。
3.2 自動(dòng)移除
在1.3.3 dealloc
方法中已描述清晰
資料
FBKVOController
FaceBook出品武鲁,支持block
和action
回調(diào)爽雄,支持自動(dòng)移除
觀察者
GNU源碼
可參閱它推測還原的Foundation庫