美文网首页
OC-KVC原理分析

OC-KVC原理分析

作者: Sharif_su | 来源:发表于2021-09-07 18:39 被阅读0次

    OC之Method_Swizling一些坑点、KVC原理分析

    Method_Swizling

    Method_Swizling我们并不陌生,通过交换两个方法SEL的IMP指向,达到方法交换的目的。一般来说,我们通常写在cateogry里,在+load()方法里实现方法的交换。

    常规的方法交换

    // SSJPerson.h
    @interface SSJPerson : NSObject

    • (void)person_walk;
      @end

    // SSJPerson.m

    import "SSJPerson.h"

    @implementation SSJPerson

    • (void)person_walk{
      NSLog(@"SSJPerson ---> person_walk");
      }

    // SSJStudent.h
    // SSJStudent 继承自 SSJPerson
    @interface SSJStudent : SSJPerson

    • (void)student_sleep;
      @end

    // SSJStudent.m

    import "SSJStudent.h"

    @implementation SSJStudent

    • (void)student_sleep{
      NSLog(@"SSJStudent ---> student_sleep");
      }
      @end

    //SSJStudent+category.m

    import "SSJStudent+category.h"

    import <objc/runtime.h>

    @implementation SSJStudent (category)

    • (void)load{
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      Method originalMethod = class_getInstanceMethod([self class], @selector(student_sleep));
      Method swizzlingMethod = class_getInstanceMethod([self class], @selector(student_sleepNew));
      method_exchangeImplementations(originalMethod, swizzlingMethod);
      });

    }

    • (void)student_sleepNew{
      NSLog(@"替换过的方法 -- > student_sleepNew");
      [self student_sleepNew];
      }
      @end
      复制代码
      调用的时候:

    SSJStudent *stu = [SSJStudent new];
    [stu student_sleep];
    复制代码
    运行也没问题:

    image.png

    父类实现,子类没有实现

    那么替换一个父类的已经实现了,但当前cateogry类没实现的方法呢?

    对代码进行修改:

    // SSJStudent+category.m

    import "SSJStudent+category.h"

    import <objc/runtime.h>

    @implementation SSJStudent (category)

    • (void)load{
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      /// 替换父类方法:person_walk
      /// 父类实现了person_walk,子类并没实现person_walk
      Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
      Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
      method_exchangeImplementations(originalMethod, swizzlingMethod);
      });

    }

    //- (void)student_sleepNew{
    // NSLog(@"替换过的方法 -- > student_sleepNew");
    // [self student_sleepNew];
    //}

    • (void)person_walkNew{
      NSLog(@"替换过的方法 -- > person_walkNew");
      [self person_walkNew];
      }

    @end
    复制代码
    // ViewController.m
    @implementation ViewController

    • (void)viewDidLoad {
      [super viewDidLoad];

      SSJStudent *student = [SSJStudent new];
      /// 这里换成person_walk
      [student person_walk];
      }

    @end
    复制代码
    运行:

    image.png

    看打印结果,子类调用person_walk都没问题。

    这里对ViewController.m添加两行代码:

    // ViewController.m
    @implementation ViewController

    • (void)viewDidLoad {
      [super viewDidLoad];

      SSJStudent *student = [SSJStudent new];
      /// 这里换成person_walk
      [student person_walk];
      NSLog(@"\n");
      /// 添加代码,父类也调用person_walk
      SSJPerson *person = [SSJPerson new];
      [person person_walk];
      }

    @end
    复制代码
    再次运行,就发现提示找不到方法:

    image.png

    这边我画了一张图:

    image.png

    由于SSJStudent+category内部实现了+load()方法,导致程序在load_images阶段,就调用了+load()方法。

    而+load()方法里对父类(SSJPerson)的person_walk方法进行了替换,导致父类在调用自己方法person_walk的时候,提示找不到具体的person_walkNew实现,因为父类根本就没这个方法。

    在实际多人开发过程中,提供父类的那个人他不一定知道你交换了父类的方法,当他调用自己父类的方法时,可能就一下子对这个报错感到莫名其妙:我明明没有调用这个方法啊,为什么提示这个错误?

    如何避免这种子类替换了父类的方法,子类自己却没有实现父类方法的情况呢?
    我们对SSJStudent+category.m的+load()方法进行修改:

    // SSJStudent+category.m

    import "SSJStudent+category.h"

    import <objc/runtime.h>

    @implementation SSJStudent (category)

    • (void)load{
      /// 替换父类方法:person_walk
      /// 父类实现了person_walk,子类并没实现person_walk
      Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
      Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
      //添加一个Method(SEL - person_walk,IMP - person_walkNew)
      BOOL isAdded = class_addMethod([self class], method_getName(originalMethod), method_getImplementation(swizzlingMethod), method_getTypeEncoding(originalMethod));
      if(isAdded){
      /// 添加成功 ,就说明子类没有实现父类person_walk对应的IMP方法.
      /// 经过class_addMethod这一步,子类已经有了一个person_walk方法,并且IMP指向person_walkNew
      /// 接下来,直接添加一个Method(SEL - person_walkNew,IMP - person_walk)
      /// 然后子类就实现了有了两个IMP互相交换的Method,最终效果跟method_exchangeImplementations一样
      class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
      }else{
      ///添加不成功,说明子类本身就已经实现了person_walk的IMP方法,那就直接交换两个Method的IMP即可
      method_exchangeImplementations(originalMethod, swizzlingMethod);
      }

    }
    复制代码
    运行效果:

    image.png

    简单来说,就是:

    class_addMethod添加Method(SEL - person_walk,IMP - person_walkNew)

    成功 -》则调用class_replaceMethod添加Method(SEL - person_walkNew,IMP - person_walk)。

    失败 -》则调用method_exchangeImplementations交换两个Method的IMP指向。

    为了便于理解,我画了张图

    image.png

    说明:

    class_addMethod:只能在SEL没有IMP指向时才可以添加成功;
    class_replaceMethod:不管SEL 有没有IMP实现,都可以添加成功;
    父类没有实现,子类也没有实现

    把父类实现部分注释

    image.png

    然后再运行:

    image.png

    提示找不到这个person_walk这个方法实现,那就说明category那里出了问题。
    我们在class_getInstanceMethod那一行打上断点:

    image.png

    由于父类和子类都没有实现person_walk,导致这边获取的originalMethod为空。

    我们对category的+load()方法进行修改,添加originalMethod空值处理:

    • (void)load{
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{

        /// 替换父类方法:person_walk
        /// 父类实现了person_walk,子类并没实现person_walk
        Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
        Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
        if (!originalMethod) {
        /// 没有,那就添加一个person_walk方法,并且手动添加一个临时处理的IMP实现
           class_addMethod([self class], @selector(person_walk), method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));
            /// originalMethod需要重新获取一边,不然依旧是空的
            originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
            method_setImplementation(originalMethod, imp_implementationWithBlock(^(id self,SEL _cmd){
                NSLog(@"临时方法");
            }));
        }
        //添加一个Method(SEL - person_walk,IMP - person_walkNew)
        BOOL isAdded = class_addMethod([self class], method_getName(originalMethod), method_getImplementation(swizzlingMethod), method_getTypeEncoding(originalMethod));
        if(isAdded){
            /// 添加成功 ,就说明子类没有实现父类person_walk对应的IMP方法.
            /// 经过class_addMethod这一步,子类已经有了一个person_walk方法,并且IMP指向person_walkNew
            /// 接下来,直接添加一个Method(SEL - person_walkNew,IMP - person_walk)
            /// 然后子类就实现了有了两个IMP互相交换的Method,最终效果跟method_exchangeImplementations一样
            class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else{
            ///添加不成功,说明子类本身就已经实现了person_walk的IMP方法,那就直接交换两个Method的IMP即可
            method_exchangeImplementations(originalMethod, swizzlingMethod);
        }
      

      });

    }
    复制代码
    运行:

    image.png

    KVC分析

    在讲解KVC之前,请先允许我演示一段骚操作,对SSJPerson类的未开放属性进行读写操作:

    // SSJPerson.h

    import <Foundation/Foundation.h>

    @interface SSJPerson : NSObject
    @end

    // SSJPerson.m

    import "SSJPerson.h"

    @interface SSJPerson ()
    /// 昵称
    @property (nonatomic , strong) NSString *nickName_private;
    @end
    @implementation SSJPerson
    @end
    复制代码
    image.png

    如图所示,对于一个未开放出来的属性,我们无法通过对象.属性名这种常规的方式进行访问。但是我们可以利用setValue:forKey:这方式进行赋值。

    这种通过setValue:forKey:方式进行赋值的操作,我们称之为KVC。
    KVC是一种设计模式,那么它的原理又是什么呢?为什么可以对未开放属性进行直接操作呢?

    存在即是真理。带着探索的思维,我们决定去看一下setValue:forKey:的底层实现。

    进入苹果官方文档:KVC部分

    image.png

    大概意思是:
    NSObject提供的NSKeyValueCoding协议,默认实现使用一组明确定义的规则,将基于密钥的访问器调用映射到对象的底层属性。这些协议方法使用一个关键参数来搜索它们自己的对象实例,以查找访问器、实例变量和遵循某些命名约定的相关方法。

    Setter

    接下来看一下Setter搜索模式: image.png

    按照图上所说,Setter搜索模式分为3步:

    找set<Key>: 或 _set<Key>,找到了就调用它;

    如果没找到,就去依次查找_<key>, is<Key>, <key>, 或is<Key>,找到了就用输入值设置变量(比如找到了查找<key>,那么后面的_is<Key>等就不需要找了)。

    如果还是没找到,就会调用setValue:forUndefinedKey:并引发异常。

    我们来根据这3个步骤,实操一下:

    // SSJPerson.h
    @interface SSJPerson : NSObject{
    @public
    NSString *boddy;
    NSString *_boddy;
    NSString *isBoddy;
    NSString *_isBoddy;
    }
    @end

    // SSJPerson.m
    //(根据第2步,找个要设置为YES)
    @implementation SSJPerson

    • (BOOL)accessInstanceVariablesDirectly{
      return true;
      }
      @end

    // ViewController.m

    • (void)viewDidLoad {
      [super viewDidLoad];
      /// 设置值
      [personA setValue:@"足球" forKey:@"boddy"];
      /// 打印内容,我们要看一下具体赋值给哪个值
      NSLog(@"_<key>---%@",personA->_boddy);
      NSLog(@"_is<Key>---%@",personA ->_isBoddy);
      NSLog(@"<key>---%@",personA ->boddy);
      NSLog(@"is<Key>---%@",personA ->isBoddy);
      }
      复制代码
      运行:

    image.png

    注释_boddy:

    image.png

    注释_isBoddy:

    image.png

    这也就验证了里第2点:当存在多个类似变量,会依次查找_<key>, _is<Key>, <key>, 或is<Key>,找到了就给它赋值,后面的就赋值了。
    思考:关于第一点set<Key>:,是不是也有第2点类似的关系呢?

    image.png

    注释setBoddy方法:

    image.png

    注释_setBoddy方法:

    image.png

    注释setIsBoddy方法:

    image.png

    经过4次打印,我们发现,在我们调用setValue:forKey:的时候,会依次查找: set<Key>: > _set<Key> > setIs<Key>。找到后就用输入值赋值给变量

    Getter

    接下来看一下Getter:

    IMG_0978.JPG

    大概意思如下:

    1、查找 get<Key>, <key>, is<Key>, or _<key>,找到了就进入步骤5;找不到就进入步骤2;

    2、在实例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
    countOf<Key>必须实现,另外两个找到了其中一个,就创建集合代理对象,找不到就进入步骤3;

    3、改为搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3个方法都存在才行,否则就进入步骤4;

    4、当确定AccessInstanceVariables方法返回YES(默认也是YES),
    顺序搜索名为 _<key>, _is<Key>, <key>, 或is<Key>的实例变量。
    如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。

    5、如果检索到的属性值是对象指针,只需返回结果。
    如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回该值。
    如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。

    6、如果都找不到,调用valueForUndefinedKey:并抛出异常。
    复制代码
    针对第1点,我们来进行实操:

    // SSJPerson.m

    • (NSString *)getBoddy{
      NSLog(@"%s -->Getter",func);
      return boddy;
      }

    • (NSString *)boddy{
      NSLog(@"%s -->Getter",func);
      return boddy;
      }

    • (NSString *)isBoddy{
      NSLog(@"%s -->Getter",func);
      return isBoddy;
      }

    • (NSString *)_boddy{
      NSLog(@"%s -->Getter",func);
      return _boddy;
      }

    // ViewController.m

    • (void)viewDidLoad {
      [super viewDidLoad];
      SSJPerson *personA = [SSJPerson new];
      /// 设置值
      [personA setValue:@"足球" forKey:@"boddy"];
      NSLog(@"打印---%@",[personA valueForKey:@"boddy"]);
      }

    复制代码
    运行:

    image.png

    注释getBoddy:

    image.png

    注释boddy:

    image.png

    注释isBoddy: image.png

    至于为什么只有boddy方法执行之后,valueForKey才打印出内容,那是因为默认情况下,setter和getter是一一对应的关系,setter模式优先执行<key>方法,getter模式对应着_boddy方法。

    针对第2点,我们来进行实操

    在实例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
    countOf<Key>必须实现,另外两个找到了其中一个,就创建集合代理对象,找不到就进入步骤
    复制代码
    这里不能使用boddyArray,不然会走第一步的Getter方法:

    image.png

    把key改为myBoddyArray

    image.png

    注释掉objectInMyBoddyArrayAtIndex:方法:

    image.png

    针对第3点,我们来进行实操

    改为搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3个方法都存在才行,否则就进入步骤4;
    复制代码
    image.png

    针对第4点,我们来进行实操

    当确定AccessInstanceVariables方法返回YES(默认也是YES),
    顺序搜索名为 _<key>, _is<Key>, <key>, 或is<Key>的实例变量。
    如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。
    复制代码
    image.png 注释_boddy:

    image.png

    注释_isBoddy:

    image.png

    注释boddy:

    image.png

    针对第6点,我们来进行实操

    如果都找不到,调用valueForUndefinedKey:并抛出异常
    复制代码
    在不做处理的情况下,用一个不存在的key去访问,会报错:

    image.png

    我们实现一下valueForUndefinedKey:,然后:

    image.png

    总结一下KVC的Setter和Getter

    Setter:

    依次查找set<Key>: 或 _set<Key>,找到了就调用方法;找不到就进入步骤2;

    先确定AccessInstanceVariables返回YES,然后依次查找_<key>, _is<Key>, <key>, 或is<Key>,找到了就用输入值设置变量。找不到就进入步骤3;

    (比如找到了查找_<key>,那么后面的_is<Key>等就不需要找了)。

    调用setValue:forUndefinedKey:并引发异常。
    Getter:

    查找 get<Key>, <key>, is<Key>, or _<key>,找到了就进入步骤5;找不到就进入步骤2;

    在实例中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:, 其中countOf<Key>必须实现,另外两个找到了其中一个,就创建集合代理对象,找不到就进入步骤3;

    改为搜索countOf<Key>、enumeratorf<Key>和memberOf<Key>:,3个方法都存在才行,否则就进入步骤4;

    当确定AccessInstanceVariables方法返回YES(默认也是YES), 顺序搜索名为 _<key>, _is<Key>, <key>, 或is<Key>的实例变量。 如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。

    如果检索到的属性值是对象指针,只需返回结果。 如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回该值。 如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。

    如果都找不到,调用valueForUndefinedKey:并抛出异常。

    至此,我们就完成了KVC的探索。

    相关文章

      网友评论

          本文标题:OC-KVC原理分析

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