码迷,mamicode.com
首页 > 其他好文 > 详细

KVO底层实现原理,仿写KVO

时间:2017-06-19 10:03:57      阅读:188      评论:0      收藏:0      [点我收藏+]

标签:stat   ase   reg   常见   rem   简单介绍   set   option   pen   

 这篇文章简单介绍苹果的KVO底层是怎么实现的,自己仿照KVO的底层实现,写一个自己的KVO监听

 

#pragma mark--KVO底层实现 

 

 第一步:新建一个Person类继承NSObject

 

Person.h

 

#import <Foundation/Foundation.h>

@interface Person : NSObject

//字符串类型的属性name
@property (nonatomic, strong) NSString *name;

@end

 

Person.m

 

#import "Person.h"

@implementation Person
- (void)setName:(NSString *)name
{ //别问为什么(下面有用处),就是要自己处理set方法
    _name = [NSString stringWithFormat:@"%@aaaa",name];
}
@end

 

 

第二步:在控制器中创建一个Person类型的对象p,利用苹果的KVO来监听该对象p的name属性的变化

 

ViewController.h

 

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

 

ViewController.m

 

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) Person *p;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    Person *p = [[Person alloc] init];
    
    // 监听name属性有没有改变
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    self.p = p;
}

//点击屏幕修改self.p的name属性的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int i = 0;
    i++;
    self.p.name = [NSString stringWithFormat:@"%d",i];
}

// 所有的KVO监听都会来到这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"%@",self.p.name);
}

@end

 

会打印1aaaa,2aaaa,3aaaa...... //从打印可以看出KVO监听的是set方法的调用!!!

 

实际上:KVO的本质就是监听一个对象有没有调用set方法!!!

 

怎么验证呢?

1.将Person.h中的代码修改为

 

#import <Foundation/Foundation.h>

@interface Person : NSObject
{
    @public
    NSString *_name;//为了验证KVO监听的是setter方法
}

@end

 

将ViewController.m的viewDidLoad中的代码修改为

 

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    Person *p = [[Person alloc] init];
    
    // 监听_name有没有改变
    [p addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionNew context:nil];
    self.p = p;
}

 

运行后,点击屏幕是没有任何打印的-->结论1:KVO的本质就是监听一个对象有没有调用set方法!!!

 

第三步:打断点看一下addObserver 到底干了什么事情?

 

技术分享

 

继续走一步:

技术分享

 

这个NSKVONotifying_Person类是什么鬼?

估计你已经猜到了! 没错这个NSKVONotifying_Person类就是系统帮我们实现的!!!

 怎么验证呢? 想到了吗?我们自己把这个类给重写了!看看会发生什么?

第四步:创建NSKVONotifying_Person类

 

NSKVONotifying_Person.h

 

#import <Foundation/Foundation.h>

@interface NSKVONotifying_Person : NSObject

@end

 

NSKVONotifying_Person.m

 

#import "NSKVONotifying_Person.h"

@implementation NSKVONotifying_Person

@end

 

第五步运行:

 

技术分享

 

恭喜猜测没错,验证通过!!! 

 

结论2:系统创建了一个NSKVONotifying_XXX的类

结论3:修改了对象p的isa指针

 

那么系统帮我们创建的NSKVONotifying_XXX类有没有继承自我们自己常见的XXX类呢?

我既然这么问,肯定是继承啦!!因为我们已经把属性对应的set方法给重写了!!!因为已经修改了对象的指针(用户调用对象p的方法就不是Person的方法了,是isa指针指向的类的对应的方法),如果不继承的话,相当于把用户重写的set方法给覆盖了,用户调用自己写的set方法就不起作用了!!!也就有了 结论4:重写对应的set方法再内部实现父类做法,通知观察者

 

 KVO底层实现结论:(都是系统自动帮我们实现的)

 1> 创建NSKVONotifying_XXX的类

 2> 重写对应属性的set方法,在内部实现父类做法,通知观察者

 3> 修改当前对象的isa指针,指向创建的NSKVONotifying_XXX(这样实际调用的时候就会走NSKVONotifying_XXX类中对应的方法)

 

 

 

 #pragma mark--仿写KVO实现

 

知道了KVO底层实现的原理就可以仿照KVO写一个自己的"KVO"了 

步骤一:给NSObject写一个分类

 

NSObject+KVO.h

 

#import <Foundation/Foundation.h>

@interface NSObject (KVO)

//添加监听
- (void)ey_addObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

//移除监听
- (void)ey_removeObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath context:(nullable void *)context;

@end

 

NSObject+KVO.m

 

    //
    //  NSObject+KVO.m
    //  KVO底层实现
    //
    //  Created by lieryang on 2017/6/17.
    //  Copyright ? 2017年 lieryang. All rights reserved.
    //

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

static NSString * const EYKVONotifying_ = @"EYKVONotifying_";
static NSString * const observerKey = @"observer";
@implementation NSObject (KVO)

    //添加监听
