KVC 使用方法详解及底层实现

你要知道的KVC、KVO、Delegate、Notification都在这里

转载请注明出处 http://www.jianshu.com/p/fa941b769606

本系列文章主要通过讲解KVC、KVO、Delegate、Notification的使用方法,来探讨KVO、Delegate、Notification的区别以及相关使用场景,本系列文章将分一下几篇文章进行讲解,读者可按需查阅。

  • KVC 使用方法详解及底层实现
  • KVO 正确使用姿势进阶及底层实现
  • Protocol与Delegate 使用方法详解
  • NSNotificationCenter 通知使用方法详解
  • KVO、Delegate、Notification 区别及相关使用场景

KVC使用方法详解与底层实现

KVC(key value coding)键值编码是一种可以使用字符串形式来间接操作对象相关属性的方法。KVC需要由类别Category NSKeyValueCoding来支持,OC在实现KVC时没有采用实现接口的方式,而是针对NSObject创建了一个类别,通过这样的方式使得NSObject的子类可以自行实现NSKeyValueCoding类别定义的相关方法。

KVC使用非常简单,但KVC却异常强大,最暗黑的功能就是它可以无视访问限制,无论是否为private都可以进行赋值或取值操作,readonly的属性也可以无视,提供了一种比runtime更便捷的方式来修改或访问系统级隐藏的属性,因此,经常在开发中通过runtime获取相关属性名后使用KVC来修改那些只读readonly或隐藏的属性。

KVC基础方法详解

KVC常用方法主要由如下几个:

//获取属性名为key的属性的值
- (nullable id)valueForKey:(NSString *)key;

//设置属性名为key的属性的值为value
- (void)setValue:(nullable id)value forKey:(NSString *)key;

/*
提供一种类似于Java ONGL语法来访问嵌套属性
获取嵌套属性名为keyPath的属性的值
*/
- (nullable id)valueForKeyPath:(NSString *)keyPath;

//设置嵌套属性名为keyPath的属性的值为value
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

/*
获取属性名为key的属性值时,如果属性不存在则执行该方法,可自定义实现,
默认实现方式为抛出NSUnknownKeyException异常
*/
- (nullable id)valueForUndefinedKey:(NSString *)key;

/*
设置属性名为key的属性值为value时,如果属性不存在则执行该方法,可自定义实现,
默认实现方式为抛出NSUnknownKeyException异常
*/
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

针对上述方法举一个栗子:

//Phone类
@interface Phone : NSObject
@property (nonatomic, strong) NSString *phoneNumber;
@end

@implementation Phone
@synthesize phoneNumber = _phoneNumber;
@end

//Person类
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
//组合一个Phone的对象
@property (nonatomic, strong) Phone *phone;
- (void)showMyself;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;
@synthesize phone = _phone;

- (void)showMyself {
    NSLog(@"My name is %@ I am %ld years old. my phone number is %@", self.name, self.age, self.phone.phoneNumber);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        [p setValue:@"Jiaming Chen" forKey:@"name"];
        [p setValue:@22 forKey:@"age"];
        [p setValue:[[Phone alloc] init] forKey:@"phone"];
        [p setValue:@"18666668888" forKeyPath:@"phone.phoneNumber"];
        //输出: My name is Jiaming Chen I am 22 years old. my phone number is 18666668888
        [p showMyself];
        //输出: Name: Jiaming Chen
        NSLog(@"Name: %@", [p valueForKey:@"name"]);
        //输出: Age: 22
        NSLog(@"Age: %@", [p valueForKey:@"age"]);
        //输出: Phone Number: 18666668888
        NSLog(@"Phone Number: %@", [p valueForKeyPath:@"phone.phoneNumber"]);
    }
    return 0;
}

上面的栗子使用了setValue:forKeyvalueForKey:setValue:forKeyPathvalueForKeyPath方法。Person类组合了Phone类,因此在访问phone属性phoneNumber属性时,需要使用keyPath这样的字符串点语法,可以根据实际情况一直嵌套下去。这个栗子比较简单,不做过多赘述。接下来在看一个栗子:

@interface Person : NSObject
{
    @private
    NSString *name;
    NSString *_name;
}

- (void)outputAddress;

@end

@implementation Person
{
    NSInteger age;
}

- (void)outputAddress
{
    NSLog(@"Address name: %p _name: %p", name, _name);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        [p setValue:@"Jiaming Chen" forKey:@"name"];
        [p setValue:@"CCCC" forKey:@"_name"];
        [p setValue:@22 forKey:@"age"];
        
        //输出: Name: CCCC 0x1000010a8
        NSLog(@"Name: %@ %p", [p valueForKey:@"name"], [p valueForKey:@"name"]);
        //输出: _Name: CCCC 0x1000010a8
        NSLog(@"_Name: %@ %p", [p valueForKey:@"_name"], [p valueForKey:@"_name"]);
        //输出: Age: 22
        NSLog(@"Age: %@", [p valueForKey:@"age"]);
        //输出: Address name: 0x0 _name: 0x1000010a8
        [p outputAddress];

    }
    return 0;
}

