美文网首页
iOS开发 之 KVO底层原理

iOS开发 之 KVO底层原理

作者: hui8685291 | 来源:发表于2021-10-05 16:32 被阅读0次

    首先来看看几个定义:

    编译型语言:
    概念:需要编辑器将源代码编译成机器码之后才能执行的语言。一般分两个步骤 编译(compile)、链接(linker)编译是把各个文件源代码编译成机器码,链接是把各个文件的机器码和依赖库串连起来生成可执行文件。
    流程:源代码->汇编代码->机器码->CPU执行

    优点: 编译器一般会有预编译的过程对代码进行优化。因为编译只做一次,运行时不需要编译,所以编译型语言的程序执行效率高。可以脱离语言环境独立运行(执行效率高)
    缺点: 编译之后如果需要修改就需要整个模块重新编译。编译的时候根据对应的运行环境生成机器码,不同的操作系统之间移植就会有问题,需要根据运行的操作系统环境编译不同的可执行文件(跨平台性能差)
    代表语言:C、C++、Pascal、Object-C、swift。

    解释型语言:
    概念:不需要编译,比编译型语言省了道工序,运行的时候逐行进行解释,生成机器代码。
    流程:源代码->字节码->解释器->机器码->CPU执行
    优点: 有良好的平台兼容性,在任何环境中都可以运行,前提是安装了解释器(虚拟机)。灵活,修改代码的时候直接修改就可以,可以快速部署,不用停机维护。(跨平台性强)
    缺点: 程序不需要编译,程序在运行时才翻译成机器码,每执行一次就要翻译一次,不可脱离语言环境独立运行(需要虚拟机)(执行效率差)
    代表语言:JavaScript、Python、Erlang、PHP、Perl、Ruby。

    image.png

    混合型语言:
    既然编译型和解释型各有缺点就会有人想到把两种类型整合起来,取其精华去其糟粕。就出现了半编译型语言。比如C#,C#在编译的时候不是直接编译成机器码而是中间码,.NET平台提供了中间语言运行库运行中间码,中间语言运行库类似于Java虚拟机。.net在编译成IL代码后,保存在dll中,首次运行时由JIT在编译成机器码缓存在内存中,下次直接执行

    动态语言:
    是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
    主要动态语言:Object-C、C#、JavaScript、PHP、Python。

    静态语言:
    与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++

    动态类型语言:
    动态类型语言是指在运行期间才去做数据类型检查的语言
    这里需要跟动态语言区别开,动态类型语言说的是数据类型,动态语言说的是运行时改变结构,说的是代码结构。

    静态类型语言:
    静态语言的数据类型是在编译其间确定的或者说运行之前确定的,编写代码的时候要明确确定变量的数据类型

    强类型语言:
    强类型语言也称为强类型定义语言。是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。

    弱类型语言:
    与上正好相反,像vb、php、js(也就是说,一个变量,你可以直接给他赋值字符串,也可以直接给他赋值数值,你还可以直接让字符串类型的变量和数值类型的变量相加,虽然得出的最终结果未必是你想象的那样,但一定不会报错)

    基于上面的例子,你可以说swift允许我们不声明类型并且让编译器自己检测类型

    var a = 10
    

    看上去像是弱类型语言,但是swift推出它是int类型的,所以不能像其赋值其他类型的值


    image.png

    这说明了,Swift 是一门强类型的语言。Swift 的类型声明,你可以看成是在定义变量的时候,隐式声明的(由编译器推断出),当然也可以显式的声明。如下:

    var a :Int = 10
    

    综上所述,可以得出 :
    OC 是 动态类型语言&&强类型语言&&动态语言&&编译型语言;
    swift 是 动态类型语言&&强类型语言&&静态语言&&编译型语言;

    再来了解下Objective-C语言的特性:

    在Objective-C中
    1、所有的类都必须继承自NSObject;
    2、所有对象都是指针的形式;
    3、用self代替this;
    4、使用id代替void*;
    5、使用nil表示NULL;
    6、只支持单继承,不允许多重继承;
    7、使用YES/NO表示TRUE/FALSE;
    8、使用#import代替#include;
    9、用消息表示类的方法,并采用[aInstance method:argv]调用形式;
    10、支持反射机制;
    11、支持Dynamic Typing(动态类型), Dynamic Binding(动态绑定)和Dynamic Loading(动态加载);
    12、不支持命名空间机制;

    动态特性

    OC做为一门面向对象语言,自然具有面向对象的语言特性,如封装、继承、多态。他具有静态语言的特性(如C/C++),又有动态语言的特性(动态绑定、动态加载等)。OC的动态特性表现为了三个方面:动态类型、动态绑定、动态加载。之所以叫做动态,是因为必须到运行时(run time)才会做一些事情。

    (1)动态类型
    动态类型,说简单点就是id类型。动态类型是跟静态类型相对的。像内置的明确的基本类型都属于静态类型(int,CGFloat等)。静态类型是强类型,而动态类型属于弱类型,静态类型在编译的时候就能被识别出来。所以,若程序发生了类型不对应,编译器就会发出警告。而动态类型在编译器编译的时候是不能被识别的,要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。
    Hold on!!!
    先看一段代码:

      NSString *str = [NSData data];
    这段代码我们command+B编译发现程序可以运行通过,但是Xcode会进行警告,因为指针指向的类型为NSString,
    但是赋值为NSData对象,所以在编译时会警告,但是编译时其类型依然作为NSString类型来编译,
    
      NSString *str = [NSData data];
      [str stringByAppendingString:@"字符串"]; 
    在这里进行编译发现编译也可以通过,因为str在编译时的类型为NSString,所以它调用字符串的方法是可以编译
    通过的,但是我们运行程序发现此时程序会崩溃,此时我们打一个断点来看一下str在运行时的类型
    
    image.png

    此时我们可以看到str在程序运行时的类型为NSData,这就是OC的动态类型,将程序的真实类型推迟到程序运行时才去决定。

    (2)动态绑定
    动态绑定(dynamic binding)只需记住关键词@selector/SEL即可。先来看看“函数”,对于其他一些静态语言,比如c++,一般在编译的时候就已经将将要调用的函数的函数签名都告诉编译器了。静态的,不能改变,而在OC中,其实是没有函数的概念的,我们叫“消息机制”,所谓的函数调用就是给对象发送一条消息,这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法。最终形成的这个SEL变量就代表一个方法的引用。这里要注意一点:SEL并不是C里面的函数指针,虽然很像,但真心不是函数指针。SEL变量只是一个整数,他是该方法的ID。以前的函数调用,是根据函数名,也就是字符串去查找函数体。但现在,我们是根据一个ID整数来查找方法,整数的查找自然要比字符串的查找快得多。所以,动态绑定的特定不仅方便,而且效率更高。

    (3)动态加载
    动态加载指的有两方面:1.动态资源的加载 2.部分可执行代码模块的加载,这些资源在程序运行时动态的选择性加载。动态资源的加载典型就是程序中不同像素的图片的加载,例如根据不同的机型做适配,最经典的例子就是在Retina设备上加载@2x的图片,而在老一些的普通屏设备上加载原图,程序会根据当前屏幕的像素来加载。 部分可执行代码模块的加载指的程序中典型的懒加载。

    了解KVC

    概念: 即是指 NSKeyValueCoding,一个非正式的 Protocol,提供一种机制来间接访问对象的属性, 可以通过字符串来访问对应的属性方法或成员变量。KVO 就是基于 KVC 实现的关键技术之一。

    特性:
    - KVC是一个用于间接访问对象属性的机制;
    - KVC使用该机制不需要调用存取方法和变量实例就可以访问对象属性;
    - KVC键-值编码方法在Objective-C非正式协议(类目)NSKeyValueCoding中被声明;
    - KVC键-值编码支持带有对象值的属性,同时也支持纯数值类型和结构;
    - KVC可以用来访问和设置实例变量的值。key是属性名称;

    属性的访问和设置
    KVC可以用来访问和设置实例变量的值。key:键,用于标识实例变量
    value:实例变量对应的值
    设置方式:[self setValue:aName forKey:@"name"]
    等同于 self.name = aName;
    访问方式: aString = [self valueForKey:@"name"]
    等同于 aString = self.name;
    

    修改值的Api:

    setValue:forKey:
    
    setValue:forKeyPath:
    
    setValue:forUndefinedKey:
    
    setValueForKeysWithDictionary:
    

    获取值的Api:

    valueForKey:
    
    valueForKeyPath:
    
    valueForUndefinedKey:
    

    注意事项:

    当key不存在的时候,会执行setValue:forUndefinedKey:系统默认实现是抛出一个异常.

    KVC使用

    1,大致步骤:
    (1)首先找到后面的key有没有get(set)方法,如果有,则直接调用
    (2)如果没有get(set)方法,直接找_key这个属性,如果没有找到key,然后再去找key这个属性,然后直接赋值
    (3)如果key这个属性也没有,则报错重写
    2.设置的key最好不要加
    ,因为系统会自动的优先地寻找_key这个属性;
    3.捕获程序设置方法的异常:- (void)setValue:(id)value forUndefinedKey:(NSString *)key
    捕获程序访问方法的异常:- (id)valueForUndefinedKey:(NSString *)key

    KVC键值查找

    1、setValue:forKey:搜索方式
    (1)首先搜索setKey:方法。(key指成员变量名,首字母大写)。
    (2)上面的setter方法没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。(NSKeyValueCodingCatogery中实现的类方法,默认实现为返回YES)
    (3)如果没有找到成员变量,调用setValue:forUnderfinedKey: 。
    
    2、valueForKey:的搜索方式
    (1)首先按getKey,key,isKey的顺序查找getter方法,找到直接调用。如果是BOOL、int等内建值类型,会做NSNumber的转换。
    (2)上面的getter没找到,查找countOfKey、objectInKeyAtindex、KeyAtindexes格式的方法。如果countOfKey和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法。
    (3)还没找到,查找countOfKey、enumeratorOfKey、memberOfKey格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合。
    (4)还是没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。
    (5)再没找到,调用valueForUndefinedKey。
    

    KVC的使用场景

    // 可以访问并使用公私有属性

    - (void)kvcTest{
            Boy *jack = [[Boy alloc] init];
            [jack setValue:@"Jack" forKey:@"name"];
            [jack setValue:@"18" forKey:@"age"];
    //    [jack setValue:@"183" forKeyPath:@"_height"];
    
            [jack setValue:@"football" forKey:@"sport"];
    
            NSLog(@"jack.name : %@",jack.name);
            NSLog(@"jack.age : %ld",jack.age);
            [jack logTest];
            NSLog(@"jack.sport : %@",[jack valueForKey:@"height"]);
    }
    

    //复杂属性赋值,嵌套赋值
    //当 Boy 有一个其它类型属性 Book 的属性时候:

    - (void)kvcTest1{
           Boy *jack = [[Boy alloc] init];
           jack.book = [[Book alloc] init];
            
           [jack.book setValue:@"iOS" forKeyPath:@"bookName"]; //方式一
           [jack setValue:@"C++" forKeyPath:@"book.bookName"]; //方式二
        
        NSLog(@"book.bookName : %@",[jack valueForKeyPath:@"book.bookName"]);
    }
    

    // 字典转模型

    - (void)kvcTest2{
        NSDictionary *dic = @{@"name":@"LiMing", @"eid" : @"南昌"};
           Boy *jack = [[Boy alloc] init];
            
            [jack setValuesForKeysWithDictionary:dic];
            
            NSLog(@"model.name : %@",jack.name);
            NSLog(@"model.num : %@",jack.city);
       /**
           第一种情况,model多一个属性:这样程序没问题,model多出的属性会是nil
           第二种情况,model少一个属性:程序会崩溃
           第三种情况,model的属性名字和dic的key不匹配 : 程序会崩溃
           第二种和第三种崩溃的解决办法是重写方法  -(void)setValue:(id)value forUndefinedKey:(NSString *)key
        **/
    }
    

    //模型转字典

    - (void)kvcTest3{
        NSDictionary *dic = @{@"name":@"LiMing", @"eid" : @"南昌"};
           Boy *jack = [[Boy alloc] init];
            [jack setValuesForKeysWithDictionary:dic];
           NSDictionary *modelDic = [jack dictionaryWithValuesForKeys:@[@"name",@"age"]];
           NSLog(@"modelDic : %@", modelDic);
    }
    

    KVO简述

    定义:KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

    苹果KVO官网地址:

    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i

    KVO的实现原理:

    当某个类的对象第一次被观察时,系统就会在运行时动态地创建该类的一个派生类,在这个派生类中重写原类中被观察属性的 setter 方法 , 派生类在被重写的 setter 方法实现真正的通知机制 (Person->NSKVONotifying_Person). 派生类重写了 class 方法以 “ 欺骗 ” 外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter ,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

    KVO的一般的使用场景:

    1. 实现上下拉刷新控件 content offset;
    2. webview 混合排版 content size;
    3. 监听模型属性实时更新UI;

    带着问题探索
    1.iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
    答,当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针, 改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现, set方法实现内部会按顺序调用willChangeValueForKey方法、原来的setter方法实现、 didChangeValueForKey方法,而didChangeValueForKey方法内部 又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

    2.如何手动触发KVO
    答, 被监听的属性的值被修改时,就会自动触发KVO。
    如果想要手动触发KVO,则需要我们自己重写+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key来禁用自动监听,然后再调用willChangeValueForKey和
    didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO
    ,并且这两个方法缺一不可。
    范例代码:

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
        if ([key isEqualToString:@"name"]) {
            return NO;
        } else { 
            return [super automaticallyNotifiesObserversForKey:key];
        }
    }
    
    - (void)setName:(NSString *)name{
        if (_name!=name) {
            [self willChangeValueForKey:@"name"];
            _name=name;
            [self didChangeValueForKey:@"name"];
        }
    }
    
    - (void)willChangeValueForKey:(NSString *)key
    {
        NSLog(@"willChangeValueForKey: - begin");
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey: - end");
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        NSLog(@"didChangeValueForKey: - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey: - end");
    }
    

    KVO实现原理探索

    image1.png

    上述代码中可以看出,在添加监听之后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法

    1. 探寻KVO底层实现原理

    通过上述代码我们发现,一旦age属性的值发生改变时,就会通知到监听者,并且我们知道赋值操作都是调用 set方法,我们可以来到Person类中重写age的set方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。 我们发现即使重写了set方法,p1对象和p2对象调用同样的set方法,但是我们发现p1除了调用set方法之外还会另外执行监听器的observeValueForKeyPath方法。 说明KVO在运行时获取对p1对象做了一些改变。相当于在程序运行过程中,对p1对象做了一些变化,使得p1对象在调用setage方法的时候可能做了一些额外的操作,所以问题出在对象身上,两个对象在内存中肯定不一样,两个对象可能本质上并不一样。接下来来探索KVO内部是怎么实现的。

    2. KVO底层实现分析

    在分析之前我们先来了解下实例对象、类对象以及元类对象的关系,如下图所示


    image.png

    isa的指向
    上图中的Root class(class)是根类,即NSObject类。Root class(meta)就是NSObject的元类,即根元类。从图中可知,isa的指向如下:

    image.png
    从下往上分别是:
    实例对象 --> 类对象 --> 元类对象 --> 根元类对象
    父类的实例对象 --> 父类的类对象 --> 父类的元类对象 --> 根元类对象
    根类的实例对象 --> 根类的类对象 --> 根元类对象
    根元类对象 --> 自己本身

    类的继承
    从经典图中可知,继承关系如下:

    image.png
    类对象 --> 父类的类对象 --> 根类的类对象 --> nil
    元类对象 --> 父类的元类对象 --> 根元类对象 --> 根类的类对象 --> nil
    从这个继承关系可知,只有类对象和元类对象才有继承关系,实例对象是没有继承关系的。且所有对象都是继承于NSObject类对象,NSObject类对象则继承于nil。

    首先我们对上述代码中添加监听的地方打断点,看观察一下,addObserver方法对p1对象做了什么处理?也就是说p1对象在经过addObserver方法之后发生了什么改变,我们通过打印isa指针如下图所示


    image2.png

    通过上图我们发现,p1对象执行过addObserver操作之后,p1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifying_Person类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。
    那么我们先来观察p2对象在内容中是如何存储的,然后对比p2来观察p1。
    首先我们知道,p2在调用setAge方法的时候,首先会通过p2对象中的isa指针找到Person类对象,然后在类对象中找到setAge方法。然后找到方法对应的实现。如下图所示


    image3.png

    但是刚才我们发现p1对象的isa指针在经过KVO监听之后已经指向了NSKVONotifying_Person类对象,NSKVONotifying_Person其实是Person的子类,那么也就是说其superclass指针是指向Person类对象的,NSKVONotifying_Person是runtime在运行时生成的。那么p1对象在调用setAge方法的时候,肯定会根据p1的isa找到NSKVONotifying_Person,在NSKVONotifying_Person中找setAge的方法及实现。
    经过查阅资料我们可以了解到。
    NSKVONotifying_Person中的setAge方法中其实调用了 Fundation框架中C语言函数 _NSSetIntValueAndNotify,_NSSetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。

    那么如何验证KVO真的如上面所讲的方式实现?
    首先经过之前打断点打印isa指针,我们已经验证了,在执行添加监听的方法时,会将isa指针指向一个通过runtime创建的Person的子类NSKVONotifying_Person, 另外我们可以通过打印方法实现的地址来看一下p1和p2的setAge的方法实现的地址在添加KVO前后有什么变化。

    image4.png

    我们发现在添加KVO监听之前,p1和p2的setAge方法实现的地址相同,而经过KVO监听之后,p1的setAge方法实现的地址发生了变化,我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1的setAge方法的实现由Person类方法中的setAge方法转换为了C语言的Foundation框架的_NSSetIntValueAndNotify函数。
    Foundation框架中会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,那么我们看到Foundation框架中调用的_NSSetIntValueAndNotify函数。那么我们把age的属性类型变为double重新打印一遍


    image5.png

    我们发现调用的函数变为了_NSSetDoubleValueAndNotify,那么这说明Foundation框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。 那么我们可以推测Foundation框架中还有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函数。 我们可以找到Foundation框架文件,通过命令行查询关键字找到相关函数


    image6.png

    NSKVONotifying_Person内部结构是怎样的?
    首先我们知道,NSKVONotifying_Person作为Person的子类,其superclass指针指向Person类,并且NSKVONotifying_Person内部一定对setAge方法做了单独的实现,那么NSKVONotifying_Person同Person类的差别可能就在于其内存储的对象方法及实现不同。
    我们通过runtime分别打印Person类对象和NSKVONotifying_Person类对象内存储的对象方法

    image7.png

    上述打印内容如下:
    通过上述代码我们发现NSKVONotifying_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此我们可以画出NSKVONotifying_Person的内存结构以及方法调用顺序。


    image8.png

    这里NSKVONotifying_Person重写class方法是为了隐藏NSKVONotifying_Person。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。


    image9.png

    如果NSKVONotifying_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找到NSObject,而NSObject的class的实现大致为返回自己isa指向的类,返回的p1的isa指向的类那么打印出来的类就是NSKVONotifying_Person,但是Apple不希望将NSKVONotifying_Person类暴露出来,并且不希望我们知道NSKVONotifying_Person内部实现, 所以在内部重写了class类, 直接返回Person类,所以外界在调用p1的class对象方法时,是Person类。这样p1给外界的感觉p1还是Person类,并不知道NSKVONotifying_Person子类的存在。

    那么我们可以猜测NSKVONotifying_Person内重写的class内部实现大致为:

    - (Class) class {
         // 得到类对象,在找到类对象父类
         return class_getSuperclass(object_getClass(self));
    }
    
    

    验证didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法.
    我们在Person类中重写willChangeValueForKey:和didChangeValueForKey:方法,模拟他们的实现。

    - (void)setAge:(int)age {
        NSLog(@"setAge:");
        _age = age; 
    } 
    
    - (void)willChangeValueForKey:(NSString *)key { 
       NSLog(@"willChangeValueForKey: - begin"); 
       [super willChangeValueForKey:key];
       NSLog(@"willChangeValueForKey: - end"); 
    } 
    
    - (void)didChangeValueForKey:(NSString *)key{ 
       NSLog(@"didChangeValueForKey: - begin"); 
       [super didChangeValueForKey:key];
       NSLog(@"didChangeValueForKey: - end");
    }
    
    

    再次运行来查看didChangeValueForKey的方法内运行过程,通过打印内容可以看到,确实在didChangeValueForKey方法内部已经调用了observer的observeValueForKeyPath:ofObject:change:context:方法。


    image10.png

    KVO底层实现代码

    自己通过代码来模拟KVO内部实现监听

    #import "NSObject+EXKVO.h"
    #import <objc/message.h>
    
    @implementation NSObject (EXKVO)
    
    - (void)EX_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    //  1.创建一个类 -- self.class 就是Person
        NSString *oldname = NSStringFromClass(self.class);
        NSString *newNem = [@"NSKVONotifying_" stringByAppendingString:oldname];
       Class myclass = objc_allocateClassPair(self.class, newNem.UTF8String, 0);
        // 注册类
        objc_registerClassPair(myclass);
            
    //   2.重写子类set方法 -- 所谓的重写就是给子类添加这个方法 setAge,因为子类没有父类的setAge方法
        /* class :给那个类添加方法
         *sel:方法编号
         *imp :方法实现(函数指针)
         *type :返回值类型
         */
        class_addMethod(myclass, @selector(setAge:), (IMP)setAge, "v@:@");
    //    3.修改isa指针
        object_setClass(self, myclass);
    //    4.将观察保存到当前对象
        objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_RETAIN);
        
    }
    
    void setAge(id self,SEL _cmd,int newAge){
        NSLog(@"来了--%d",newAge);
    //    调用父类的setName方法
        Class class = [self class];
        object_setClass(self, class_getSuperclass(class));//改成父类
        
        objc_msgSend(self,@selector(setAge:),newAge);//发送消息给父类
        
        //    观察者
        id observer = objc_getAssociatedObject(self, @"observer");
        
        if (observer) {
            objc_msgSend(observer, @selector(lg_observeValueForKeyPath:ofObject:newValue:),@"age",self,@{@"new:":@(newAge),@"kind:":@"1"});
        }
        
    //    改回子类
        object_setClass(self, class);
    }
    
    @end
    
    
    

    KVO 和线程

    一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange... 会触发 KVO 通知。

    所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。

    KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO 会保证下列两种情况的发生:

    首先,如果我们调用一个支持 KVO 的 setter 方法,如下所示:

    self.exchangeRate = 2.345;
    KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。

    其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用之前, exchangeRate 的 accessor 方法都会返回同样的值。

    KVO的优缺点

    一、KVO优点

        1.能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;
    
        2.能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象;
    
        3.能够提供观察的属性的最新值以及先前值;
    
        4.用key paths来观察属性,因此也可以观察嵌套对象;
    

    二、KVO缺点:

        1.我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查;
    
        2.对属性重构将导致我们的观察代码不再可用;
    
        3.复杂的“IF”语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向; 
    
        4.当释放观察者时不需要移除观察者。

    相关文章

      网友评论

          本文标题:iOS开发 之 KVO底层原理

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