- (void)ey_addObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
    if (keyPath.length == 0) {//如果传进来的keyPath为@""或者为nil 直接返回
        return;
    }
    
    // 1. 检查对象的类有没有相应的 setter 方法。
    SEL setterSelector = NSSelectorFromString([self setterForGetter:keyPath]);
    
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    
    if (!setterMethod) {//如果没有直接返回,不需要任何处理
        NSLog(@"找不到该方法");
        return;
    }
    
    // 2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类
    Class clazz = object_getClass(self);
    NSString *className = NSStringFromClass(clazz);
    
    if (![className hasPrefix:EYKVONotifying_]) {
        clazz = [self ey_KVOClassWithOriginalClassName:className];
        object_setClass(self, clazz);
    }
    
    // 3. 为EYKVONotifying_XXX添加setter方法的实现
    const char *types = method_getTypeEncoding(setterMethod);
    class_addMethod(clazz, setterSelector, (IMP)ey_setter, types);
    
    // 4. 添加该观察者到观察者列表中
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(observerKey));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(observerKey), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    if ([observers indexOfObject:observer] == NSNotFound) {
        [observers addObject:observer];
    }
}

    //移除监听
- (void)ey_removeObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath context:(nullable void *)context
{
    
    NSMutableArray* observers = objc_getAssociatedObject(self, (__bridge const void *)(observerKey));
    
    [observers removeObject:observer];
}
#pragma mark - 注册自己的EYKVONotifying_XXX
- (Class)ey_KVOClassWithOriginalClassName:(NSString *)className
{
    // 生成EYKVONotifying_XXX的类名
    NSString *kvoClassName = [EYKVONotifying_ stringByAppendingString:className];
    Class kvoClass = NSClassFromString(kvoClassName);
    
    // 如果EYKVONotifying_XXX已经被注册过了, 则直接返回
    if (kvoClass) {
        return kvoClass;
    }
    
    // 如果EYKVONotifying_XXX不存在, 则创建这个类
    Class originClass = object_getClass(self);
    kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);
    
    // 修改EYKVONotifying_XXX方法的实现, 学习Apple的做法, 隐瞒这个EYKVONotifying_XXX
    Method classMethod = class_getInstanceMethod(kvoClass, @selector(class));
    const char *types = method_getTypeEncoding(classMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)ey_class, types);
    
    // 注册EYKVONotifying_XXX
    objc_registerClassPair(kvoClass);
    
    return kvoClass;
}

Class ey_class(id self, SEL cmd)
{
    Class clazz = object_getClass(self); // EYKVONotifying_XXX
    Class superClazz = class_getSuperclass(clazz); // origin_class
    return superClazz; // origin_class
}

/**
 *  重写setter方法, 新方法在调用原方法后, 通知每个观察者
 */
static void ey_setter(id self, SEL _cmd, id newValue)
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = [self getterForSetter:setterName];
    
    if (!getterName) {
        NSLog(@"找不到getter方法");
    }
    
    // 调用原类的setter方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 这里需要做个类型强转, 否则会报too many argument的错误
    ((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClazz, _cmd, newValue);
    
    // 找出观察者的数组
    NSArray *observers = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(observerKey));
        // 遍历数组
    for (id observer in observers) {
        // 调用监听者observer的observeValueForKeyPath 方法,因为observer为id类型,所以就偷懒调用了系统的监听回调,要是自己定义方法,会报找方法的错误,可以在添加监听的时候,传进来一个block代码块,在此处回调block,更方便外界的调用
        [observer observeValueForKeyPath:getterName ofObject:self change:nil context:nil];
    }
}

#pragma mark - 生成对应的setter方法字符串
- (NSString *)setterForGetter:(NSString *)key
{
    // 1. 首字母转换成大写
    NSString * firstString = [[key substringToIndex:1] uppercaseString];
    // 2. 剩下的字母
    NSString * remainingString = [key substringFromIndex:1];
    
    // 3. 最前增加set, 最后增加: setName:
    NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstString, remainingString];
    
    return setter;
    
}
#pragma mark - 生成对应的getter方法字符串
- (NSString *)getterForSetter:(NSString *)key
{
    // setName
    if (key.length <=0 || ![key hasPrefix:@"set"] || ![key hasSuffix:@":"]) {
        return nil;
    }
    
    // 移除set和:
    NSRange range = NSMakeRange(3, key.length - 4);
    NSString *getter = [key substringWithRange:range];
    
    // 小写
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1)
                                       withString:firstString];
    
    return getter;
}
@end

 

 

外界使用

创建Person类

Person.h

 

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, copy) NSString * name;

@end

 

Person.m

 

#import "Person.h"

@implementation Person

@end

 

 

#import "ViewController.h"
#import "Person.h"
#import "NSObject+KVO.h"

@interface ViewController ()

@property (nonatomic, strong) Person * p;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person * p = [[Person alloc] init];
    [p ey_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    self.p = p;
}

// 监听的回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"%@", self.p.name);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    
    self.p.name = [NSString stringWithFormat:@"%u", arc4random_uniform(20)];
}

@end

 

 

 

  更多内容--> 博客导航 每周一篇哟!!!

 

 

有任何关于iOS开发的问题!欢迎下方留言!!!或者邮件lieryangios@126.com 虽然我不一定能够解答出来,但是我会请教iOS开发高手!!!解答您的问题!!!

KVO底层实现原理,仿写KVO

标签:stat   ase   reg   常见   rem   简单介绍   set   option   pen   

原文地址:http://www.cnblogs.com/CoderEYLee/p/Object-C-0025.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!