自定義KVO

首先了解一下系統(tǒng)的KVO實(shí)現(xiàn)原理:其實(shí)就是動(dòng)態(tài)的創(chuàng)建了一個(gè)被觀察者的子類表牢,然后動(dòng)態(tài)修改它的isa指針指向它的子類盖喷,在子類里重寫屬性的set方法夯秃,最后在set方法里監(jiān)聽屬性變化愤惰,并發(fā)出通知包归。

1锨推、驗(yàn)證系統(tǒng)原理:

image.png

image.png

打個(gè)斷點(diǎn),發(fā)現(xiàn)實(shí)例s的isa是指向Student的公壤,然后單步執(zhí)行一下换可,
image.png

image.png

這時(shí)候發(fā)現(xiàn)實(shí)例s的isa指針變成了NSKVONotifying_Student,由此可見厦幅,當(dāng)我們添加觀察者的時(shí)候沾鳄,系統(tǒng)動(dòng)態(tài)的創(chuàng)建了一個(gè)子類NSKVONotifying_Student,并把s的類型修改成了它确憨。

2译荞、接下來我們自己用RunTime來仿照系統(tǒng)KVO原理來自己寫一個(gè)KVO

首先創(chuàng)建一個(gè)NSObject的分類NSObject+KVO,然后自己實(shí)現(xiàn)一個(gè)監(jiān)聽方法缚态。

#import "NSObject+KVO.h"
#import <objc/message.h>

static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";

@implementation NSObject (KVO)
-(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    
    Class newClass = [self createClass:keyPath];

    object_setClass(self, newClass);
    
    // 4.將觀察者與對象綁定
    objc_setAssociatedObject(self, SJKVOAssiociateKey, observer, OBJC_ASSOCIATION_ASSIGN);
    
}
- (Class) createClass:(NSString*) keyPath {
    
    // 1. 拼接子類名 / SJKVO_Student
    NSString* oldName = NSStringFromClass([self class]);
    NSString* newName = [NSString stringWithFormat:@"SJKVO_%@", oldName];
    
    // 2. 創(chuàng)建并注冊類
    Class newClass = NSClassFromString(newName);
    if (!newClass) {
        
        // 創(chuàng)建并注冊類
        newClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
        objc_registerClassPair(newClass);
        
        // 動(dòng)態(tài)添加方法
        // class
        Method classMethod = class_getInstanceMethod([self class], @selector(class));
        const char* classTypes = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, @selector(class), (IMP)SJ_class, classTypes);

    }
    
    // setter
    NSString* setterMethodName = getSetter(keyPath);
    SEL setterSEL = NSSelectorFromString(setterMethodName);
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char* setterTypes = method_getTypeEncoding(setterMethod);
    
    class_addMethod(newClass, setterSEL, (IMP)SJ_setKey, setterTypes);

    return newClass;
}

Class SJ_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