为了展示实验效果这里没有使用合成存取方法,Person类声明的属性name_name以及age都是private的,但是KVC依旧可以为其设置值,同样的也可以获取private属性的值,这就是KVC的强大之处。

但似乎上面栗子的输出结果与我们预期不同,明明通过setValue:forKey:name属性设置的值是Jiaming Chen但通过valueForKey:输出的结果却与_name属性值一致,连输出的地址都一样。通过outputAddress方法输出name_name的地址后发现name的地址为0x0,这表示其并未初始化,出现这种情况的原因正是因为KVC获取值和赋值的顺序有关,由于篇幅问题,这里没有给出所有的实验过程,有兴趣的读者可以按照下述顺序自行实验,通过实验可得如下赋值顺序:

  • 首先通过setter方法即set(Key属性名):,这里是setName:方法进行赋值。
  • 如果没有setter方法,寻找_(key属性名),这里是_name成员变量,无视该成员变量的访问修饰符,也无视该成员变量是在@interface的类接口部分定义的还是在@implementation类实现部分定义的,只要存在该名称的成员变量就为其赋值。
  • 如果没有setter方法也没有_(key属性名),这里是_name成员变量,就会寻找key属性名,这里是name成员变量,同样无视其访问修饰符,无视其定义位置,只要存在该名称的成员变量就为其赋值。
  • 如果setter_(key属性名)key属性名都不存在则会调用setValue:forUndefinedKey:方法,该方法默认实现是抛出NSUnknownKeyException异常。

同样的,对于valueForKey:方法来获取值的顺序如下:

  • 首先通过getter方法来获取值,这里为name方法。
  • 如果没有getter方法则会查找名称为_(key属性名)这里为_name的成员变量,同样无视访问修饰符,无视定义位置,只要存在该成员变量就返回其值。
  • 如果没有getter方法也没有_(key属性名)成员变量,则查找名称为key属性值这里为name的成员变量,同样无视访问修饰符,无视定义位置,只要存在该成员变量就返回其值。
  • 如果getter_(key属性名)key属性名都不存在则会调用valueForKey方法,该方法默认实现是抛出NSUnknownKeyException方法。

当我们清楚的认识到上述KVC获取值和赋值的相关顺序后,也就理解了前一个栗子结果产生的原因,通过上面的讲解也可以发现其实KVC方法的效率并不高,KVC还是要去搜索gettersetter搜索各种成员变量,显然通过直接赋值或获取值效率更高,所以,在普通情况下尽量不要使用KVC这样的方式。

接下来再举一个在实际开发中常使用的栗子:

#import 
//Person类
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
//服务端为id,由于id是OC的关键字,取名为idNumber
@property (nonatomic, copy) NSString *idNumber;
- (void)showMyself;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;
@synthesize idNumber = _idNumber;

- (void)showMyself {
    NSLog(@"Name: %@ Age: %ld idNumber: %@", self.name, self.age, self.idNumber);
}

- (nullable id)valueForUndefinedKey:(NSString *)key
{
    return nil;
}

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
{
    //如果这个key为id
    if ([key isEqualToString:@"id"])
    {
        //调用setValue:forKey方法为idNumber赋值
        [self setValue:value forKey:@"idNumber"];
    }
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //假设为服务端获取的json数据转换的dictionary
        NSDictionary *dict = @{@"name": @"Jiaming Chen", @"age": @20, @"id": @"1603121434"};
        
        Person *p = [[Person alloc] init];
        //遍历上述字典的key
        [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            //直接使用kvc赋值,不需要再写一行一行代码赋值
            [p setValue:obj forKey:key];
        }];
        //输出: Name: Jiaming Chen Age: 20 idNumber: 1603121434
        [p showMyself];
    }
    return 0;
}

上面的栗子在Person类中自定义实现了valueForUndefinedKey:setValue:forUndefinedKey:方法,如果不实现该方法设置不存在的key时默认抛出异常,在实际开发中通常需要从服务端获取大量的json数据,转换为字典后往往需要一个属性一个属性的赋值,使用KVC方法就能够避免编写冗长的代码,但有时服务端和客服端的数据名称会有不同,此时可以按情况在setValue:forUndefinedKey:方法中进行处理。

在实际开发中还遇到过一种情况,iOS端的对象使用NSString类型存储用户ID,但服务端返回的是int类型的数据,在赋值时就会崩溃,解决该问题需要我们自己实现setValue:forKey:方法,在该方法中判断value的类型后手动转换即可,在此不再赘述。

通过上面的栗子,如果需要使用KVC进行赋值操作,最好按照需求自定义实现valueForUndefinedKey:setValue:forUndefinedKey:以及setValue:forKey:方法。

KVC修改readonly的系统隐藏变量

首先上一张阿里云iOS端app的图,如下图所示:

KVC 使用方法详解及底层实现_第1张图片
阿里云iOS端首页

我们发现首页上方旋转木马的UIPageControl不是传统的圆形而是长条形,如果不使用自定义控件或是使用h5实现,那我们该如何实现这个效果呢?

