iOS-runtime自定義KVO----分類添加關(guān)鍵屬性



1. KVO原理
2. runtime自定義KVO
3. runtime給分類添加關(guān)聯(lián)屬性
  • 我們注冊監(jiān)聽的時(shí)候,會對注冊者動態(tài)的創(chuàng)建一個(gè)子類對象,然后底層找方法的的isa指針就變成指向新創(chuàng)建的子類對象。當(dāng)改變注冊對象某個(gè)屬性的時(shí)候,就重寫屬性的set方法來進(jìn)行監(jiān)聽。這么說可能理解上不是很明白,下面我們結(jié)合代碼來分析:

  • 自定義
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (KVO)

@property (nonatomic, strong) NSObject *test;

@property (nonatomic, strong) NSTimer *timer;

- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

@end

NS_ASSUME_NONNULL_END


/***********************************************/

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

// timer 與 test 是runtime添加關(guān)聯(lián)屬性代碼, 與自定義kvo無關(guān)
static const char *key_test = "test";
static const char *key_observer = "objc";
static const char *key_timer = "timer";

@implementation NSObject (KVO)

- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {

    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"KVC_%@", oldClassName];
    const char *newName = [newClassName UTF8String];
    Class newClass = objc_allocateClassPair([self class], newName, 0);
    class_addMethod(newClass,@selector(setAge:), (IMP)setAge, "v@:i");
    objc_registerClassPair(newClass);
    
   // (__bridge const void *)@"objc"
    object_setClass(self, newClass);
    objc_setAssociatedObject(self, key_observer, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    Class nclass = NSClassFromString(@"KVO_ViewController");
    
}

- (NSObject *)test {
    NSLog(@"分類添加屬性值test_get:%@",objc_getAssociatedObject(self, key_test));
    return objc_getAssociatedObject(self, key_test);
}

- (void)setTest:(NSObject *)test {
    NSLog(@"分類添加屬性值test_set前:%@",objc_getAssociatedObject(self, key_test));
    objc_setAssociatedObject(self, key_test, test, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"分類添加屬性值test_set后:%@",objc_getAssociatedObject(self, key_test));
}

- (void)setTimer:(NSTimer *)timer {
    objc_setAssociatedObject(self, key_timer, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSTimer *)timer {
    return objc_getAssociatedObject(self, key_timer);
}

void setAge(id self, SEL _cmd, int age) {
    //保存當(dāng)前類
    Class myclass = [self class];

    //將self的isa指針指向父類
    object_setClass(self, class_getSuperclass([self class]));

    //調(diào)用父類
    objc_msgSend(self,@selector(setAge:),age);
    
    //拿出觀察者 (__bridge const void *)@"objc"
    id objc = objc_getAssociatedObject(self, key_observer);
    
    //通知觀察者
    objc_msgSend(objc,@selector(observeValueForKeyPath:ofObject:change:context:),@"age",self,@{},nil);

    //改為子類
    object_setClass(self, myclass);

}

@end


  • 使用
#import "ViewController.h"
#import "NSObject+KVO.h"

@interface ViewController ()


@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // [self addObserver:(nonnull NSObject *) forKeyPath:(nonnull NSString *) options:(NSKeyValueObservingOptions) context:(nullable void *)];
    self.view.backgroundColor = [UIColor whiteColor];
    
    [self kvo_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.age = 20;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"自定義kvo監(jiān)聽到屬性改變");
}

- (void)setAge:(int)age {
    _age = age;
    NSLog(@"方法沒有被覆蓋:%s", __func__);
}

@end


  • 詳解
1. 動態(tài)添加一個(gè)當(dāng)前類的子類

    NSString *oldClassName = NSStringFromClass([self class]);

    NSString *newClassName = [@"AWKVO_" stringByAppendingString:oldClassName];

    const char * newName = [newClassName UTF8String];

    Class myclass = objc_allocateClassPair([self class], newName, 0);

    

    //添加setter方法,相當(dāng)于重寫setter方法, "v@:i" 含義 @: id   : SEL     v : void  

      OC(消息發(fā)送機(jī)制),方法由兩部分組成,方法編號@selector和方法實(shí)現(xiàn)(imp方法指針),先找方法編號再得到方法的指針,再執(zhí)行方法的代碼塊。

    class_addMethod(myclass, @selector(setAge:), (IMP)setAge, "v@:i");

    

    //注冊新添加的這個(gè)類

    objc_registerClassPair(myclass);

    

    //修改被觀察這的isa指針,isa指針指向Person類改成指向myclass這個(gè)類

    object_setClass(self, myclass);


    //將觀察者的屬性保存到當(dāng)前類里面去

    objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}




//相當(dāng)于重寫父類的方法

void setAge(id self, SEL _cmd, int age) {

    

    //保存當(dāng)前類

    Class myclass = [self class];

    

    //將self的isa指針指向父類

    object_setClass(self, class_getSuperclass([self class]));

    

    //調(diào)用父類

    objc_msgSend(self, @selector(setAge:),age);

    

    //拿出觀察者

    objc_getAssociatedObject(self, (__bridge const void *)@"objc");

    

    //通知觀察者

    objc_msgSend(objc,@selector(observeValueForKeyPath:ofObject:change:context:),self,age,nil,nil);

    

    //改為子類

    object_setClass(self, myclass);

}


這樣就可以回調(diào)到ViewContoller中的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:中去,而且相關(guān)的值也傳遞過去了。

總結(jié)自定義一個(gè)KVO思路:

1.自定義一個(gè)類,繼承 [self class]的一個(gè)子類

2.重寫父類的屬性setter方法

3.調(diào)用observeValueForKeyPath:ofObject:change:context:方法,回調(diào)到ViewController中去。


  • 附加:
    • 蘋果為什么要用子類(就是C語言創(chuàng)建的那個(gè)子類)監(jiān)聽setter方法,而不用分類(Person+AWKVO)呢?
    • 回答:原因是當(dāng)你用分類監(jiān)聽setter方法的時(shí)候,Person類中setter方法就不會走了,這樣不好,所以蘋果使用了子類監(jiān)聽setter方法。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容