void SJ_setKey(id self, SEL _cmd, id newValue) {
    struct objc_super oldSuper = {self,class_getSuperclass([self class])};
    // 修改屬性值
    objc_msgSendSuper(&oldSuper, _cmd, newValue);
    // 拿出觀察者
    id observer = objc_getAssociatedObject(self, SJKVOAssiociateKey);
    NSLog(@"---%@",newValue);
    
    // 調(diào)用observer
    NSString *methodName = NSStringFromSelector(_cmd);
    NSString *key = getValueKey(methodName);
    
    objc_msgSend(observer, sel_registerName("observeValueForKeyPath:ofObject:change:context:"),key,self,@{key:newValue},nil);
    
//    [observer observeValueForKeyPath:key ofObject:self change:@{key:newValue} context:nil];
}
// key -> setter
static NSString  * getSetter(NSString *keyPath){
    
    if (keyPath.length <= 0) { return nil; }
    
    NSString *firstString = [[keyPath substringToIndex:1] uppercaseString];
    NSString *leaveString = [keyPath substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

// cmd -> key
NSString* getKey(NSString * cmd) {
    if (cmd.length <= 0 || ![cmd hasPrefix:@"set"] || ![cmd hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, cmd.length-4);
    NSString *getter = [cmd substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
    
    return getter;
    
}

@end

然后在用我們自己的方法來添加一下觀察者磁椒,看是否能觀察到

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Student *s = [[Student alloc] init];
    
//    [s addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    
    [s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    
    self.s = s;
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"+++%@",self.s.age);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static NSInteger i = 0;
    i++;
    self.s.age = [NSString stringWithFormat:@"%ld",i];
}
image.png

通過打印可以看出跟系統(tǒng)的效果一樣。

基本都有注釋玫芦,我就不詳解了浆熔,里邊一些關(guān)于runtime的動(dòng)態(tài)函數(shù)和消息轉(zhuǎn)發(fā)函數(shù),在我之前關(guān)于runtime的文章里都有詳解,不了解可以參照前幾篇文章做基礎(chǔ)医增。

3慎皱、用block回調(diào)

用系統(tǒng)的回調(diào)會一個(gè)問題,就是如果觀察的屬性多了叶骨,在回調(diào)方法里需要先判斷是哪個(gè)對象的哪個(gè)屬性茫多,比較麻煩,但是用block回調(diào)的話忽刽,就省去了這些麻煩天揖,并且代碼邏輯更清晰,更緊湊跪帝。接下來代碼多了今膊,我們順便整理封裝一下。

首先定義一個(gè)block和一個(gè)可以有block的構(gòu)造方法:

typedef void(^ValueChangeBlock)(id observer, NSString* keyPath, id oldValue, id newValue);

@interface NSObject (KVO)
// 系統(tǒng)回調(diào)
- (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

// block回調(diào)
- (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock;

@end

然后在.m文件內(nèi)部創(chuàng)建一個(gè)類伞剑,來存儲要監(jiān)聽的信息:

static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";

@interface SJInfo : NSObject

@property (nonatomic, weak) NSObject* observer;
@property (nonatomic, strong) NSString* keyPath;
@property (nonatomic, copy) ValueChangeBlock valueChangeBlock;

@end

@implementation SJInfo

- (instancetype) initWithObserver:(NSObject*)observer forKeyPath:(NSString*) keyPath valueChangeBlock:(ValueChangeBlock) block {
    if (self == [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _valueChangeBlock = block;
    }
    return self;
}

@end

然后在創(chuàng)建是就把參數(shù)和block保存到數(shù)組里斑唬,以便下邊拿出來調(diào)用:

-(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock {

    Class newClass = [self createClass:keyPath];

    object_setClass(self, newClass);

    // 信息保存
    SJInfo* info = [[SJInfo alloc] initWithObserver:observer forKeyPath:keyPath valueChangeBlock:valueChangeBlock];
    NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
    if (!array) {
        array = [NSMutableArray array];
        objc_setAssociatedObject(self, SJKVOAssiociateKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [array addObject:info];

}

void SJ_setKey(id self, SEL _cmd, id newValue) {
    
    struct objc_super oldSuper = {self,class_getSuperclass(object_getClass(self))};
    
    // 獲取key
    NSString *key = getKey(NSStringFromSelector(_cmd));
    
    // 獲取舊值
    id oldValue = objc_msgSendSuper(&oldSuper, NSSelectorFromString(key));
    
    // 修改屬性值
    objc_msgSendSuper(&oldSuper, _cmd, newValue);
    
    NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
    if (array) {
        for (SJInfo* info in array) {
            if ([info.keyPath isEqualToString:key]) {
                info.valueChangeBlock(info.observer, key, oldValue, newValue);
                return;
            }
        }
    }
}

在setKey方法里拿出數(shù)組中的信息,找到對應(yīng)的key黎泣,然后block回調(diào)就可以了恕刘,這樣就可以同時(shí)監(jiān)聽多個(gè)屬性了。

銷毀觀察者

其實(shí)銷毀觀察者就是把isa指針指從子類回到原來就可以了抒倚,我們把他放在dealloc方法里來做比較合適褐着,有兩種方法:1.利用hook在創(chuàng)建子類的方法里做方法交換,把dealloc的方法實(shí)現(xiàn)指向自己的方法里衡便,然后做isa指回献起;2.也是在創(chuàng)建子類的時(shí)候同時(shí)動(dòng)態(tài)添加自己的dealloc方法來做,我們這里就用第二種實(shí)現(xiàn)一下:

放在子類的創(chuàng)建方法里镣陕,保證只創(chuàng)建一次

        // 添加SJ_Dealloc谴餐,銷毀觀察者
        SEL deallocSEL = NSSelectorFromString(@"dealloc");
        Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
        const char* deallocTypes = method_getTypeEncoding(deallocMethod);
        class_addMethod(newClass, deallocSEL, (IMP)SJ_Dealloc, deallocTypes);
void SJ_Dealloc(id self, SEL _cmd) {
    // 父類
    Class superClass = [self class];//class_getSuperclass(object_getClass(self));
    
    object_setClass(self, superClass);

}

最后在外邊調(diào)用一下,非常方便

    [s SJ_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
        NSLog(@"oldValue ---- %@, newValue ---- %@", oldValue, newValue);
    }];
    
    [s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
        NSLog(@"oldValue ++++ %@, newValue ++++ %@", oldValue, newValue);
    }];

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呆抑,一起剝皮案震驚了整個(gè)濱河市岂嗓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鹊碍,老刑警劉巖厌殉,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侈咕,居然都是意外死亡公罕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門耀销,熙熙樓的掌柜王于貴愁眉苦臉地迎上來楼眷,“玉大人,你說我怎么就攤上這事」蘖” “怎么了掌腰?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長张吉。 經(jīng)常有香客問我齿梁,道長,這世上最難降的妖魔是什么肮蛹? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任勺择,我火速辦了婚禮,結(jié)果婚禮上蔗崎,老公的妹妹穿的比我還像新娘酵幕。我一直安慰自己,他們只是感情好缓苛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著邓深,像睡著了一般未桥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芥备,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天冬耿,我揣著相機(jī)與錄音,去河邊找鬼萌壳。 笑死亦镶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的袱瓮。 我是一名探鬼主播缤骨,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尺借!你這毒婦竟也來了绊起?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤燎斩,失蹤者是張志新(化名)和其女友劉穎虱歪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栅表,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡笋鄙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怪瓶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萧落。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铐尚,到底是詐尸還是另有隱情拨脉,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布宣增,位于F島的核電站玫膀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏爹脾。R本人自食惡果不足惜帖旨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望灵妨。 院中可真熱鬧解阅,春花似錦、人聲如沸泌霍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朱转。三九已至蟹地,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間藤为,已是汗流浹背怪与。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缅疟,地道東北人分别。 一個(gè)月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像存淫,于是被迫代替她去往敵國和親耘斩。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

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

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,393評論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,104評論 1 32
  • 自己實(shí)現(xiàn)kvo之前纫雁,需要知道iOS系統(tǒng)對kvo的實(shí)現(xiàn)煌往。 系統(tǒng)實(shí)現(xiàn)kvo的原理 這依賴了OC強(qiáng)大的runtime特性...
    mws100閱讀 2,779評論 6 3
  • 你好熱熱熱
    古箏風(fēng)閱讀 52評論 0 0
  • 有關(guān)鯽魚,記憶中轧邪,小的時(shí)候經(jīng)常吃刽脖,每每做到魚湯,爸媽總會先舀出一碗魚湯讓我先喝下去忌愚,依舊到現(xiàn)在也是如此曲管。可以說硕糊,我...
    獅子貓2017閱讀 257評論 0 1