OC底層-KVO探索

在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是路徑,也就是我們需要觀察的屬性或者成員變量厦画;optionscontext通過(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:用戶選定NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld

(4)、options=4:用戶選定NSKeyValueObservingOptionInitial

(5)晃听、options=5:用戶選定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionNew

(6)百侧、options=6:用戶選定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionOld

(7)、options=7:用戶選定NSKeyValueObservingOptionInitial能扒、NSKeyValueObservingOptionOld佣渴、NSKeyValueObservingOptionNew

(8)、options=8:用戶選定NSKeyValueObservingOptionPrior

(9)初斑、options=9:用戶選定NSKeyValueObservingOptionPriorNSKeyValueObservingOptionNew

(10)辛润、options=10:用戶選定NSKeyValueObservingOptionPriorNSKeyValueObservingOptionOld

(11)、options=11:用戶選定NSKeyValueObservingOptionPrior见秤、NSKeyValueObservingOptionOld砂竖、NSKeyValueObservingOptionNew

(12)、options=12:用戶選定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionPrior

(13)秦叛、options=13:用戶選定NSKeyValueObservingOptionInitial晦溪、NSKeyValueObservingOptionPriorNSKeyValueObservingOptionNew

(14)挣跋、options=14:用戶選定NSKeyValueObservingOptionInitial三圆、NSKeyValueObservingOptionPriorNSKeyValueObservingOptionNew避咆、NSKeyValueObservingOptionOld
然后我們通過(guò)上述補(bǔ)充中的方案編寫(xiě)代碼去看下具體效果:

方案1:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan)

image.png

方案2:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan)

image.png

方案3:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan)

image.png

方案4:(不需要點(diǎn)擊屏幕)

image.png

方案5:(不需要點(diǎn)擊屏幕)

image.png

方案6:(不需要點(diǎn)擊屏幕)

image.png

方案7:(不需要點(diǎn)擊屏幕)

image.png

方案8:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan)

image.png

方案9:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan)

image.png

方案10:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan)

image.png

方案11:(需要點(diǎn)擊屏幕觸發(fā)touchesBegan)

image.png

方案12:(不點(diǎn)擊屏幕舟肉,打印第一行,點(diǎn)擊屏幕觸發(fā)touchesBegan打印后兩行)

image.png

方案13:(不點(diǎn)擊屏幕查库,打印第一行路媚,點(diǎn)擊屏幕觸發(fā)touchesBegan打印后兩行)

image.png

方案14:(不點(diǎn)擊屏幕,打印第一行樊销,點(diǎn)擊屏幕觸發(fā)touchesBegan打印后兩行)

image.png

以上就是所有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)的移除饱亿,如下:

image.png

2、我們?cè)陂_(kāi)發(fā)中有的時(shí)候闰靴,比如說(shuō)成員變量的KVO監(jiān)聽(tīng)彪笼,我們直接修改值是監(jiān)聽(tīng)不到的,那么這時(shí)候我們通常會(huì)手動(dòng)調(diào)用willChangeValueForKeydidChangeValueForKey來(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)用willChangeValueForKeydidChangeValueForKey,具體寫(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ō)話胃惜。

yzsrs-65wku.gif

從以上動(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í)例的類。
image.png

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é)果如圖所示:

image.png

我們分別在添加屬性監(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)待错,然后看下效果:

image.png

從上面打印結(jié)果可以看出,對(duì)于成員變量的監(jiān)聽(tīng)沒(méi)有效果烈评,對(duì)于屬性的監(jiān)聽(tīng)是能夠監(jiān)聽(tīng)到火俄。然后我對(duì)以上代碼進(jìn)行修改,再次運(yùn)行看下效果:

image.png

對(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)行分析:

image.png

通過(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é)果如下:

image.png

我們?cè)?code>dealloc方法中進(jìn)行監(jiān)聽(tīng)的移除,然后移除完成后走斷點(diǎn)蜻直,打印輸出可以看到po object_getClassName(self.person)對(duì)象的isa又指會(huì)到KGPerson了锭汛,然后我們?cè)谏弦粋€(gè)界面打印下KGPerson的所有子類,代碼以及結(jié)果如下:

image.png

5袭蝗、從上圖再次證明了唤殴,上面第三條結(jié)論,當(dāng)動(dòng)態(tài)創(chuàng)建子類后到腥,系統(tǒng)會(huì)默認(rèn)緩存子類朵逝。到此我們了解了系統(tǒng)KVO運(yùn)行的一個(gè)基本原理,流程如下:

11.png

當(dāng)我們了解了KVO的原理后乡范,那么我們是否可以自定義實(shí)現(xiàn)KVO呢配名?下面一起研究自定義KVO

自定義模擬KVO

1晋辆、首先我們先對(duì)系統(tǒng)的KVO方法進(jìn)行分析渠脉,通過(guò)查看KVOapi我們可以看到,系統(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)用的地方如下圖所示使用:

image.png

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)化后代碼如下:

image.png
image.png

2、那么使用的時(shí)候就更加簡(jiǎn)單了堂竟,如下:

image.png

3魂毁、從書(shū)寫(xiě)上我們就可以看到,在哪添加的觀察出嘹,就在那實(shí)現(xiàn)回調(diào)席楚,邏輯來(lái)說(shuō)更加清晰,而且去掉了繁瑣的observerkeyPath的判斷税稼,讓代碼更加簡(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)化如下:

image.png

直接去掉了監(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)顾彰,使用如下:

image.png

一行代碼,搞定通過(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市峦朗,隨后出現(xiàn)的幾起案子挡爵,更是在濱河造成了極大的恐慌,老刑警劉巖甚垦,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茶鹃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡艰亮,警方通過(guò)查閱死者的電腦和手機(jī)闭翩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)迄埃,“玉大人疗韵,你說(shuō)我怎么就攤上這事≈斗牵” “怎么了蕉汪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵流译,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我者疤,道長(zhǎng)福澡,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任驹马,我火速辦了婚禮革砸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糯累。我一直安慰自己算利,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布泳姐。 她就那樣靜靜地躺著效拭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胖秒。 梳的紋絲不亂的頭發(fā)上缎患,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音扒怖,去河邊找鬼较锡。 笑死业稼,一個(gè)胖子當(dāng)著我的面吹牛盗痒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播低散,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼俯邓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了熔号?” 一聲冷哼從身側(cè)響起稽鞭,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎引镊,沒(méi)想到半個(gè)月后朦蕴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弟头,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年吩抓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赴恨。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡疹娶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伦连,到底是詐尸還是另有隱情雨饺,我是刑警寧澤钳垮,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站额港,受9級(jí)特大地震影響饺窿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锹安,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一短荐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叹哭,春花似錦忍宋、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至超升,卻和暖如春入宦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背室琢。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工乾闰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盈滴。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓涯肩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親巢钓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子病苗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容