runtime使用三:自实现kvo

一:kvo 主要原理:
当我们调用系统的addObserver: forKeyPath options: context:方法时候,系统在运行时为我们创建了一个NSKVONotifying_XXX类,该类属于XXX类的子类,这个子类重写了我们要监听的那个属性的set方法,额外的去调用了observeValueForKeyPath: ofObject: change: context:,
我们可以写如下代码验证一下:

 _person = [[Person alloc] init];
 [_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

在调用addObserver: forKeyPath options: context:之前之后我们都在控制台上打印_person对象的isa指针,得到如下答案:

(lldb) po _person->isa
Person

(lldb) po _person->isa
NSKVONotifying_Person

这也验证了为什么我们直接对一个属性赋值以及当为@property的属性重命名setter方法之后不会触发kvo,(额外的kvc会触发,因为kvc底层会调用set方法)

二:自实现kvo
建立NSobject的分类实现自定义kvo
NSObject+kvo.h

@interface NSObject (kvo)

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

- (void)zsh_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

@end

NSObject+kvo.m

@implementation NSObject (kvo)

- (void)zsh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    
    const char *className = object_getClassName(self);
    NSString *newClassStr = [NSString stringWithFormat:@"%s_kvo",className];
    
    Class newClass = objc_allocateClassPair(self.class, newClassStr.UTF8String, 0);
    objc_registerClassPair(newClass);
    
    SEL sel = NSSelectorFromString([NSString stringWithFormat:@"set%@:",[keyPath capitalizedString]]);
    class_addMethod(newClass, sel, (IMP)setAllValue, "v@:@");
    
    object_setClass(self, newClass);
    
    objc_setAssociatedObject(self, @"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(self, @"keyPath", keyPath, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

void setAllValue(id self,SEL _cmd, NSString *newValue) { //NSString *newValue
    
    id observer = objc_getAssociatedObject(self, @"objc");
    NSString *keyPath = objc_getAssociatedObject(self, @"keyPath");
    SEL sel = NSSelectorFromString([NSString stringWithFormat:@"set%@:",[keyPath capitalizedString]]);
    
    Class newClass = [self class];
    object_setClass(self, class_getSuperclass(newClass));
    objc_msgSend(self, sel, newValue);
    object_setClass(self, newClass);
    
    NSMutableDictionary *dic = [[NSMutableDictionary alloc] init];
    [dic setValue:newValue forKey:NSKeyValueChangeNewKey];
    [dic setValue:@"test" forKey:@"zhou"];
    
    objc_msgSend(observer, @selector(zsh_observeValueForKeyPath:ofObject:change:context:),keyPath,observer,dic,nil);
}
@end

在这里我们做一些代码解释:

第一步:zsh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context实现:
object_getClassName ------> 获得某个对象的类名
我们需要创建一个当前类的子类 系统为NSKVONotifying_XXX类,我这里创建的类名为XXX_kvo
objc_allocateClassPair -----> 第一个参数是父类 第二个我想创建的类的类名 第三个暂时不管传0
objc_registerClassPair -----> 注册这个类
————————————————>以上新的一个子类就创建好了
class_addMethod -----> 为这个newClass添加一个方法,这个方法的SEL(选择器)为sel,IMP为setAllValue,"v@?"表示无返回值(第一个v),参数为对象类型(第二个@)、SEL类型(第三个:)、要传递的参数(第四个@)
额外的简单说明一下SEL和IMP
1:SEL和IMP相当于组成了一个表,SEL是索引,IMP是方法实现,它是一个函数指针指向了方法体的首地址(所以我们可知可以改变SEL和IMP的对应关系,来达到改变方法的功能,这就是方法交换的原理)
2:我们的oc方法默认会带有2个参数,第一个是id类型的self,只能该方法的声明方,第二个是SEL,对于这个方法的索引
————————————————>以上是为新的这个类添加方法,该方法的SEL为sel,IMP为setAllValue
object_setClass -----> 将self的isa指针指向newClass(运用了isa交换技术),目的是为了能在newClass中找到这个方法
额外的简单说明
1:一个对象的isa指向它的class,class的isa指向了metaClass,metaClass指向了rootClass(即NSObject)
object_getClass(<#id _Nullable obj#>) 就是获取obj的isa指向
class 对于对象是class,对于class是class,对于metaClass是metaClass
2:我们的对象方法和属性存储在class中,类方法和类属性存储在metaClass中;所以调用一个对象的某个方法,其实是在class结构体中查找这个方法,依次到superClass
————————————————>以上将self的isa指向新的这个类
objc_setAssociatedObject -----> 关联对象,只在保存observer和keyPath以供后续zsh_observeValueForKeyPath:ofObject:change:context:中传递回去
————————————————>zsh_addObserver:forKeyPath:options:context:功能完毕
第二步:处理setXX方法并回调zsh_observeValueForKeyPath:ofObject:change:context:
setAllValue前面2行:拿到关联对象的observer和keyPath
SEL sel = NSSelectorFromString---->得到当前方法的SEL
object_setClass(self, class_getSuperclass(newClass)); —> 将self的isa指针指回自己的class,是为了调用自己的setXX方法
objc_msgSend ----> 调用self的setXX方法
调用完毕isa重指向子类的class
创建change信息并调用observer的zsh_observeValueForKeyPath:ofObject:change:context:这样我们的使用放就能走它的这个方法了
————————————————>完毕

测试:
person.h

@interface Person : NSObject

@property (nonatomic, strong) NSString *name; // ,setter=setzhouName:kvo不能作用到

@property (nonatomic, assign) NSString *avator;

- (void)aa;

@end

person.m

@implementation Person

- (void)setName:(NSString *)name {
    
    _name = name;
    NSLog(@" Person类的setName:方法");
}

- (void)setAvator:(NSString *)avator {
    
    _avator = avator;
     NSLog(@" Person类的setAvator:方法");
}

@end

ViewController.m中调用

@interface ViewController ()

{
    Person *_person;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    _person = [[Person alloc] init];
    
    [_person zsh_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    [_person zsh_addObserver:self forKeyPath:@"avator" options:NSKeyValueObservingOptionNew context:nil];
}

- (IBAction)set:(id)sender {
   
    [_person setName:@"123"];
}
- (IBAction)setAvator:(id)sender {
    
    [_person setAvator:@"qwe"];
}

- (void)zsh_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@" 有值改变了 === observer=%@,keyPath=%@ change=%@",object,keyPath,change);
}

@end

运行打印结果如下:

2019-05-14 17:54:24.918459+0800 Test[36837:2344998]  Person类的setAvator:方法
2019-05-14 17:54:24.918684+0800 Test[36837:2344998]  有值改变了 === observer=<ViewController: 0x7f8aad519d10>,keyPath=avator change={
    new = 123;
    zhou = test;
}
2019-05-14 17:54:26.707345+0800 Test[36837:2344998]  Person类的setAvator:方法
2019-05-14 17:54:26.707466+0800 Test[36837:2344998]  有值改变了 === observer=<ViewController: 0x7f8aad519d10>,keyPath=avator change={
    new = qwe;
    zhou = test;
}

发布了40 篇原创文章 · 获赞 10 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/ai_pple/article/details/90212701