首先我们使用如下代码创建一个UIPageControl:

- (instancetype)init
{
    if (self=  [super init])
    {
        self.view.backgroundColor = [UIColor whiteColor];
        
        UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 200, ScreenWidth, 200)];
        containerView.backgroundColor = [UIColor greenColor];
        [self.view addSubview:containerView];
        
        UIPageControl *pageControler = [[UIPageControl alloc] initWithFrame:CGRectMake(0, 180, ScreenWidth, 20)];
        pageControler.numberOfPages = 4;
        [pageControler setPageIndicatorTintColor:[UIColor blueColor]];
        [pageControler setCurrentPageIndicatorTintColor:[UIColor blackColor]];
        [containerView addSubview:pageControler];       
    }
    return self;
}

实现效果如下图:

KVC 使用方法详解及底层实现_第2张图片
UIPageControl基本样式

首先查看UIPageControl提供给我们可访问的属性,看一下有没有可以操作的属性,这里可以自行查看,我们发现并没有这样的属性存在,这个时候该怎么办呢?接着我们可以使用runtimeUIPageControl的所有属性都打印出来,runtime的强大之处就在于可以获取类的任意属性和方法,关于runtime部分本博客有一系列文章来讲解,有兴趣的读者可以自行查阅iOS runtime探究(一): 从runtime开始理解面向对象的类到面向过程的结构体

我们先打印出UIPageControl所有属性,看一下有没有我们需要的,代码如下:

执行下述代码需要import 头文件

unsigned int count = 0;
//该方法是C函数,获取所有属性
Ivar * ivars = class_copyIvarList([pageControler class], &count);
for (unsigned int i = 0; i < count; i ++) 
{
    Ivar ivar = ivars[i];
    //获取属性名
    const char * name = ivar_getName(ivar);
    //使用KVC直接获取相关属性的值
    NSObject *value = [pageControler valueForKey:[NSString stringWithUTF8String:name]];
    NSLog(@"%s %@", name, value);
}
//需要释放获取到的属性
free(ivars);

输出如下:

_lastUserInterfaceIdiom -1
_indicators (
    ">",
    ">",
    ">",
    ">"
)
_currentPage 0
_displayedPage 0
_pageControlFlags (null)
_currentPageImage (null)
_pageImage (null)
_currentPageImages (null)
_pageImages (null)
_backgroundVisualEffectView (null)
_currentPageIndicatorTintColor UIExtendedGrayColorSpace 0 1
_pageIndicatorTintColor UIExtendedSRGBColorSpace 0 0 1 1
_legibilitySettings (null)
_numberOfPages 4        

从属性名我们发现了几个比较重要的属性_currentPageImage_pageImage_currentPageImages_pageImages,通过属性名称可以判断这些就是我们要找的属性,接着使用KVC为其设置我们自己的图片,代码如下:

[pageControler setValue:[UIImage imageNamed:@"line"] forKeyPath:@"pageImage"];
[pageControler setValue:[UIImage imageNamed:@"current"] forKeyPath:@"currentPageImage"];

实现效果如下:

KVC 使用方法详解及底层实现_第3张图片
修改后的效果

在我们需要修改系统提供UI界面而又束手无策时可以使用runtime获取属性来查看是否有可以使用的属性或方法,接着可以使用KVC获取相关值或进行赋值操作,这种方法可能也会存在风险,如果获取的是苹果禁用的私有API那就只能乖乖想别的方法了,不过KVC提供了一种修改系统实现的思路。

KVC底层实现

首先,继续第一个栗子,我们实现如下代码:

#import 

@interface Person : NSObject
//为了方便查看重写的代码将name改成cjmName
@property (nonatomic, copy) NSString *cjmName;
@property (nonatomic, assign) NSUInteger age;
- (void)showMyself;

@end

@implementation Person

@synthesize cjmName = _cjmName;
@synthesize age = _age;

- (void)showMyself {
    NSLog(@"Name: %@ Age: %ld", self.cjmName, self.age);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        [p setValue:@"Jiaming Chen" forKey:@"cjmName"];
        [p setValue:@22 forKey:@"age"];
        
        p.cjmName = @"CCCC";
        
        [p showMyself];
    }
    return 0;
}

接着使用clang -rewrite-objc main.m重写为cpp文件,查看main函数重写后的代码如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));

        ((void (*)(id, SEL, id _Nullable, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setValue:forKey:"), (id _Nullable)(NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_1, (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_2);
        ((void (*)(id, SEL, id _Nullable, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setValue:forKey:"), (id _Nullable)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 22), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_3);

        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setCjmName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_4);

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));
    }
    return 0;
}

通过上面的重写代码似乎没有什么特别的发现,对于setValue:forKey:方法的调用与普通方法相同,所以,这里猜测底层实现可能是在执行KVC相关方法时,在继承树上沿着isa指针按照之前讲解的顺序去查找相关属性进行赋值和获取值的操作。如有读者清楚还请不吝赐教。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

你可能感兴趣的:(KVC 使用方法详解及底层实现)