zoukankan      html  css  js  c++  java
  • KVC与Runtime结合使用(案例)及其底层原理

    一、KVC 的用法和实践

    用法

    KVC(Key-value coding)键值编码,顾名思义。额,简单来说,是可以通过对象属性名称(Key)直接给属性值(value)编码(coding)“编码”可以理解为“赋值”。这样可以免去我们调用getter和setter方法,从而简化我们的代码,也可以用来修改系统控件内部属性,KVC是KVO、Core Data、CocoaBindings的技术基础,他们都是利用了OC的动态性

    KVC用法

    • setValue:forKey:(为对象的属性赋值)
    • setValue: forKeyPath:(为对象的属性赋值(包含了setValue:forKey:的功能,并且还可以对对象内的类的属性进行赋值))
    • valueForKey:(根据key取值)
    • valueForKeyPath:(根据keyPath取值)
    • setValuesForKeysWithDictionary:(对模型进行一次性赋值)

    为什么可以用NSNumber来接收int、float的数据类型?

    因为:使用valueForKey:时,KVC会自动将标量值(int、float、struct等)翻入NSNumber或NSValue中包装成一个对象,然后返回。因此,KVC有自动包装功能。 

    例如:生成一个这样子的对象Person
    person.h

    @class Car;
    @interface Person : NSObject
    @property (nonatomic,copy) NSString *name;
    @property (nonatomic,strong)Car *car;
    @end

    Car.h

    @interface Car : NSObject
    @property (nonatomic,strong) NSNumber *price;
    @end

    在ViewController.m中调用

    ViewController.m

    - (void)viewDidLoad {
          [super viewDidLoad];
          Person *person=[[Person alloc]init];
          [person setValue:@"lxh" forKey:@"name"];
          float price=100.0;
          Car *car=[[Car alloc]init];
          person.car=car;
          [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];
          NSLog(@"%@",person.name);
    
          NSLog(@"%f",car.price.floatValue);
    }

    注意点:

    1. 在Person中我仅仅只是声明了@class Car,而没有引用#import "Car.h",然后在ViewController.m中便可以对其进行: [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];这样子的赋值。所以说明KVC会去自动查找Car类进行赋值
    2. - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;你会发现value的值必须是id,也就是说不能传基本数据类型,必须是指针类型的变量。

    key和keyPath的区别

    keyPath方法是集成了key的所有功能,也就是说对一个对象的一般属性进行赋值、取值,两个方法是通用的,都可以实现。但是对对象中的对象进的属性行赋值,只有keyPath能够实现。

    setValuesForKeysWithDictionary:的巧妙使用(字典转模型) 

    -(instancetype)initWithDict:(NSDictionary *)dict{
             if (self = [super init]) {
                   [self setValuesForKeysWithDictionary:dict]; 
              } 
             return self;
    }

    注意点:

    • 字典转模型的时候,字典中的某一个key一定要在模型中有对应的属性
    • 如果一个模型中包含了另外的模型对象,是不能直接转化成功的。
    • 通过kvc转化模型中的模型,也是不能直接转化成功的
    • 底层还是调用了setValue: forKey:

    使用例子

    (1)修改系统控件内部属性(runtime + KVC)

    例如,界面设计图是这样的

    怎么感觉有点不同,这UIPageControl怎么跟我平常用的不一样?平常不都是这样的??如下图

    首先想到的肯定是,查看UIPageControl的头文件,如下

    NS_CLASS_AVAILABLE_IOS(2_0) @interface UIPageControl : UIControl 
    
    @property(nonatomic) NSInteger numberOfPages;          // default is 0
    @property(nonatomic) NSInteger currentPage;            // default is 0. value pinned to 0..numberOfPages-1
    
    @property(nonatomic) BOOL hidesForSinglePage;          // hide the the indicator if there is only one page. default is NO
    
    @property(nonatomic) BOOL defersCurrentPageDisplay;    // if set, clicking to a new page won't update the currently displayed page until -updateCurrentPageDisplay is called. default is NO
    - (void)updateCurrentPageDisplay;                      // update page display to match the currentPage. ignored if defersCurrentPageDisplay is NO. setting the page value directly will update immediately
    
    - (CGSize)sizeForNumberOfPages:(NSInteger)pageCount;   // returns minimum size required to display dots for given page count. can be used to size control if page count could change
    
    @property(nullable, nonatomic,strong) UIColor *pageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
    @property(nullable, nonatomic,strong) UIColor *currentPageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
    
    @end

    不够用啊兄弟。能不能给我个可以赋值UIImage对象的属性?看来正常途径使用系统的控件是设不了了!如何解呢 ?

    第一种方式:自定义UIPageControl   第二种方式:通过runtime遍历出UIPageControl所有属性(包括私有成员属性,runtime确实很强大)

    直接用第二种吧 第一种有兴趣的可以自己试试!

    使用runtime遍历UIPageControl结果如下打印:

    2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _lastUserInterfaceIdiom = q
    2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _indicators = @"NSMutableArray"
    2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _currentPage = q
    2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _displayedPage = q
    2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _pageControlFlags = {?="hideForSinglePage"b1"defersCurrentPageDisplay"b1}
    2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _currentPageImage = @"UIImage" // 当前选中图片
    2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _pageImage = @"UIImage" // 默认图片
    2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _currentPageImages = @"NSMutableArray"
    2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _pageImages = @"NSMutableArray"
    2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _backgroundVisualEffectView = @"UIVisualEffectView"
    2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _currentPageIndicatorTintColor = @"UIColor"
    2016-03-23 01:09:26.163 TenMinDemo[6224:507269] UIPageControl -> _pageIndicatorTintColor = @"UIColor"
    2016-03-23 01:09:26.163 TenMinDemo[6224:507269] UIPageControl -> _legibilitySettings = @"_UILegibilitySettings"
    2016-03-23 01:09:26.163 TenMinDemo[6224:507269] UIPageControl -> _numberOfPages = q

    结果非常满意,果然找到我想要的图片设置属性

    然后通过KVC设置自定义图片,实现了效果,代码如下

    UIPageControl *pageControl = [[UIPageControl alloc] init]; 
     [pageControl setValue:[UIImage imageNamed:@"home_slipt_nor"] forKeyPath:@"_pageImage"];
     [pageControl setValue:[UIImage imageNamed:@"home_slipt_pre"] forKeyPath:@"_currentPageImage"];

      

    (2) 在xib/Storyboard中,也可以使用KVC,下面是在xib中使用KVC把图片边框设置成圆角

     (3)id

    {
        "id" : "tripleCC",
        "age" : "30",
        "address" : "杭州",
        "schooll" : "HDU"
        ...
    }

    其中的id是什么?是Objective-C关键字,也就是说我定义以下属性会出现警告: 

    @property (nonatomic, strong) NSString *id;

    虽然可以使用以下方法,对模型中的成员变量进行统一设置,但是出现警告总归是不好的:

    - (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
    底层会调用 setValue:forKey:
    既然这样,可以选择手动一个个去实现。但是这样在数据少的时候可以试试,在数据比较多时就不太现实了,程序的可扩展性也不好。
    两种解决方法:

    方式1.重写setValue:forKey:

    setValuesForKeysWithDictionary:的底层是调用setValue:forKey:的,所以可以考虑重写这个方法,并且判断其key是id时,手动转换成模型的成员变量名,这里假设把id对应成以下属性:

    @property (nonatomic, strong) NSString *ID;

    有了对应的属性名后,就可以重写底层方法了

    - (void)setValue:(id)value forKey:(NSString *)key
    {
        if ([key isEqualToString:@"id"]) {
            [self setValue:value forKeyPath:@"ID"];
        }else{
            [super setValue:value forKey:key];
    
        }
    }

    这样,当使用setValuesForKeysWithDictionary:就不会出现模型中找不到对应的成员变量的错误了。

    方式2.使用runtime

    由于需要针对所有模型使用,可以将其设置为NSObject分类
    // dict  -> 资源文件提供的字典
    // mapDict  -> 提供的key映射(实际变量名:资源文件key)
    + (instancetype)objcWithDict:(NSDictionary *)dict mapDict:(NSDictionary *)mapDict
    {
        id objc = [[self alloc] init];
    
    
        // 遍历模型中成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(self, &outCount);
    
        for (int i = 0 ; i < count; i++) {
            Ivar ivar = ivars[i];
    
            // 成员变量名称
            NSString *ivarName = @(ivar_getName(ivar));
    
            // 获取出来的是`_`开头的成员变量名,需要截取`_`之后的字符串
            ivarName = [ivarName substringFromIndex:1];
    
            id value = dict[ivarName];
            // 由外界通知内部,模型中成员变量名对应字典里面的哪个key
            // ID -> id
            if (value == nil) {
                if (mapDict) {
                    NSString *keyName = mapDict[ivarName];
    
                    value = dict[keyName];
                }
            }
            [objc setValue:value forKeyPath:ivarName];
        }
        return objc;
    }

     使用方法:

    + (instancetype)itemWithDict:(NSDictionary *)dict
    {
        // 传入key和实例变量名的映射字典@{@"ID":@"id"}
        TPCItem *item = [TPCItem objcWithDict:dict mapDict:@{@"ID":@"id"}];
    
        return item;
    }

    二、底层原理的分析

    KVC的赋值原理

    setValue:forKey:赋值原理如下:

    • 去模型中查找有没有对应的setter方法:例如:setIcon方法,有就直接调用这个setter方法给模型这个属性赋值[self setIcon:dic[@"icon"]];
    • 如果找不到setter方法,接着就会去寻找有没有icon属性,如果有,就直接访问模型中的icon属性,进行赋值,icon=dict[@"icon"];
    • 如果找不到icon属性,接着又会去寻找_icon属性,如果有,直接进行赋值_icon=dict[@"icon"];
    • 如果都找不到就会报错:[<Flag 0X7fb74bc7a2c0> setValue:forUndefinedKey:]
    • 如果对某个类,不允许使用KVC,可以通过设置 accessInstanceVariablesDirectly 控制。

      // 在该类的内部,重写此方法,外部使用KVC时,禁用没有写set get 方法的属性值。
      // 注意:对于 @property 定义的属性可以 KVC+   
      -(BOOL)accessInstanceVariablesDirectly{ 
        return NO;
      }
    • 赋值检查
      // 在类的内部,进行检查,不符合要求 返回NO ,提供外部参考。
      - (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError{
         if ([inKey isEqualToString:@"colors"] && [*ioValue isKindOfClass:[NSArray class]]) { 
                  return YES; 
            } else { 
                  return NO; 
            }
      }
      //用法:
      // 外部 使用时,先判断是否符合要求,再使用KVC。 
      NSError *error; 
      NSString *apoint = @"name"; 
      if ([aPerson validateValue:&apoint forKey:@"_colors" error:&error]) { 
          NSLog(@"可以赋值 apoint"); 
          [aPerson setValue:apoint forKey:@"_colors"]; 
      } else { 
          NSLog(@"不可以赋值 apoint");  
          NSLog(@"%@",error.debugDescription); 
      }

    KVC内部的实现

    比如说如下的一行KVC的代码:

    [site setValue:@"sitename" forKey:@"name"];

    就会被编译器处理成:

    SEL sel = sel_get_uid ("setValue:forKey:");

    IMP method = objc_msg_lookup (site->isa,sel);

    method(site, sel, @"sitename", @"name");

    这下KVC内部的实现就很清楚的清楚了:一个对象在调用setValue的时候,(1)首先根据方法名找到运行方法的时候所需要的环境参数。(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。(3)再直接查找得来的具体的方法实现。

     

  • 相关阅读:
    BZOJ3670: [Noi2014]动物园
    BZOJ4424: Cf19E Fairy
    BZOJ1257: [CQOI2007]余数之和
    BZOJ2438: [中山市选2011]杀人游戏
    SDOI2017第一轮
    BZOJ4820: [Sdoi2017]硬币游戏
    NOIP2016
    HDU1848 Fibonacci again and again(SG 函数)
    HDU1517 Multiply Game
    HDU1907 Jhon
  • 原文地址:https://www.cnblogs.com/junhuawang/p/5802516.html
Copyright © 2011-2022 走看看