在iOS開(kāi)發(fā)中啸蜜,KVO的使用頻率是非常高的骑晶,可能是間接使用也可能是直接使用,今天主要通過(guò)以下幾點(diǎn)進(jìn)行探索集惋。
KVO初探
1、 首先通過(guò)簡(jiǎn)單的使用KVO進(jìn)行分析蘋(píng)果提供的KVO中的API每個(gè)參數(shù)所代表的含義以及怎么使用能夠讓API達(dá)到最優(yōu)使用衣厘。
下面看下今天第一份代碼:
KGPerson.h
代碼如下:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KGPerson : NSObject
/// 用戶姓名
@property(nonatomic,copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
KGPerson.m
代碼如下:
#import "KGPerson.h"
@implementation KGPerson
@end
ViewController.m
中代碼如下:
#import "ViewController.h"
#import "KGPerson.h"
@interface ViewController ()
@property (nonatomic,strong) KGPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[KGPerson alloc] init];
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionPrior context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@==%@==%@",keyPath,object,change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_person.name = @"KG";
}
@end
以上就是我們經(jīng)常使用的KVO
的時(shí)候的常規(guī)寫(xiě)法捺典,那么接下來(lái)先看下- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
這個(gè)方法中參數(shù)的含義,observer
是觀察者對(duì)象大州,也就是消息接受者续语;keyPath
是路徑,也就是我們需要觀察的屬性或者成員變量厦画;options
和context
通過(guò)KVO我們可以看到蘋(píng)果對(duì)于參數(shù)的一些解釋疮茄,那么我們通過(guò)代碼去觀察下這些屬性,首先看下options根暑,這是一個(gè)枚舉力试,如下:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,//值為1,指示更改字典應(yīng)提供新的屬性值(如果適用)排嫌。
NSKeyValueObservingOptionOld = 0x02,//值為2畸裳,指示更改字典應(yīng)包含舊屬性值(如果適用)。
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,//值為4躏率,如果指定,則應(yīng)在觀察者注冊(cè)方法返回之前立即向觀察者發(fā)送通知民鼓。也就是用戶主要注冊(cè)了監(jiān)聽(tīng)薇芝,在值還沒(méi)有改變前就發(fā)送一次消息
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08,值為8丰嘉,是否應(yīng)該在每次更改前后向觀察者發(fā)送單獨(dú)的通知夯到,而不是更改后的單個(gè)通知。
};
補(bǔ)充:
1饮亏、枚舉值如果是這種按照1<<x
位表示耍贾,那么就代表可以進(jìn)行多選2阅爽、以上枚舉值
options
存在以下幾種情況:(1)、
options=1
:用戶選定NSKeyValueObservingOptionNew
(2)荐开、
options=2
:用戶選定NSKeyValueObservingOptionOld
(3)付翁、
options=3
:用戶選定NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
(4)、
options=4
:用戶選定NSKeyValueObservingOptionInitial
(5)晃听、
options=5
:用戶選定NSKeyValueObservingOptionInitial
和NSKeyValueObservingOptionNew
(6)百侧、
options=6
:用戶選定NSKeyValueObservingOptionInitial
和NSKeyValueObservingOptionOld
(7)、
options=7
:用戶選定NSKeyValueObservingOptionInitial
能扒、NSKeyValueObservingOptionOld
佣渴、NSKeyValueObservingOptionNew
(8)、
options=8
:用戶選定NSKeyValueObservingOptionPrior
(9)初斑、
options=9
:用戶選定NSKeyValueObservingOptionPrior
和NSKeyValueObservingOptionNew
(10)辛润、
options=10
:用戶選定NSKeyValueObservingOptionPrior
和NSKeyValueObservingOptionOld
(11)、
options=11
:用戶選定NSKeyValueObservingOptionPrior
见秤、NSKeyValueObservingOptionOld
砂竖、NSKeyValueObservingOptionNew
(12)、
options=12
:用戶選定NSKeyValueObservingOptionInitial
和NSKeyValueObservingOptionPrior
(13)秦叛、
options=13
:用戶選定NSKeyValueObservingOptionInitial
晦溪、NSKeyValueObservingOptionPrior
、NSKeyValueObservingOptionNew
(14)挣跋、
options=14
:用戶選定NSKeyValueObservingOptionInitial
三圆、NSKeyValueObservingOptionPrior
、NSKeyValueObservingOptionNew
避咆、NSKeyValueObservingOptionOld
然后我們通過(guò)上述補(bǔ)充中的方案編寫(xiě)代碼去看下具體效果:
方案1:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan
)
方案2:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan
)
方案3:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan
)
方案4:(不需要點(diǎn)擊屏幕)
方案5:(不需要點(diǎn)擊屏幕)
方案6:(不需要點(diǎn)擊屏幕)
方案7:(不需要點(diǎn)擊屏幕)
方案8:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan
)
方案9:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan
)
方案10:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan
)
方案11:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan
)
方案12:(不點(diǎn)擊屏幕舟肉,打印第一行,點(diǎn)擊屏幕觸發(fā)touchesBegan
打印后兩行)
方案13:(不點(diǎn)擊屏幕查库,打印第一行路媚,點(diǎn)擊屏幕觸發(fā)touchesBegan
打印后兩行)
方案14:(不點(diǎn)擊屏幕,打印第一行樊销,點(diǎn)擊屏幕觸發(fā)touchesBegan
打印后兩行)
以上就是所有options
的方案結(jié)果整慎,可以根據(jù)自己項(xiàng)目中場(chǎng)景去選擇相應(yīng)的方案。
接下來(lái)看下context
參數(shù)围苫,這個(gè)參數(shù)在KVO中解釋的很通徹险毁,主要就是來(lái)區(qū)分不同的類有相同的屬性監(jiān)聽(tīng)改橘,也就是相同用的keyPath
情況下海洼,簡(jiǎn)化判斷條件用的慰照,而且這種判斷更安全。下面看下我們常規(guī)判斷和使用context
判斷的實(shí)例:
常規(guī)觀察不同對(duì)象的相同屬性代碼書(shū)寫(xiě):(我們經(jīng)常寫(xiě)的時(shí)候
context
傳的是nil
,這種寫(xiě)法雖然運(yùn)行沒(méi)有錯(cuò)淤袜,但是從代碼嚴(yán)謹(jǐn)程度來(lái)說(shuō)是錯(cuò)的痒谴,因?yàn)樘O(píng)果在文檔中明確指出,這塊如果不使用铡羡,傳入NULL
)image.png使用
context
觀察不同對(duì)象的相同屬性代碼書(shū)寫(xiě):image.png補(bǔ)充:
nil
:在OC
中表示一個(gè)指針的值為空积蔚,經(jīng)常用來(lái)創(chuàng)建一個(gè)空對(duì)象,表示指針不指向任何內(nèi)存空間蓖墅。
NULL
:在C
中表示一個(gè)指針的值為空库倘,表示指針不指向任何內(nèi)存空間。
到這里基本上對(duì)于KVO
的使用论矾,我想已經(jīng)信手拈來(lái)了教翩,那么我們?cè)龠M(jìn)一步了解下KVO
的主動(dòng)調(diào)用和被動(dòng)調(diào)用。最后記得移除監(jiān)聽(tīng)贪壳,一般都是在dealloc
中進(jìn)行監(jiān)聽(tīng)的移除饱亿,如下:
2、我們?cè)陂_(kāi)發(fā)中有的時(shí)候闰靴,比如說(shuō)成員變量的KVO
監(jiān)聽(tīng)彪笼,我們直接修改值是監(jiān)聽(tīng)不到的,那么這時(shí)候我們通常會(huì)手動(dòng)調(diào)用willChangeValueForKey
和didChangeValueForKey
來(lái)觸發(fā)KVO
監(jiān)聽(tīng)蚂且。代碼如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[_person willChangeValueForKey:@"_nikeName"];
[_person setValue:@"KG" forKey:@"_nikeName"];
[_person didChangeValueForKey:@"_nikeName"];
}
另外對(duì)于屬性的監(jiān)聽(tīng)配猫,我們?nèi)绻枰謩?dòng)去觸發(fā)KVO
的回調(diào),那么那么應(yīng)該先重寫(xiě)+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法杏死。具體如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if (key isEqualToString:@"name") {
return NO;
}
return YES;
}
對(duì)于屬性我們手動(dòng)調(diào)用除了以上方法重寫(xiě)泵肄,還需要主動(dòng)調(diào)用willChangeValueForKey
和didChangeValueForKey
,具體寫(xiě)法如下:
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
到此我們對(duì)于KVO
的基本使用就完成了淑翼,那么接下來(lái)那就看下蘋(píng)果是如何去實(shí)現(xiàn)KVO
的腐巢,原理是什么?請(qǐng)聽(tīng)下回分析玄括。
KVO原理探索
1冯丙、話不多說(shuō),先看以下動(dòng)圖遭京,我們看圖說(shuō)話胃惜。
從以上動(dòng)畫(huà)我們可以看到,當(dāng)我們對(duì)一個(gè)對(duì)象添加KVO
屬性觀察的時(shí)候哪雕,系統(tǒng)會(huì)修改我們對(duì)象的isa
指針船殉,指向一個(gè)運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建的類NSKVONotifying_KGPerson
,那么會(huì)有同學(xué)問(wèn)了热监,你咋知道捺弦,我不告訴你是蘋(píng)果給我說(shuō)的,蘋(píng)果當(dāng)時(shí)是這么說(shuō)的:
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.
經(jīng)過(guò)我的翻譯是這么說(shuō)的:
自動(dòng)鍵值觀察是使用稱為isa-swizzling的技術(shù)實(shí)現(xiàn)的饮寞。
該isa指針孝扛,顧名思義列吼,指向?qū)ο蟮念悾3忠粋€(gè)調(diào)度表苦始。該調(diào)度表主要包含指向類實(shí)現(xiàn)的方法的指針寞钥,以及其他數(shù)據(jù)。
當(dāng)觀察者為對(duì)象的屬性注冊(cè)時(shí)陌选,被觀察對(duì)象的 isa 指針被修改理郑,指向中間類而不是真正的類。因此咨油,isa 指針的值不一定反映實(shí)例的實(shí)際類您炉。
您永遠(yuǎn)不應(yīng)該依賴isa指針來(lái)確定類成員資格。相反赚爵,您應(yīng)該使用該class方法來(lái)確定對(duì)象實(shí)例的類。
2法瑟、實(shí)際上這個(gè)是在蘋(píng)果官方文檔KVO中有解析冀膝。說(shuō)的很明確,就是通過(guò)isa-swizzling
技術(shù)實(shí)現(xiàn)的霎挟,簡(jiǎn)單點(diǎn)來(lái)說(shuō)就是在運(yùn)行時(shí)窝剖,使用runtime
動(dòng)態(tài)創(chuàng)建一個(gè)類,然后將對(duì)象的isa
指針指向進(jìn)行修改酥夭,讓isa
指向動(dòng)態(tài)創(chuàng)建的類赐纱,那么這個(gè)動(dòng)態(tài)創(chuàng)建的類是繼承于哪個(gè)類呢?我們一起修改下代碼采郎,然后運(yùn)行打印千所,具體代碼以及結(jié)果如圖所示:
我們分別在添加屬性監(jiān)聽(tīng)前以及添加監(jiān)聽(tīng)后打印KGPerson
這個(gè)類的以及它的所有子類,我們可以通過(guò)打印輸出看到蒜埋,當(dāng)添加監(jiān)聽(tīng)后淫痰,系統(tǒng)會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)繼承于KGPerson
類的子類NSKVONotifying_KGPerson
。然后我們修改下KGPerson
這個(gè)類代碼如下:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KGPerson : NSObject{
@public
NSString *_nikeName;
}
@property (nonatomic,copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
修改完成后整份,我們?cè)谑褂?code>LGPerson類的地方進(jìn)行如圖所示的修改以及屬性監(jiān)聽(tīng)待错,然后看下效果:
從上面打印結(jié)果可以看出,對(duì)于成員變量的監(jiān)聽(tīng)沒(méi)有效果烈评,對(duì)于屬性的監(jiān)聽(tīng)是能夠監(jiān)聽(tīng)到火俄。然后我對(duì)以上代碼進(jìn)行修改,再次運(yùn)行看下效果:
對(duì)于成員變量的屬性監(jiān)聽(tīng)走了讲冠,那么對(duì)此我們可以得出以下結(jié)論:
KVO
的原理包含以下兩點(diǎn):
動(dòng)態(tài)生成子類:
NSKVONotifying_XXX
觀察的是
setter
方法
3瓜客、下面我們看下動(dòng)態(tài)創(chuàng)建的這個(gè)繼承于KGPerson
的子類NSKVONotifying_KGPerson
中系統(tǒng)做了哪些操作?下面先看以下代碼,然后我們進(jìn)行分析:
通過(guò)以上代碼運(yùn)行結(jié)果谱仪,我們可以分析出動(dòng)態(tài)創(chuàng)建的子類做了以下幾個(gè)操作:
重寫(xiě)觀察的屬性的
setter
方法重寫(xiě)
class
方法玻熙,這個(gè)方法返回的還是KGPerson
類。重寫(xiě)
dealloc
方法疯攒,在執(zhí)行銷毀方法后嗦随,會(huì)將isa
指針指會(huì)到KGPerson
,而且動(dòng)態(tài)創(chuàng)建的類會(huì)進(jìn)行緩存敬尺。實(shí)現(xiàn)了
_isKVOA
方法
4枚尼、對(duì)于上面第三條結(jié)論,我們進(jìn)行驗(yàn)證下砂吞,修改下代碼然后運(yùn)行署恍,結(jié)果如下:
我們?cè)?code>dealloc方法中進(jìn)行監(jiān)聽(tīng)的移除,然后移除完成后走斷點(diǎn)蜻直,打印輸出可以看到po object_getClassName(self.person)
對(duì)象的isa
又指會(huì)到KGPerson
了锭汛,然后我們?cè)谏弦粋€(gè)界面打印下KGPerson
的所有子類,代碼以及結(jié)果如下:
5袭蝗、從上圖再次證明了唤殴,上面第三條結(jié)論,當(dāng)動(dòng)態(tài)創(chuàng)建子類后到腥,系統(tǒng)會(huì)默認(rèn)緩存子類朵逝。到此我們了解了系統(tǒng)KVO
運(yùn)行的一個(gè)基本原理,流程如下:
當(dāng)我們了解了
KVO
的原理后乡范,那么我們是否可以自定義實(shí)現(xiàn)KVO
呢配名?下面一起研究自定義KVO
。
自定義模擬KVO
1晋辆、首先我們先對(duì)系統(tǒng)的KVO
方法進(jìn)行分析渠脉,通過(guò)查看KVO
的api
我們可以看到,系統(tǒng)是對(duì)NSObject
類進(jìn)行了擴(kuò)展瓶佳,也就是通過(guò)分類來(lái)實(shí)現(xiàn)的芋膘。那么我們也同樣通過(guò)分類來(lái)進(jìn)行,比較如果想要對(duì)全局所有的類都能進(jìn)行屬性監(jiān)聽(tīng)霸饲,對(duì)NSObject
進(jìn)行分類擴(kuò)展是最好的为朋,因?yàn)樗械念惗际抢^承于NSObject
的,那么下面我們先創(chuàng)建一個(gè)NSObject
的分類厚脉,命名為KGKVO
习寸。具體代碼如下,代碼中有詳細(xì)的注釋:
NSObject+KGKVO.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KGKVO)
- (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
NS_ASSUME_NONNULL_END
NSObject+KGKVO.m
#import "NSObject+KGKVO.h"
#import <objc/message.h>
// 動(dòng)態(tài)創(chuàng)建子類時(shí)的類名前綴
static NSString *const kKGKVOPrefix = @"KGKVONotifying_";
// 獲取消息觀察者需要的關(guān)鍵字
static NSString *const kKGKVOAssiociateKey = @"kKGKVO_AssiociateKey";
@implementation NSObject (KGKVO)
- (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 1傻工、驗(yàn)證setter方法是否存在
[self judgeSetterMethodFormKeyPath:keyPath];
// 2霞溪、動(dòng)態(tài)生成子類
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3孵滞、isa指向新創(chuàng)建的子類,isa_swizzling
object_setClass(self, newClass);
// 4鸯匹、保存觀察者
objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
// 獲取到當(dāng)前子類的父類
Class superClass = [self class];
object_setClass(self, superClass);
}
#pragma mark -- 驗(yàn)證是否存在setter方法
- (void)judgeSetterMethodFormKeyPath:(NSString *)keyPath{
// 獲取父類
Class supperClass = object_getClass(self);
// 生成setter方法
SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
// 根據(jù)SEL獲取方法
Method setterMethod = class_getInstanceMethod(supperClass, setterSEL);
// 判斷是否存在方法
if (!setterMethod) {
// 如果方法不存在剃斧,拋出異常
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"對(duì)不起老鐵,當(dāng)前%@沒(méi)有setter方法",keyPath] userInfo:nil];
}
}
#pragma mark -- 動(dòng)態(tài)生成子類
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 獲取當(dāng)前類的類名
NSString *oldClassName = NSStringFromClass([self class]);
// 生成當(dāng)前類的子類類名
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kKGKVOPrefix,oldClassName];
// 根據(jù)生成的子類類名獲取類
Class newClass = NSClassFromString(newClassName);
// 判斷是否已經(jīng)存在子類
if (!newClass) {
// 如果不存在忽你,先申請(qǐng)類,第一個(gè)參數(shù)是需要傳入父類臂容,第二個(gè)參數(shù)是需要傳入類名科雳,第三個(gè)參數(shù)是需要申請(qǐng)的內(nèi)存空間大小
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注冊(cè)類
objc_registerClassPair(newClass);
// 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
// 根據(jù)SEL獲取Method
Method classMethod = class_getInstanceMethod([self class], @selector(class));
// 獲取方法參數(shù)以及方法返回類型
const char *classType = method_getTypeEncoding(classMethod);
// 給類添加class方法
class_addMethod(newClass, classSEL, (IMP)kg_class, classType);
}
// 創(chuàng)建setter方法SEL
SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
// 根據(jù)SEL獲取Method
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
// 獲取方法參數(shù)以及返回值類型
const char *type = method_getTypeEncoding(setterMethod);
// 添加方法
class_addMethod(newClass, setterSEL, (IMP)kg_setter, type);
// 返回子類
return newClass;
}
#pragma mark --重寫(xiě)dealloc
//static
#pragma mark --創(chuàng)建setter方法
static void kg_setter(id self,SEL _cmd,id newValue){
// 需要進(jìn)行消息轉(zhuǎn)發(fā),調(diào)用msgSendSuper()函數(shù)脓杉,所以需要?jiǎng)?chuàng)建結(jié)構(gòu)體對(duì)象
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass([self class])
};
// 進(jìn)行強(qiáng)轉(zhuǎn)
void (*kg_objc_msgSendSuper)(void *,SEL ,id) = (void *)objc_msgSendSuper;
// 進(jìn)行消息轉(zhuǎn)發(fā)
kg_objc_msgSendSuper(&superStruct,_cmd,newValue);
// 然后拿到消息觀察者
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
// 然后將消息發(fā)送給觀察者
SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
// 獲取getter方法
NSString *keyPath = getterFormSetter(NSStringFromSelector(_cmd));
// 消息轉(zhuǎn)發(fā)
((void (*)(id, SEL, NSString *, id, NSDictionary *, void *))(void *)objc_msgSend)(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
}
#pragma mark -- 獲取類的父類
Class kg_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
#pragma mark -- 從getter方法獲取setter方法 keyPath->setKeyPath
static NSString *setterSelector(NSString *getter){
// 判斷keyPath是否合法
if (getter.length <= 0) {
return nil;
}
// 取第一個(gè)字符并且轉(zhuǎn)換成大寫(xiě)
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
// 獲取除第一個(gè)字符外的其它字符
NSString *leaveString = [getter substringFromIndex:1];
// 返回setter方法
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragma mark -- 從setter方法獲取getter方法
static NSString *getterFormSetter(NSString *setter){
// 判斷setter方法時(shí)候合法
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
// 獲取keyPath
NSRange range = NSMakeRange(3, setter.length-4);
// 獲取getter方法名
NSString *getter = [setter substringWithRange:range];
// 獲取第一個(gè)字符糟秘,并轉(zhuǎn)換成小寫(xiě)
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
// 返回getter方法
return [setter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end
2、然后在調(diào)用的地方如下圖所示使用:
3球散、到這里我們仿照系統(tǒng)的KVO
實(shí)現(xiàn)原理自定義實(shí)現(xiàn)了KVO
尿赚,但是也只是仿照系統(tǒng)的實(shí)現(xiàn)對(duì)屬性的觀察,沒(méi)有達(dá)到和系統(tǒng)KVO
一樣的完善蕉堰,所以我們繼續(xù)去完善這個(gè)自定義的KVO
凌净。目前存在的問(wèn)題:
如果說(shuō)多個(gè)對(duì)象進(jìn)行屬性值觀察,那么我們通過(guò)
objc_setAssociatedObject
來(lái)保存的observer
就會(huì)出現(xiàn)錯(cuò)亂屋讶。如果一個(gè)對(duì)象同時(shí)需要觀察多個(gè)屬性冰寻,那么意味著我們要多次調(diào)用
objc_setAssociatedObject
這個(gè)函數(shù),那么就會(huì)出現(xiàn)重復(fù)使用同一個(gè)關(guān)鍵值和關(guān)聯(lián)策略去關(guān)聯(lián)不同的值給對(duì)象皿渗,那么很容易造成
內(nèi)存泄露斩芭。
4、那么接下來(lái)我們針對(duì)以上的問(wèn)題進(jìn)行完善自定義KVO
乐疆,請(qǐng)看下回分析划乖。
自定義模擬KVO+完善
1、首先我們針對(duì)多個(gè)屬性進(jìn)行觀察挤土,首先能夠想到的就是通過(guò)數(shù)組去保存一些信息琴庵,然后從數(shù)組中取值然后判斷做一系列的操作,那么那些信息我們?cè)趺礆w類呢仰美?在iOS中信息歸類细卧,我們無(wú)非就是結(jié)構(gòu)體、對(duì)象筒占、字典等等贪庙,但是在此處我們使用對(duì)象,定義如下對(duì)象:
@interface KGInfo : NSObject
/// 觀察者對(duì)象
@property (nonatomic,strong) NSObject *observer;
/// 屬性
@property (nonatomic,copy) NSString *keyPath;
/// 觀察鍵值條件
@property (nonatomic,assign) NSKeyValueObservingOptions options;
/// 辨別標(biāo)識(shí)符
@property (nonatomic,assign) void * context;
/// 初始化方法
/// @param observer 觀察者對(duì)象
/// @param keyPath 屬性
/// @param options 觀察鍵值條件
/// @param context 辨別標(biāo)識(shí)符
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
@implementation KGInfo
/// 初始化方法
/// @param observer 觀察者對(duì)象
/// @param keyPath 屬性
/// @param options 觀察鍵值條件
/// @param context 辨別標(biāo)識(shí)符
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
self.context = context;
}
return self;
}
@end
3翰苫、然后在之前的基礎(chǔ)上做相應(yīng)的修改:
- (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 1止邮、驗(yàn)證setter方法是否存在
[self judgeSetterMethodFormKeyPath:keyPath];
// 2这橙、動(dòng)態(tài)生成子類
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3、isa指向新創(chuàng)建的子類导披,isa_swizzling
object_setClass(self, newClass);
// 4屈扎、先進(jìn)行判斷是否已經(jīng)存在對(duì)象屬性觀察表了
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey));
// 5、判斷arr是否存在撩匕,如果不存在進(jìn)行創(chuàng)建鹰晨,類似懶加載
if (!arr) {
arr = [NSMutableArray array];
}
// 6、創(chuàng)建信息對(duì)象
KGInfo *info = [[KGInfo alloc] initWithObserver:observer forKeyPath:keyPath options:options context:context];
// 7止毕、將對(duì)象信息添加到數(shù)組
[arr addObject:info];
// 7模蜡、保存觀察屬性信息
objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
if (arr.count <= 0) {
return;
}
for (KGInfo *info in arr) {
if ([info.keyPath isEqualToString:keyPath]) {
[arr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observer.copy <= 0) {
// 獲取到當(dāng)前子類的父類
Class superClass = [self class];
object_setClass(self, superClass);
}
}
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 獲取當(dāng)前類的類名
NSString *oldClassName = NSStringFromClass([self class]);
// 生成當(dāng)前類的子類類名
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kKGKVOPrefix,oldClassName];
// 根據(jù)生成的子類類名獲取類
Class newClass = NSClassFromString(newClassName);;
if (!newClass) {
// 如果不存在,先申請(qǐng)類扁凛,第一個(gè)參數(shù)是需要傳入父類忍疾,第二個(gè)參數(shù)是需要傳入類名,第三個(gè)參數(shù)是需要申請(qǐng)的內(nèi)存空間大小
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注冊(cè)類
objc_registerClassPair(newClass);
// 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
// 根據(jù)SEL獲取Method
Method classMethod = class_getInstanceMethod([self class], @selector(class));
// 獲取方法參數(shù)以及方法返回類型
const char *classType = method_getTypeEncoding(classMethod);
// 給類添加class方法
class_addMethod(newClass, classSEL, (IMP)kg_class, classType);
}
// 創(chuàng)建setter方法SEL
SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
// 根據(jù)SEL獲取Method
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
// 獲取方法參數(shù)以及返回值類型
const char *setterTypes = method_getTypeEncoding(setterMethod);
// 添加方法
class_addMethod(newClass, setterSEL, (IMP)kg_setter, setterTypes);
// 返回子類
return newClass;
}
#pragma mark --創(chuàng)建setter方法
static void kg_setter(id self,SEL _cmd,id newValue){
// 獲取getter方法
NSString *keyPath = getterFormSetter(NSStringFromSelector(_cmd));
// 獲取舊值
id oldValue = [self valueForKey:keyPath];
// 需要進(jìn)行消息轉(zhuǎn)發(fā)谨朝,調(diào)用msgSendSuper()函數(shù)卤妒,所以需要?jiǎng)?chuàng)建結(jié)構(gòu)體對(duì)象
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass([self class])
};
// 進(jìn)行消息轉(zhuǎn)發(fā)
((void (*)(struct objc_super *,SEL,id))(void *)objc_msgSendSuper)(&superStruct,_cmd,newValue);
// 然后拿到觀察信息數(shù)組
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
// 判斷數(shù)組是否有值
if (arr.count > 0) {
// 循環(huán)拿出數(shù)組中的KGInfo對(duì)象
for (KGInfo *info in arr) {
// 判斷是否是需要的info
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionary];
// 對(duì)新舊值進(jìn)行處理
if (info.options & NSKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & NSKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
}else{
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
// 然后將消息發(fā)送給觀察者
SEL observerSEL = @selector(kg_observeValueForKeyPath:ofObject:change:context:);
// 消息轉(zhuǎn)發(fā)
((void (*)(id, SEL, NSString *, id, id, void *))(void *)objc_msgSend)(info.observer,observerSEL,keyPath,self,change,NULL);
});
}
}
}
}
4、到此的話一個(gè)基礎(chǔ)的KVO屬性觀察完成了字币,但是還是存在一些瑕疵则披,我們需要添加屬性觀察,然后跑到- (void)kg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
去判斷監(jiān)聽(tīng)返回的是哪個(gè)屬性的監(jiān)聽(tīng)等等一系列操作洗出,那么到此處我們是否還能優(yōu)化呢收叶?當(dāng)然是可以的,我們自然而然就想到了Block
共苛,函數(shù)式編程能夠讓我們的代碼更加簡(jiǎn)潔判没,所以請(qǐng)看下回分析。
自定義模擬KVO+函數(shù)式編程思想
1隅茎、針對(duì)之前的代碼比較繁瑣澄峰,我們?cè)俅巫鲆幌聝?yōu)化,使用函數(shù)式編程的思想辟犀,去打破系統(tǒng)KVO
繁瑣的步驟俏竞,經(jīng)過(guò)優(yōu)化后代碼如下:
2、那么使用的時(shí)候就更加簡(jiǎn)單了堂竟,如下:
3魂毁、從書(shū)寫(xiě)上我們就可以看到,在哪添加的觀察出嘹,就在那實(shí)現(xiàn)回調(diào)席楚,邏輯來(lái)說(shuō)更加清晰,而且去掉了繁瑣的observer
和keyPath
的判斷税稼,讓代碼更加簡(jiǎn)潔烦秩。
4垮斯、既然我們對(duì)自定義KVO
都簡(jiǎn)化到這個(gè)程度了,那么我們是否還能再簡(jiǎn)化只祠,直接實(shí)現(xiàn)自動(dòng)銷毀呢兜蠕?不需要手動(dòng)去銷毀呢?因?yàn)榻?jīng)常會(huì)忘記手動(dòng)remove
監(jiān)聽(tīng)抛寝,答案是肯定可以的熊杨,那么請(qǐng)看下一節(jié)分析。
補(bǔ)充:
- 在這里有一個(gè)細(xì)節(jié)性的東西盗舰,就是對(duì)
observer
的修飾符晶府,為什么不用strong
而是使用weak
的原因在于,防止循環(huán)引用岭皂,因?yàn)槿绻挥?code>weak去打斷閉環(huán),那么就是處于VC持有->person(持有)->arr(持有)->info(持有)->VC
的情況沼头。
自定義模擬KVO+自動(dòng)銷毀
1爷绘、我們發(fā)現(xiàn)每次需要添加屬性觀察都需要去手動(dòng)移除,而且很多時(shí)候因?yàn)榇中拇笠饨叮苋菀淄浺瞥林粒敲闯绦蜻\(yùn)行就會(huì)立馬報(bào)錯(cuò)奔潰,所以為了防止這種情況猾昆,我們需要去考量能否讓它進(jìn)行自動(dòng)銷毀陶因,當(dāng)對(duì)象釋放的時(shí)候,自動(dòng)移除屬性觀察呢垂蜗?此時(shí)我們想到了對(duì)系統(tǒng)的方法dealloc
進(jìn)行method_swizzled
楷扬,但是我們應(yīng)該在什么時(shí)機(jī)下去做這個(gè)操作呢?目前來(lái)說(shuō)我們經(jīng)常用的是在load
方法中贴见,還有當(dāng)前添加監(jiān)聽(tīng)的時(shí)候烘苹,也就是addObserver
的時(shí)候,但是選擇哪個(gè)呢片部?首先我們?nèi)ヅ袛嗳绻?code>addObserver的時(shí)候做方法Hook的話镣衡,怎么去判斷是否已經(jīng)Hook過(guò)了?所以我們決定在load
方法中進(jìn)行hook
档悠,但是因?yàn)?code>NSObject是所有類的基類廊鸥,這樣每個(gè)類的load
方法走的時(shí)候都會(huì)進(jìn)行Hook,會(huì)造成混亂辖所,到時(shí)候我們自己也不知道是否Hook成功惰说,所以我們?cè)谡麄€(gè)app啟動(dòng)后,在load
方法中只進(jìn)行一次Hook缘回,而且之后不會(huì)再進(jìn)行Hook助被,所以我們想到了使用單利時(shí)的做法剖张,通過(guò)dispatch_once
,然這個(gè)操作只執(zhí)行一次揩环。
2搔弄、當(dāng)我們理清思路后,然后回頭去修改代碼丰滑,就變的很簡(jiǎn)單了顾犹,NSObject+KGKVO.h
的優(yōu)化如下:
直接去掉了監(jiān)聽(tīng)移除的一系列處理,直接在Hook后的myDealloc
方法中進(jìn)行移除褒墨,簡(jiǎn)單而且嚴(yán)謹(jǐn)炫刷。
3、當(dāng)我們使用的時(shí)候只需要去添加屬性監(jiān)聽(tīng)郁妈,不需要去做額外的處理浑玛,也不需要去檢查是否進(jìn)行監(jiān)聽(tīng)的移除了,只需要一句簡(jiǎn)簡(jiǎn)單單的代碼調(diào)用噩咪,就完成了整個(gè)復(fù)雜的KVO
監(jiān)聽(tīng)顾彰,使用如下:
一行代碼,搞定通過(guò)KVO
監(jiān)聽(tīng)屬性值變化胃碾,而且也沒(méi)有其他的配置選項(xiàng)涨享,自動(dòng)返回舊值、新值等等仆百。
總結(jié)
到此對(duì)于KVO
的探索基本完成了厕隧,但是里面還有很多細(xì)節(jié)性的東西需要自己去優(yōu)化,比如:
監(jiān)聽(tīng)不同類型的屬性俄周,目前的代碼中
kg_setter
方法返回值以及參數(shù)不一致會(huì)導(dǎo)致奔潰問(wèn)題對(duì)于成員變量的監(jiān)聽(tīng)實(shí)現(xiàn)等等
如果對(duì)此感興趣的同學(xué)可以一起探討吁讨,或者去分析下FBKVOController