美文网首页iOS
iOS-底层原理21:KVO底层原理

iOS-底层原理21:KVO底层原理

作者: AcmenL | 来源:发表于2020-12-17 17:21 被阅读0次

    上一篇文章iOS-底层原理20:KVC底层原理中了解了KVC底层原理,本文将讲解KVO底层原理。

    结合上一篇文章有个问题

    问题: 通过KVC修改属性会触发KVO吗

    带着问题开始探索

    1. 定义

    KVO,全称为Key-Value observing,中文名为键值观察,KVO是一种机制,它允许将其他对象的指定属性的更改通知给对象官方文档

    2. KVO 使用注意事项

    2.1 基本使用

    KVO的基本使用主要分为3步:

    step1: 向观察对象注册观察者 addObserver:forKeyPath:options:context:

    • Options
    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew = 0x01,    //提供更改后的值
        NSKeyValueObservingOptionOld = 0x02,    //提供更改前的值
        NSKeyValueObservingOptionInitial,     //初始值,在注册观察服务时会调用一次触发方法
        NSKeyValueObservingOptionPrior   //分别在值修改前后触发方法,NSKeyValueChangeNotificationIsPriorKey可以来判断在前调用还是在后调用
    }
    
    • Context

    翻译成中文

    大致含义就是:addObserver:forKeyPath:options:context: 方法中的上下文context指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以通过指定context为NULL,从而依靠keyPath键路径字符串传来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的keyPath创建一个不同的context,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析

    通俗的讲,context上下文主要是用于区分不同对象的同名属性,从而在KVO回调方法中可以直接使用context进行区分,可以大大提升性能,以及代码的可读性

    context使用总结
    • 不使用context,使用keyPath区分通知来源
    //context的类型是 nullable void *,应该是NULL,而不是nil
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
    
    • 使用context区分通知来源
    //定义context
    static void *PersonNickContext = &PersonNickContext;
    static void *PersonNameContext = &PersonNameContext;
    
    //注册观察者
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
        
        
    //KVO回调
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        if (context == PersonNickContext) {
            NSLog(@"%@",change);
        }else if (context == PersonNameContext){
            NSLog(@"%@",change);
        }
    }
    

    step2: 接收更改通知消息 observeValueForKeyPath:ofObject:change:context:

    • change
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
         NSKeyValueChangeSetting = 1,      //赋值
         NSKeyValueChangeInsertion = 2,     //添加值
         NSKeyValueChangeRemoval = 3,     //移除值
         NSKeyValueChangeReplacement = 4,  //替换值
    };
    //kind,表示动作的种类,值为上面这个枚举
    NSKeyValueChangeKey const NSKeyValueChangeKindKey; 
    //new, 更改后的值
    NSKeyValueChangeKey const NSKeyValueChangeNewKey; 
    //old, 更改前的值
    NSKeyValueChangeKey const NSKeyValueChangeOldKey;  
    //indexes,数组等改变的index信息
    NSKeyValueChangeKey const NSKeyValueChangeIndexesKey; 
    //notificationIsPrior,这个用于NSKeyValueObservingOptionPrior用于区分是更改前的触发通知还是更改后的触发通知
    NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey; 
    

    step3: 注销观察者 removeObserver:forKeyPath:context:

    删除观察者时,请记住以下几点:

    • 如果观察者未注册调用移除将会导致NSRangeException。 成对调用addObserver:forKeyPath:options:context:removeObserver:forKeyPath:context:,或将removeObserver:forKeyPath:context:放入try/catch块内处理潜在的异常。

    • 释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,你可以确保观察者在从内存中消失之前将自己删除

    • 该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在init或viewDidLoad中注册为观察者,并在释放过程中(通常在dealloc中注销,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来

    移除观察者Demo

    step1: 新建一个LBHPerson类和一个LBHStudent

    /********LBHPerson********/
    //.h
    @interface LBHPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, copy) NSString *nick;
    @end
    
    //.m
    @implementation LBHPerson
    
    @end
    
    /********LBHStudent********/
    
    //.h
    @interface LBHStudent : LBHPerson
    
    + (instancetype)shareInstance;
    
    @end
    
    //.m
    @implementation LBHStudent
    
    static LBHStudent* _instance = nil;
    + (instancetype)shareInstance{
        static dispatch_once_t onceToken ;
        dispatch_once(&onceToken, ^{
            _instance = [[super allocWithZone:NULL] init] ;
        }) ;
        return _instance ;
    }
    @end
    

    step2: 有一个嵌套在UINavigationController中的ViewController类,有一个导航按钮可以push到下一个类LBHViewController

    @interface ViewController ()
    
    @property (nonatomic, strong) LBHStudent *student;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.student = [LBHStudent shareInstance];
    }
    

    step3: LBHViewController

    @interface LBHViewController ()
    
    @property (nonatomic, strong) LBHStudent *student;
    
    @end
    
    @implementation LBHViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.student = [LBHStudent shareInstance];
    
        [self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.student.name = [NSString stringWithFormat:@"%@+",self.student.name];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        // fastpath
        // 性能 + 代码可读性
        NSLog(@"%@",change);
    }
    
    @end
    

    step4: 运行,跳转到LBHViewController,返回,继续跳转到LBHViewController,点击屏幕,崩溃了

    问题: 为什么会崩溃?

    解答: 第一次进入LBHViewController注册了KVO观察者,返回上一个页面时没有移除监听,再次进入会重新注册KVO观察者,而且第一次的通知对象还在内存中,没有进行释放,点击屏幕属性发生变化,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听

    【注】:这里的崩溃案例是通过单例对象实现,因为单例对象在内存是常驻的,针对一般的类对象,貌似不移除也是可以的,但是为了防止线上意外,建议还是移除比较好

    解决方法

    LBHViewControllerdealloc方法中加上移除监听

    - (void)dealloc{
        [self.student removeObserver:self forKeyPath:@"name" context:NULL];
    }
    

    2.2 KVO的自动触发与手动触发

    KVO观察的开启关闭有两种方式,自动手动

    • 自动开关,返回NO,就监听不到,返回YES,表示监听
    // 自动开关
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
        return YES;
    }
    
    • 自动开关关闭的时候,可以通过手动开关监听,重写监听属性的set方法
    - (void)setName:(NSString *)name{
        //手动开关
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
    
    测试

    step1: 在上面demo中,LBHPerson中添加automaticallyNotifiesObserversForKey方法返回一个NO

    // 自动开关
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
        return NO;
    }
    

    step2: 运行

    没有任何打印,说明并没有监听

    step3:LBHPerson中添加如下方法

    - (void)setName:(NSString *)name
    {
        [self willChangeValueForKey:@"name"];   //即将改变
        _name = name;
        [self didChangeValueForKey:@"name"];   //已改变
    }
    

    step4: 打印正常

    2.3 KVO观察:一对多

    KVO观察中的一对多,意思是通过注册一个KVO观察者,可以监听多个属性的变化

    以下载进度为例,比如目前有一个需求,需要根据总的下载量totalData当前下载量writtenData 来计算当前的下载进度downloadProgress,实现有两种方式:

    1、 分别观察 总的下载量totalData当前下载量writtenData 两个属性,当其中一个发生变化 重新计算当前下载进度currentProcess

    2、 实现keyPathsForValuesAffectingValueForKey方法,将两个观察合为一个观察,即观察当前下载进度currentProcess

    // LBHPerson
    @interface LBHPerson : NSObject
    @property (nonatomic, copy) NSString *downloadProgress;
    @property (nonatomic, assign) double writtenData;
    @property (nonatomic, assign) double totalData;
    @end
    
    @implementation LBHPerson
    
    - (NSString *)downloadProgress {
        if (self.writtenData == 0) {
            self.writtenData = 10;
        }
        if (self.totalData == 0) {
            self.totalData = 100;
        }
        return [NSString stringWithFormat:@"%.2f", 1.0f * self.writtenData / self.totalData];
    }
    
    // 下载进入 writtenData / totalData
    +(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        
        if ([key isEqualToString:@"downloadProgress"]) {
            NSArray * affectingKeys = @[@"totalData", @"writtenData"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    @end
    
    // ViewController
    @interface ViewController ()
    @property (nonatomic, strong) LBHPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person = [LBHPerson new];
    
        // 添加监听
        [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
        
    }
    
    // 处理监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@: %@", keyPath, change);
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.person.writtenData += 20;
        self.person.totalData +=10;
    }
    
    -(void)dealloc {
        // 移除监听
        [self.person removeObserver:self forKeyPath:@"downloadProgress" context: NULL];
    }
    
    @end
    

    运行结果

    两个属性在点击时都有赋值,所以每次打印两条

    2.4 KVO 观察可变数组

    可变数组如果直接添加数据,是不会调用setter方法的,所有对可变数组的KVO观察下面这种方式不生效的,即直接通过[self.person.dateArray addObject:@"1"];向数组添加元素,是不会触发kvo通知回调的

    2.4.1 官方文档说明

    翻译成中文

    2.4.2 测试代码
    // LBHPerson
    @interface LBHPerson : NSObject
    @property (nonatomic, strong) NSMutableArray *dateArray;
    @end
    
    @implementation LBHPerson
    
    - (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
        [self.dateArray insertObject:object atIndex:index];
    }
    
    @end
    
    // ViewController
    @interface ViewController ()
    @property (nonatomic, strong) LBHPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person = [LBHPerson new];
    
        self.person.dateArray = [NSMutableArray new];
        
        // 添加监听
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    
        [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
    }
    
    // 处理监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@: %@", keyPath, change);
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     
        [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    
        //集合相关API
        [[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"2" atIndex:0];
        [[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"3" atIndexedSubscript:0];
        [[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
    }
    
    -(void)dealloc {
        // 移除监听
        [self.person removeObserver:self forKeyPath:@"name" context: NULL];
        [self.person removeObserver:self forKeyPath:@"dateArray" context: NULL];
    }
    
    @end
    

    运行

    其中的kind表示键值变化的类型,是一个枚举,主要有以下4种

    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,//设值
        NSKeyValueChangeInsertion = 2,//插入
        NSKeyValueChangeRemoval = 3,//移除
        NSKeyValueChangeReplacement = 4,//替换
    };
    

    3 KVO 底层原理探索

    • KVO是使用isa-swizzling的技术实现的。

    • 顾名思义,isa指针指向维护分配表的对象的类。该分派表实质上包含指向该类实现的方法的指针以及其他数据。

    • 当为对象的属性注册观察者时,将修改观察对象的isa指针指向中间类而不是真实类。结果,isa指针的值不一定反映实例的实际类

    • 您永远不应依靠isa指针来确定类成员身份。相反,您应该使用class方法来确定对象实例的类。

    3.1 KVO 只对属性观察

    3.1.1 测试

    LBHPerson 中创建一个成员变量nickName 和一个属性name,然后分别注册KVO

    // LBHPerson
    @interface LBHPerson : NSObject
    {
    @public NSString * nickName;
    }
    @property (nonatomic, copy) NSString *name;
    @end
    
    @implementation LBHPerson
    
    @end
    
    // ViewController
    @interface ViewController ()
    @property (nonatomic, strong) LBHPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person = [LBHPerson new];
    
        self.person.name = @"liu";
        
        // 添加监听
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
        [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
    }
    
    // 处理监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@: %@", keyPath, change);
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.person.name = [NSString stringWithFormat:@"%@name+", self.person.name];
        self.person->nickName = [NSString stringWithFormat:@"%@nickName+", self.person->nickName];
    }
    
    -(void)dealloc {
        // 移除监听
        [self.person removeObserver:self forKeyPath:@"name" context: NULL];
        [self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
    }
    
    @end
    

    运行

    多次打印结果都只有属性,并没有出现成员变量的打印,说明成员变量并没有监听 。

    结论:KVO对成员变量不观察,只对属性观察,属性和成员变量的区别在于属性多一个 setter 方法KVO观察的就是setter 方法

    3.2 中间类

    根据官方文档所述,在注册KVO观察者后观察对象的isa指针指向会发生改变

    注册观察者之前:

    注册观察者之后:

    在注册观察者之后,对象的isa指针发生变化指向的类也发生了改变

    p self.person.class得到的却是LBHPersonNSKVONotifying_LBHPersonLBHPerson是什么关系?

    3.2.1 NSKVONotifying_LBHPersonLBHPerson类 的关系

    可以通过下面封装的方法,获取LBHPerson的相关类

    #import <objc/runtime.h>
    
    #pragma mark - 遍历类以及子类
    - (void)printClasses:(Class)cls{
        
        // 注册类的总数
        int count = objc_getClassList(NULL, 0);
        // 创建一个数组, 其中包含给定对象
        NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
        // 获取所有已注册的类
        Class* classes = (Class*)malloc(sizeof(Class)*count);
        objc_getClassList(classes, count);
        for (int i = 0; i<count; i++) {
            if (cls == class_getSuperclass(classes[i])) {
                [mArray addObject:classes[i]];
            }
        }
        free(classes);
        NSLog(@"classes = %@", mArray);
    }
    
    //********调用********
    [self printClasses:[LBHPerson class]];
    

    从结果中可以说明NSKVONotifying_LBHPersonLBHPerson的子类

    3.2.2 中间类有些什么?

    我们添加遍历IvarsPropertyMethod的函数:

    /// 遍历Ivars
    -(void)printIvars: (Class)cls {
        
        // 仿写Ivar结构
        typedef struct LBH_ivar_t {
            int32_t *offset;
            const char *name;
            const char *type;
            uint32_t alignment_raw;
            uint32_t size;
        }LBH_ivar_t;
        
        // 记录函数个数
        unsigned int count = 0;
        // 读取函数列表
        Ivar * ivars = class_copyIvarList(cls, &count);
        for (int i = 0; i < count; i++) {
            LBH_ivar_t * ivar = (LBH_ivar_t *) ivars[i];
            NSLog(@"ivar: %@", [NSString stringWithUTF8String: ivar->name]);
        }
        free(ivars);
        
    }
    
    /// 遍历属性
    -(void) printProperties: (Class)cls {
        
        // 仿写objc_property_t结构
        typedef struct LBH_property_t{
            const char *name;
            const char *attributes;
        }LBH_property_t;
        
        // 记录函数个数
        unsigned int count = 0;
        // 读取函数列表
        objc_property_t * props = class_copyPropertyList(cls, &count);
        for (int i = 0; i < count; i++) {
            LBH_property_t * prop = (LBH_property_t *)props[i];
            NSLog(@"property: %@", [NSString stringWithUTF8String:prop->name]);
        }
        free(props);
        
    }
    
    /// 遍历方法
    -(void) printMethodes: (Class)cls {
        
        // 记录函数个数
        unsigned int count = 0;
        // 读取函数列表
        Method *methodList = class_copyMethodList(cls, &count);
        for (int i = 0; i < count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = class_getMethodImplementation(cls, sel);
            NSLog(@"method: %@-%p", NSStringFromSelector(sel), imp);
        }
        free(methodList);
    }
    

    在注册KVO观察者后,分别对 LBHPerson本类NSKVONotifying_LBHPerson派生类IvarsPropertyMethod进行打印

    NSLog(@"------- NSKVONotifying_LBHPerson --------");
    [self printMethodes: objc_getClass("NSKVONotifying_LBHPerson")];
    [self printIvars: objc_getClass("NSKVONotifying_LBHPerson")];
    [self printProperties: objc_getClass("NSKVONotifying_LBHPerson")];
    NSLog(@"------- LBHPerson --------");
    [self printMethodes: self.person.class];
    [self printIvars: self.person.class];
    [self printProperties: self.person.class];
    

    打印结果

    NSKVONotifying_LBHPerson的方法是继承还是重写?

    新建一个LBHPerson的子类LBHStudent

    NSLog(@"------- NSKVONotifying_LBHPerson --------");
    [self printMethodes: objc_getClass("NSKVONotifying_LBHPerson")];
    [self printIvars: objc_getClass("NSKVONotifying_LBHPerson")];
    [self printProperties: objc_getClass("NSKVONotifying_LBHPerson")];
    NSLog(@"------- LBHPerson --------");
    [self printMethodes: self.person.class];
    [self printIvars: self.person.class];
    [self printProperties: self.person.class];
    NSLog(@"------- LBHStudent --------");
    [self printMethodes: LBHStudent.class];
    [self printIvars: LBHStudent.class];
    [self printProperties: LBHStudent.class];
    

    打印

    结论

    • 直接继承的子类,没有任何方法和属性。
    • KVO派生类继承自LBHPerosn,重写了setNameclassdealloc方法,新增了_isKVOA方法
    3.2.3 KVO派生类给父类属性赋值

    step1:addObserver处添加断点,运行代码到此处时,lldb输入:watchpoint set variable self->_person->_name

    step2: 设置成功后,继续运行代码,点击屏幕触发touchesBegan事件,会进入汇编页面(观察到设置属性断点处)

    查看堆栈,会有些发现

    可以观察到,当派生类在调用willChangedidChange中间,调用了[LBHPerson setName]方法,完成了给父类LBHPerson的name属性赋值。(此时的willChange和didChange方法是继承自NSObject的)

    3.2.4 KVO派生类何时移除,是否真移除?

    step1:dealloc函数中,移除KVO处添加一个断点

    移除观察者之前:实例对象的isa指向仍是NSKVONotifying_LBHPerson中间类

    step2: 执行一步,继续查看实例对象的isa指向

    移除观察者之后:实例对象的isa指向更改为LBHPerson

    问题: 那中间类是否被删除?

    解答: 我们打印LBHPerson及其子类

    打印LBHPerosn类和子类的信息,发现NSKVONotifying_LBHPerson派生类并没有移除。

    中间类一旦生成,没有移除,没有销毁,这样可以减少频繁的添加操作。

    【总结】

    1、添加addObserver时,创建了派生类,派生类是当前类的子类,重写了被监听属性的setter方法,并将当前类的isa指向了派生类

    2、赋值: 派生类重写了被监听属性的setter方法,在派生类的setter方法触发时:在willChange之后didChange之前,调用父类属性settter方法,完成父类属性的赋值。

    3、移除: 在removeObserver后,isa从派生类指回本类, 但创建过的派生类,会一直在内存中不会销毁。

    4 自定义 KVO

    相关代码下载

    自定KVO的目的

    1、模拟系统实现KVO原理
    2、自动移除观察者
    3、实现响应式+函数式

    核心流程:

    • 1、addObserver时:
      • 1.1 验证setter方法是否存在
      • 1.2 注册KVO派生类
      • 1.3 派生类添加setter、class、dealloc方法
      • 1.4 isa指向派生类
      • 1.5 保存信息
    • 2、触发setter方法时:
      • 2.1 willChange
      • 2.1 消息转发(设置原类的属性值)
      • 2.2 didChange
    • 3、removeObserver
      • 3.1 手动移除
      • 3.2 自动移除

    示例相关文件说明

    本示例中:

    • ViewController:有导航控制器的根视图,点击Push按钮可跳转PushViewController
    • LGViewController:测试控制器,实现LBHPerson属性添加观察者触发属性变化移除观察者等功能;
    • LBHPerosn:继承自NSObject,具备name和nickName属性的类
    • NSObject+LBHKVO:重写KVO的相关功能

    1 添加addObserver

    // 添加观察者
    - (void)lbh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LBHKVOBlock)block {
        
        // 1.1 验证setter方法是否存在
        [self judgeSetterMethodFromKeyPath:keyPath];
    
        // 1.2 + 1.3 注册KVO派生类(动态生成子类) 添加方法
        Class newClass = [self creatChildClassWithKeyPath:keyPath];
    
        // 1.4 isa的指向: LBHKVONotifying_LBHPerosn
        object_setClass(self, newClass);
    
        // 1.5. 保存信息
        LBHInfo * info = [[LBHInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
        [self associatedObjectAddObject:info];
    }
    
    1.1 验证setter方法是否存在
    //MARK: -  验证是否存在setter方法
    - (void)judgeSetterMethodFromKeyPath:(NSString *) keyPath {
        Class class    = object_getClass(self);
        SEL setterSelector  = NSSelectorFromString(setterForGetter(keyPath));
        Method setterMethod = class_getInstanceMethod(class, setterSelector);
        if (!setterMethod) {
            @throw [NSException exceptionWithName: NSInvalidArgumentException
                                           reason:[NSString stringWithFormat:@"当前%@没有setter方法", keyPath]
                                         userInfo:nil];
        }
    }
    

    因为我们监听的是setter方法,所以当前被监听属性必须具备setter方法

    1.1.1 setterForGetter

    getter名称中读取setterkey => setKey

    static NSString * setterForGetter(NSString * getter) {
       
       if (getter.length <= 0) return nil;
       
       NSString * setterFirstChar = [getter substringToIndex:1].uppercaseString;
       
       return [NSString stringWithFormat:@"set%@%@:", setterFirstChar, [getter substringFromIndex:1]];
       
    }
    
    1.1.2 getterForSetter

    set方法获取getter方法, setKey => key

    #pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
    static NSString *getterForSetter(NSString *setter){
        
        if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
        
        NSRange range = NSMakeRange(3, setter.length-4);
        NSString *getter = [setter substringWithRange:range];
        NSString *firstString = [[getter substringToIndex:1] lowercaseString];
        return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
    }
    
    1.2 注册KVO派生类
    - (Class)creatChildClassWithKeyPath: (NSString *) keyPath {
        
        // 1. 类名
        NSString * oldClassName = NSStringFromClass([self class]);
        NSString * newClassName = [NSString stringWithFormat:@"%@%@",LBHKVOPrefix,oldClassName];
        
        // 2. 生成类
        Class newClass = NSClassFromString(newClassName);
        
        // 2.1 不存在,创建类
        if (!newClass) {
            
            // 2.2.1 申请内存空间 (参数1:父类,参数2:类名,参数3:额外大小)
            newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
            
            // 2.2.2 注册类
            objc_registerClassPair(newClass);
            
        }
        
        // 2.2.3 动态添加set函数
        SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
        Method setterMethod = class_getInstanceMethod([self class], setterSel); //为了保证types和原来的类的Imp保持一致,所以从[self class]提取
        const char * setterTypes = method_getTypeEncoding(setterMethod);
        class_addMethod(newClass, setterSel, (IMP)ht_setter, setterTypes);
        
        // 2.2.4 动态添加class函数 (为了让外界调用class时,不看到中间类,看到的时原来的类,isa需要指向原来的类)
        SEL classSel = NSSelectorFromString(@"class");
        Method classMethod = class_getInstanceMethod([self class], classSel);
        const char * classTypes = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSel, (IMP)ht_class, classTypes);
        
        // 2.2.5 动态添加dealloc函数
        SEL deallocSel = NSSelectorFromString(@"dealloc");
        Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
        const char * deallocTypes = method_getTypeEncoding(deallocMethod);
        class_addMethod(newClass, deallocSel, (IMP)ht_dealloc, deallocTypes);
        
        return newClass;
    }
    
    1.2.1 LBHKVO类的命名前缀,关联属性的key:
    static NSString *const kLBHKVOPrefix = @"LBHKVONotifying_";
    static NSString *const LBHKVOAssiociakey = @"kLBHKVO_AssiociateKey";
    
    1.3 派生类添加setterclassdealloc方法
    1.3.1 setter方法
    static void lbh_setter(id self, SEL _cmd, id newValue) {
        NSLog(@"新值:%@", newValue);
        // 读取getter方法(属性名)
        NSString * keyPath = getterForSetter(NSStringFromSelector(_cmd));
        // 获取旧值
        id oldValue = [self valueForKey:keyPath];
    
        // 1. willChange在此处触发(本示例省略)
    
        // 2. 调用父类的setter方法(消息转发)
        // 修改objc_super的值,强制将super_class设置为父类
        void(* lbh_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
        // 创建并赋值
        struct objc_super superStruct = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self)),
        };
    
        lbh_msgSendSuper(&superStruct, _cmd, newValue);
    //    objc_msgSendSuper(&superStruct, _cmd, newValue);
        
        // 3. didChange在此处触发(本示例省略)
        NSMutableArray * array = objc_getAssociatedObject(self, (__bridge const void * _Nonnull) LBHKVOAssiociakey);
        
        for (LBHKVOInfo * info in array) {
            if([info.keyPath isEqualToString:keyPath] && info.observer){
                // 3.1 block回调的方式
                if (info.hanldBlock) {
                    info.hanldBlock(info.observer, keyPath, oldValue, newValue);
                }
    //            // 3.2 调用方法的方式
                if([info.observer respondsToSelector:@selector(lbh_observeValueForKeyPath: ofObject: change: context:)]) {
                    [info.observer lbh_observeValueForKeyPath:keyPath ofObject:self change:@{keyPath: newValue} context:NULL];
                }
            }
        }
        
    }
    

    给属性赋值时会触发setter,有3个需要注意的点:

    • 1、赋值前: 本案例没实现赋值前的willChange事件。因为与下面的didChange方式一样,只是状态不同;

    • 2、赋值: 调用父类的setter方法,我们是通过objc_msgSendSuper进行调用。我们重写objc_super的结构体并完成receiversuper_class的赋值

    直接使用objc_msgSendSuper调用,会报参数错误

    有两种解决方法

    1、 objc_msgSend的检查关闭:target --> Build Setting --> Enable Strict Checking of objc_msgSend Calls 设置为NO

    截屏2020-12-24 上午9.41.41.png

    2、 新创建一个lbh_msgSendSuper引用objc_msgSendSuper,这样编译就不会报错,不需要关闭编译检查

    • 3、赋值后:我们有2种方法可以实现didChange事件,告知外部

    1、 和苹果官方一样,NSObject+LBHKVO.h文件中对外公开lbh_observeValueForKeyPath函数

    在调用的地方实现这个方法即可

    但是此方法方式让代码很分散,开发者需要在2个地方同时实现lbh_addObserverlbh_observeValueForKeyPath两个函数

    2、 响应式 + 函数式,直接在lbh_addObserver中添加Block回调代码块,需要响应的时候,我们直接响应block即可

    .h 文件

    .m 文件

    在调用lbh_addObserver函数时,直接实现block响应就行。这样完成了代码的内聚。

    1.3.2 class方法

    重写class方法,主要是让外界读取时,看不到KVO派生类,输出的是原来的类

    Class lbh_class(id self, SEL _cmd) {
        return class_getSuperclass(object_getClass(self)); // 返回当前类的父类(原来的类)
    }
    
    1.3.3 dealloc方法

    重写了dealloc方法,并将isa从KVO衍生类指回了原来的类

    // 重写dealloc方法
    void lbh_dealloc(id self, SEL _cmd) {
        
        NSLog(@"%s KVO派生类移除了",__func__);
        
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
    
    1.4 isa指向派生类
    // 1.4 isa的指向: LBHKVONotifying_LBHPerosn
    object_setClass(self, newClass);
    
    1.5 保存信息

    创建Info实例保存观察数据
    读取关联属性数组(当前所有观察对象) --->
    如果关联属性数组不存在,就创建一个
    (使用OBJC_ASSOCIATION_RETAIN_NONATOMIC没关系,因为关联属性不存在强引用,只是记录类名和属性名)
    case1 如果被监听对象已存在,直接跳出
    case2 添加监听对象

    LBHKVOInfo * info = [[LBHKVOInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
    [self associatedObjectAddObject:info];
    
    
    //MARK: - 关联属性添加对象
    - (void)associatedObjectAddObject:(LBHKVOInfo *)info {
        
        NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey);
        if (!mArray) {
            mArray = [NSMutableArray arrayWithCapacity:1];
            objc_setAssociatedObject(self,  (__bridge const void * _Nonnull)LBHKVOAssiociakey, mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        
        for (LBHKVOInfo * tempInfo in mArray) {
            if ([tempInfo isEqual:info]) return;
        }
        
        [mArray addObject:info];
    }
    

    2. 触发setter方法时

    1.3.1 setter方法中已描述清晰。
    主要是三步:willChange --> 设置原类属性 --> didChange

    3. removeObserver移除观察者

    3.1 手动移除

    移除指定被监听属性,如果都被移除了,就将isa指回父类

    - (void)lbh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
    {
        NSMutableArray * observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey);
        
        if (observerArr.count <= 0) return;
        
        for (LBHKVOInfo * info in observerArr) {
            if ([info.keyPath isEqualToString:keyPath]) {
                // 移除当前info
                [observerArr removeObject:info];
                // 重新设置关联对象的值
                objc_setAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey, observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                break;
            }
        }
        
        // 全部移除后,isa指回父类
        if (observerArr.count <= 0) {
            Class superClass = [self class];
            object_setClass(self, superClass);
        }
    }
    

    问题:手动把所有被监听属性都移除,触发isa指回本类,那dealloc触发lbh_dealloc触发时,isa会不会指向父类的父类了?

    解答:不会。因为isa指回本类后,KVO派生类对象已被释放。不会再进入ht_dealloc。
    这也是为什么将isa指回本类,会自动移除观察者。因为派生类对象已被释放,他记录的关联属性也自动被释放。

    3.2 自动移除

    1.3.3 dealloc方法中已描述清晰

    资料

    FBKVOController

    FaceBook出品,支持blockaction回调,支持自动移除观察者

    分析参考

    GNU源码

    可参阅它推测还原的Foundation库

    相关文章

      网友评论

        本文标题:iOS-底层原理21:KVO底层原理

        本文链接:https://www.haomeiwen.com/subject/agzagktx.html