当前位置:首页 > 分类68 > 正文

KVC和KVO学习笔记

摘要: KVC和KVO学习笔记KVC和KVO学习笔记在编程中,最常见的就是程序的流程取决于你所使用的各种变量和属性的值,根据变量和属性的...
KVC和KVO学习笔记

KVC和KVO学习笔记

在编程中,最常见的就是程序的流程取决于你所使用的各种变量和属性的值,根据变量和属性的值确定后面运行的代码,有时会检查对象是否已加入数组,或是否已被移除,因此,获取类中属性的变化是编程中重要部分。
我们有多种方式获取对象的改变,如委托、通知等。如果需要观察多个属性的变化,为避免产生大量的代码,最好是使用键值观察(Key Value Observing,简称KVO),这也是Apple在自己的软件中大量使用的一种。
使用键值观察跟踪单个属性或集合(如数组)的变化非常高效,它只需要在观察者方法中添加代码,不需要修改被观察文件内的代码,这一点和委托、通知不同。但需要注意的是,键值观察(KVO)是建立在键值编码(Key Value Coding,简称KVC)的基础上,也就是说任何你想使用KVO观察的属性必须符合键值编码。
KVC和KVO提供了一个强大高效的方式来编写代码,学习KVO前必须先掌握KVC,所以下面我们结合demo来学习KVC。在这个demo中所有结果将直接在控制台输出,没有创建用户界面。
1. 创建应用启动Xcode,点击File > New > Project…,选择iOS > Application > Single View Application模板,点击Next;在Product Name一栏填写KVC&KVODemo,点击Next;选择文件位置,点击Create创建工程。
2. 键值编码假设我们有一个NSString类型的firstName的属性,我们想把Donald赋值给属性,我们可以使用下面两种方式之一。
self.firstName = @"Donald";     // 1_firstName = @"Donald";         // 2
上面的代码我们非常熟悉,1直接为属性赋值,2直接给实例变量赋值。如果使用KVC设值,代码如下:
[self setValue:@"Donald" forKey:@"firstName"];
你会发现使用KVC和词典中设值或将标量值和结构值转换为NSValue非常类似。再举一例,下面代码3使用设值方法设值,4使用KVC模式设值。
[someObject.someProperty setText:@"This is a text"];                       // 3[self setValue:@"This is a text" forKey:@"someObject.someProperty.text"];  // 4
在第一个示例中,我们用KVC替代直接赋值,在第二个示例中,我们用KVC替代访问器方法设值。使用KVC时我们只需要将值与KeyKeyPath匹配就可以,使用字符串间接把值赋给属性。如果需要获取属性的值,可用下面方式:
NSLog(@"%@",[self valueForKey:@"firstName"]);
键值编码机制是由一个NSKeyValueCoding非正式协议定义的,NSObject实现了这个协议,所以我们继承NSObject才能让我们的类获得KVC能力。理论上,如果你的类遵守NSKeyValueCoding协议,也可以自己实现KVC的细节,这样做完全行得通,但这样太不值得了,也太占用时间了。
打开Xcode,点击File > New > File…,或使用快捷键(?+N)创建一个类。在弹出窗口中,选择iOS > Source > Cocoa Touch Class模板,点击Next;类名称为Children,父类为NSObject,点击Next;选择文件位置,点击Create创建文件。
进入Children.h文件,添加两个属性,一个是firstName,一个是age,我们将使用这两个属性展示KVC的主要特性。更新后代码如下:
@interface Children : NSObject@property (nonatomic, strong) NSString *firstName;@property (nonatomic, assign) NSUInteger age;@end
进入Children.m文件初始化上面两个属性。
@implementation Children- (instancetype)init{    self = [super init];    if (self)    {        _firstName = @"";        _age = 0;    }        return self;}@end
进入ViewController.m文件,导入Children.h,声明两个Children类型的属性。代码如下:
#import "ViewController.h"#import "Children.h"@interface ViewController ()@property (nonatomic, strong) Children *child1;@property (nonatomic, strong) Children *child2;@end
viewDidLoad方法中,初始化child1对象,使用KVC方法先设值、后取值并输出到控制台。
- (void)viewDidLoad{    [super viewDidLoad];        // child1    self.child1 = [Children new];        // 1.使用KVC设值    [self.child1 setValue:@"Jr" forKey:@"firstName"];    [self.child1 setValue:[NSNumber numberWithUnsignedInteger:39] forKey:@"age"];        // 2. 取值 输出到控制台    NSString *childFirstName = [self.child1 valueForKey:@"firstName"];    NSUInteger child1Age = [[self.child1 valueForKey:@"age"] unsignedIntegerValue];    NSLog(@"%@,%lu",childFirstName,child1Age);}
在1中使用setValue: forKey:为属性设值。需要注意的是age是数字,因此不能直接作为参数,需要转换为NSNumber类型,另外键(Key)的字符串必须和属性中的名称一致,否则运行时app会崩溃,弹出Terminating app due to uncaught exception 'NSUnknownKeyException',提示。在2中,使用valueForKey:取值,输出到控制台。
Jr,39
目前为止,我们已经学习了如何编写符合KVC的代码,如何使用KVC设值和取值,以及Key写错会如何。现在开始学习一下如何使用KeyPath。首先进入Children.h文件,添加一个Children类型的属性。
@interface Children : NSObject···@property (nonatomic, strong) Children *child;@end
返回到ViewController.m,在viewDidLoad方法中,初始化child2并设值,最后初始化child属性。
- (void)viewDidLoad{    ···    // child2    self.child2 = [Children new];    [self.child2 setValue:@"Ivanka" forKey:@"firstName"];    [self.child2 setValue:[NSNumber numberWithUnsignedInteger:35] forKey:@"age"];    self.child2.child = [Children new];    }
现在使用setValue: forKeyPath:child属性设值,这里的键是一个使用点语法的字符串@"child.firstName"
- (void)viewDidLoad{    ...    [self.child2 setValue:@"Eric" forKeyPath:@"child.firstName"];    [self.child2 setValue:[NSNumber numberWithUnsignedInteger:33] forKeyPath:@"child.age"];        NSLog(@"%@,%lu",self.child2.child.firstName, self.child2.child.age);}
最后使用NSLog测试设值是否成功。输出是:
Eric,33
valueForKey:valueForKeyPath:是在NSKeyValueCoding非正式协议中定义的方法,两者默认由根类NSObject实现,是KVC框架的一部分。
objectForKey:是由NSDictionary提供提取对应键的值的方法。
虽然在词典中使用valueForKey:也可以提取到值,但当key字符串以@开头时会遇到问题。所以在词典中使用objectForKey:,在KVC中使用valueForKey:valueForKeyPath:
3. 键值观察我们已经掌握了KVC,现在开始学习KVO。以下是实现KVO的步骤:
使用addObserver: forKeyPath: options: context:方法注册为观察者,用于观察其他类的属性。观察者必须实现observerValueForKeyPath: ofObject: change: context:方法以接收属性变化通知。使用removeObserver: forKeyPath: context:方法移除观察者。3.1 观察单个属性进入ViewController.m,在实现部分添加viewWillAppear:方法,在viewWillAppear:方法内添加firstNameage属性为观察对象。
- (void)viewWillAppear:(BOOL)animated{    [super viewWillAppear:animated];        [self.child1 addObserver:self forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];    [self.child1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];}
上面添加观察者方法中各参数含义如下:
addObserver: 注册成为观察者,以便接收KVO通知,通常为self,该对象必须实现observerValueForKeyPath: ofObject: change: context:以接收属性变化通知。keyPath:要观察的属性字符串,必须和属性一致,不能为空。options:用来指定通知词典中应包含值类型。如果参数是NSKeyValueObservingOptionNew,词典包含新产生的值;如果参数是NSKeyValueObservingOptionOld,词典包含变化前的值;如果参数是数字0,词典不包括任何值;如果需要change词典同时包括新产生值和变化前的旧值,可以像上面代码一样使用|,即或运算符;任何时候都可以使用[object valueForKey:<Key>]方法获取属性变化产生的新值。context:这是一个指针,可用做我们观察到的属性更改的唯一标志符,经常设置为NULL,后面会详细说明。现在我们已经可以观察到firstNameage两个属性的的变化,KVO观察到每一个观察对象的变化都会调用observerValueForKeyPath: ofObject: change: context:方法,如果观察多个属性的变化,观察方法内if语句可能很长,下面实现observerValueForKeyPath: ofObject: change: context:方法。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{    if ([keyPath isEqualToString:@"firstName"])    {        NSLog(@"The name of the child was changed.\n %@",change);    }    else if ([keyPath isEqualToString:@"age"])    {        NSLog(@"The new value is %@,The old value is %@",[change valueForKey:NSKeyValueChangeNewKey],[change valueForKey:NSKeyValueChangeOldKey]);    }}
在上面代码中,我们根据参数keyPath判断哪一个属性改变了,之后输出。在输出时,可以直接输出词典change,也可以用valueForKey:获取词典中的值,此处的键用NSKeyValueChangeNewKeyNSKeyValueChangeOldKey,前者获取新产生的值,后者获取改变前的旧值。现在在viewwillAppear:底部添加下面代码来验证是否可以观察到属性变化。
- (void)viewWillAppear:(BOOL)animated{    ...    // 添加观察者后改变值 验证是否可以观察到值变化    [self.child1 setValue:@"Tiffany" forKey:@"firstName"];    [self.child1 setValue:[NSNumber numberWithUnsignedInteger:23] forKey:@"age"];}
运行,输出内容为:
The name of the child was changed. {    kind = 1;    new = Tiffany;    old = Jr;}The new value is 23,The old value is 39
你可以从change词典中提取你需要的值,有了KVO观察属性变化变的如此简单。现在在viewWillAppear:方法中添加观察者,观察child2对象的属性变化,随后为age设值。
- (void)viewWillAppear:(BOOL)animated{    ...        // 观察child2属性变化 设值    [self.child2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];    [self.child2 setValue:[NSNumber numberWithUnsignedInteger:64] forKey:@"age"];}
运行后输出如下:
The name of the child was changed. {    kind = 1;    new = Tiffany;    old = Jr;}The new value is 23,The old value is 39The new value is 64,The old value is 35
正如看到的一样,我们收到两个age属性的改变通知,尽管我们自己可以区分出每一个通知来自哪一个对象属性的改变,但在程序中,目前我们无法对此进行区分。为解决这个问题,我们将使用addObserver: forKeyPath: options: context:方法中的context参数。context参数一般使用下面声明方法。
static void *XXContext = &XXContext;
表示一个静态变量存放着它自己的指针,也就是它自己什么也没有。因为要在addObserver: forKeyPath: options: context:observerValueForKeyPath: ofObject: change: context:两个方法中使用context,这里的context声明为静态全局变量。
ViewController.m实现前添加下面两个声明。
@endstatic void *child1Context = &child1Context;static void *child2Context = &child2Context;@implementation ViewController
你也可以把context声明为属性,但声明为全局变量更为简单。现在修改viewWillAppear:方法中的添加观察者方法,将context参数中的NULL替换为刚声明的全局变量。
- (void)viewWillAppear:(BOOL)animated{    [super viewWillAppear:animated];        [self.child1 addObserver:self forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child1Context];    [self.child1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child1Context];        // 添加观察者后改变值 验证是否可以观察到值变化    [self.child1 setValue:@"Tiffany" forKey:@"firstName"];    [self.child1 setValue:[NSNumber numberWithUnsignedInteger:23] forKey:@"age"];        // 观察child2属性变化 设值    [self.child2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child2Context];    [self.child2 setValue:[NSNumber numberWithUnsignedInteger:64] forKey:@"age"];}
最后修改observerValueForKeyPath: ofObject: change: context:方法,以便区分出通知来自哪一个对象属性的变化。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{    ...    // 使用context后    if (context == child1Context)    {        if ([keyPath isEqualToString:@"firstName"])        {            NSLog(@"The name of the FIRST child was changed.\n %@",change);        }        else if ([keyPath isEqualToString:@"age"])        {            NSLog(@"The new value of the FIRST child is %@,The new value of the FIRST child is %@",[change valueForKey:NSKeyValueChangeNewKey],[change valueForKey:NSKeyValueChangeOldKey]);        }    }    else if (context == child2Context)    {        if ([keyPath isEqualToString:@"age"])        {            NSLog(@"The new value of the SECOND child is %@,The new value of the SECOND child is %@",[change valueForKey:NSKeyValueChangeNewKey],[change valueForKey:NSKeyValueChangeOldKey]);        }    }}
3.2 注册相互影响的键在许多情况下,一个属性的值取决另一个对象中的一个或多个其他属性的值。如果一个属性的值改变,那么派生属性的值也应该改变。例如:姓名由姓和名两个属性组成,当其中任何一个属性变化时,姓名属性都要得到改变的通知。
进入Children.h,添加NSString类型的fullName属性和lastName两个属性。
@interface Children : NSObject...@property (nonatomic, strong) NSString *fullName;@property (nonatomic, strong) NSString *lastName;@end
进入Children.m,初始化刚声明的属性,fullNamefirstNamelastName组成。
- (instancetype)init{    self = [super init];    if (self)    {        ...        _lastName = @"";    }        return self;}- (NSString *)fullName{    return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];}
firstNamelastName属性变化时,必须通知fullName属性的应用程序,因为它们会影响fullName属性的值。可以通过实现类方法keyPathsForValuesAffecting<key>来获取哪些属性会影响<key>属性,这里的<key>fullName,首字母要大写。在Children.m中添加以下类方法:
+ (NSSet *)keyPathsForValuesAffectingFullName{    return [NSSet setWithObjects:@"firstName",@"lastName", nil];}
进入ViewController.m文件,在viewWillAppear:方法中添加观察者,观察fullname属性,之后修改lastNamefirstName属性。
- (void)viewWillAppear:(BOOL)animated{    ...    // 添加观察者 观察fullName属性 修改firstName lastName    [self.child1 addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child1Context];    self.child1.lastName = @"Trump";    self.child1.firstName = @"Ivana";}
observerValueForKeyPath: ofObject: change: context:方法中,观察到fullname属性变化时进行输出。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{    ...    // 使用context后    if (context == child1Context)    {        ...        else if ([keyPath isEqualToString:@"fullName"])        {            NSLog(@"The full name of First child was change.\n %@",change);        }    }    ...}
现在你可以运行下,可以输出fullname属性的变化。
The full name of First child was change. {    kind = 1;    new = "Tiffany Trump";    old = "Tiffany ";}The full name of First child was change. {    kind = 1;    new = "Ivana Trump";    old = "Tiffany Trump";}The name of the FIRST child was changed. {    kind = 1;    new = Ivana;    old = Tiffany;}
3.3 观察数组NSArray是KVC和KVO中的一种特殊情况,想要观察到数组的变化需要做一些额外的工作。事实上,有很多关于数组的细节,但在这里,我们只讲解一些基础的、重要的内容。因为数组不符合KVC,因此观察数组不像观察上面示例中的属性那么简单。我们要实现一些关于数组的方法以便使数组符合KVC,进而可以使用KVO观察数组的变化。
这里我们将讨论可变数组,不可变数组与可变数组类似,只是需要实现的方法少一些。假设我们有一个可变数组myArray,这里需要实现的方法与数组的插入、移除、计数类似,不同之处在于数组的名称,需要实现方法如下:
countOfMyArrayobjectInMyArrayAtIndex:insertObject:inMyArrayAtIndex:removeObjectFromMyArrayAtIndex:这些方法都很熟悉,不同的是我们用数组的名称替换里面名称。如果是不可变数组,只需要取消实现最后两个方法。
让数组符合KVC有好的一方面,也有不利的一方面。好处是Xcode会对数组名建议补全;坏的一方面是类中每一个想使用KVC观察的数组都要实现这些方法,会产生大量代码。为了避免产生大量重复代码,我们可以创建一个新的类,类内只包含一个可变数组,在这个数组内实现这些方法让这个数组符合KVC,这样在其他类中使用这个类的实例对象。这样的好处是:让数组符合KVC,必须实现的方法只需要实现一次,这个类可以重复使用。你可以理解为这是一个高级版本的数组。
现在添加一个新类,点击File > New > File…,选取iOS > Source > Cocoa Touch Class模板,点击Next;类名称为KVCMutableArray,父类为NSObject,点击Next;选择文件位置,点击Create创建文件。
进入KVCMutableArray.h文件,声明一个可变数组及一些方法以便数组符合KVC。
@interface KVCMutableArray : NSObject@property (nonatomic, strong) NSMutableArray *array;- (NSUInteger)countOfArray;- (id)objectInArrayAtIndex:(NSUInteger)index;- (void)insertObject:(id)object inArrayAtIndex:(NSUInteger)index;- (void)removeObjectFromArrayAtIndex:(NSUInteger)index;- (void)replaceObjectInArrayAtIndex:(NSUInteger)index withObject:(id)object;@end
在上面insertObject: inArrayAtIndex:方法中,object对象这里设定为id类型,以便其他类可以使用。进入KVCMutableArray.m,添加init方法,初始化array,实现头文件中声明的方法。
@implementation KVCMutableArray- (instancetype)init{    self = [super init];    if (self)    {        _array = [NSMutableArray new];    }        return self;}- (NSUInteger)countOfArray{    return self.array.count;}- (id)objectInArrayAtIndex:(NSUInteger)index{    return [self.array objectAtIndex:index];}- (void)insertObject:(id)object inArrayAtIndex:(NSUInteger)index{    [self.array insertObject:object atIndex:index];}- (void)removeObjectFromArrayAtIndex:(NSUInteger)index{    [self.array removeObjectAtIndex:index];}- (void)replaceObjectInArrayAtIndex:(NSUInteger)index withObject:(id)object{    [self.array replaceObjectAtIndex:index withObject:object];}@end
到目前我们已经创建了一个符合KVC的数组。
现在我们要在Children.h中添加一个可变数组,数组内包括姓名,用KVO观察数组内容的变化。在这里我们应该使用KVCMutableArray类型的属性,而不是系统提供的默认数组,进入Children.h,导入KVCMutableArray.h文件,添加新的属性。
#import <Foundation/Foundation.h>#import "KVCMutableArray.h"@interface Children : NSObject...@property (nonatomic, strong) KVCMutableArray *cousins;@end
Children.m中初始化刚声明的对象。
- (instancetype)init{    self = [super init];    if (self)    {        ...        _cousins = [KVCMutableArray new];    }        return self;}
进入ViewController.m文件,在viewWillAppear:方法底部添加如下代码:
- (void)viewWillAppear:(BOOL)animated{    ...    // 对数组进行观察    [self.child1 addObserver:self forKeyPath:@"cousins.array" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];    [self.child1.cousins insertObject:@"Antony" inArrayAtIndex:0];    [self.child1.cousins insertObject:@"Julia" inArrayAtIndex:1];    [self.child1.cousins replaceObjectInArrayAtIndex:0 withObject:@"Ben"];}
observerValueForKeyPath: ofObject: change: context:方法中处理接收到通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{    ...    else if ([keyPath isEqualToString:@"cousins.array"] && [object isKindOfClass:[Children class]])    {        NSLog(@"cousins.array %@",change);    }    else    {        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];    }}
这里除了使用keyPath进行判断还可以额外进行类判断,当父类也在观察属性时会有帮助。最后,如果没有合适的contextkeyPath,把它交给父类来处理,可能是父类也在观察同一个属性。
运行app,输出结果证明观察数组成功。
cousins.array {    indexes = "<_NSCachedIndexSet: 0x6000000394e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";    kind = 2;    new =     (        Antony    );}cousins.array {    indexes = "<_NSCachedIndexSet: 0x600000039500>[number of indexes: 1 (in 1 ranges), indexes: (1)]";    kind = 2;    new =     (        Julia    );}cousins.array {    indexes = "<_NSCachedIndexSet: 0x6000000394e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";    kind = 4;    new =     (        Ben    );    old =     (        Antony    );}
3.4 手动发送通知默认情况下,KVO观察到属性变化系统会自动发送通知,但在某些情况下,你可能需要控制何时发送通知。例如:在某些情况下不需要发送通知,或将多个改变合并为一个通知发送。手动发送通知提供了执行此操作的方法。
手动和自动通知并不互斥,已经存在自动通知的类内也可以添加手动通知。你可以通过重写由NSObject实现的automaticallyNotifiesObserversForKey:类方法来控制特定属性的通知发送,这个方法的参数key就是想要手动控制通知的属性,这个方法返回值类型是BOOL类型,想要手动控制通知的属性在重写这个类方法时返回NO,其他属性由超类来处理。
假设我们现在不想接收firstName属性的变化,进入Children.m文件,在实现部分添加下面类方法。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{    BOOL automatic = NO;    if ([key isEqualToString:@"firstName"])    {        automatic = NO;    } else {        automatic = [super automaticallyNotifiesObserversForKey:key];    }    return automatic;}
上面的方法非常简单,暂停firstName属性的自动通知;在else部分,使用超类调用相同方法,以便让iOS处理所有未在上面显式添加的属性。
现在运行app,你会发现所有firstName属性的变化都没有输出。现在只是能够停止特定键对应属性变化的通知,还不能称为手动发送通知。
想要手动发送通知,需要添加willChangeValueForKey:didChangeValueForKey:方法。步骤如下:
调用willChangeValueForKey:方法。修改所观察属性的值。调用didChangeValueForKey:方法。进入ViewController.m文件,在viewWillAppear:方法中找到[self.child1 setValue:@"Tiffany" forKey:@"firstName"];这一行代码,并用下面三行代码替换。
- (void)viewWillAppear:(BOOL)animated{    ...    // 添加观察者后改变值 验证是否可以观察到值变化    [self.child1 willChangeValueForKey:@"firstName"];    [self.child1 setValue:@"Tiffany" forKey:@"firstName"];    [self.child1 didChangeValueForKey:@"firstName"];    ...}
现在运行app,firstName属性的改变会在控制台输出,也就是我们已经成功手动发送通知。
The name of the FIRST child was changed. {    kind = 1;    new = Tiffany;    old = Jr;}
事实上,通知是在调用didChangeValueForKey:方法后发送的。如果不想在改变属性值后立即发送通知,可以在改变属性后任何想要发送通知的位置调用didChangeValueForKey:方法。例如这个demo中,你可以把[self.child1 didChangeValueForKey:@"firstName"];放在程序行代码的最底部,控制台内容输出顺序将会发生变化,这里不再详细说明。
如果单个操作导致多个键改变,则必须嵌套更改通知。如下:
  [self.child1 willChangeValueForKey:@"firstName"];  [self.child1 willChangeValueForKey:@"property"];  self.child1.firstName = @"First";       // 1 不能观察到  self.child1.firstName = @"Second";      // 2 可以观察到  self.child1.property = @"xxx";  [self.child1 didChangeValueForKey:@"firstName"];  [self.child1 didChangeValueForKey:@"property"];
可以把多个手动通知嵌套在一起,每个手动通知只能观察到键最新一次的改变。如上面代码,只有2可以观察到改变,1的改变不能观察到。
最后一定要记得移除观察者。如果视图控制器释放前没有移除观察者,释放时app会崩溃。一般添加观察者在viewDidLoad方法、viewWillAppear:中,移除观察者可以在observerValueForKeyPath: ofObject: change: context:处理完通知后,或viewWillDisappear:方法中,也可以在dealloc方法中。在这个demo中,我们在viewWillDisappear:移除观察者。
- (void)viewWillDisappear:(BOOL)animated{    [super viewWillDisappear:animated];        // 移除所有观察者    [self.child1 removeObserver:self forKeyPath:@"firstName" context:child1Context];    [self.child1 removeObserver:self forKeyPath:@"age" context:child1Context];    [self.child1 removeObserver:self forKeyPath:@"fullName" context:child1Context];    [self.child1 removeObserver:self forKeyPath:@"cousins.array" context:NULL];    [self.child2 removeObserver:self forKeyPath:@"age" context:child2Context];}
每一个addObserver: forKeyPath: options: context:必须对应一个removeObserver: forKeyPath: context:,KVO没有办法判断当前控制器是否被注册为观察者,并且移除不存在的观察者,app也会崩溃。
总结键值观察提供了一种允许对象在其他类属性变化时获得通知的机制。对于应用程序中模型层和控制器层通信特别有用。控制器对象通常用来观察模型对象的属性,并且视图对象也可以通过控制器对象观察模型对象的属性。此外,模型对象可以观察其他模型对象,也可以观察自身。
键值观察和键值编码都是一种帮助建立更强大、更灵活、更高效的应用,可能刚接触时觉得很奇特,最后你会感觉这些很容易掌握。
这篇文章只介绍了KVC、KVO的用法,如果你想要了解KVC、KVO的底层原理,请查看我的另一篇文章:KVC、KVO的本质。
文件名称:KVC&KVODemo源码地址:https://github.com/pro648/BasicDemos-iOS
参考资料:
Key-Value Observing Programming GuideUnderstanding Key-Value Observing and CodingKVO Considered Harmful欢迎更多指正:https://github.com/pro648/tips/wiki

iOS 开发面试通关指南:67 个必知问题

作者 | Artur Rymarz
译者 | 香槟超新星,责编 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下为译文:
我们都知道,面试可能会让人感到压力山大——不管你是第一次参加面试的新手,还是已经有几年开发经验做背书的业内人士,面试都会带来压力。
作为一名面试者,你必须好好准备,以展现出自己最好的一面,而想要在一场1小时左右的谈话中做到这一点是很困难的。作为开发人员,我们通常不情愿去用心学习文档,甚至很多简单的东西还需要临时去Google搜索。这没关系,没人会要求我们记住一切知识点……但如果是面试的话那就不一样了。
作为一名面试官,面试可能也不是那么简单的。你想为公司招募一个非常适合你们团队的人,但你没有太多时间去了解对方。你必须知道应该问哪些问题,以及注意自己举止的方式——你不想让面试者感到不舒服,否则你可能会因此而做出一些错误的判断,从而错过一个优秀的开发者。
差不多双方都需要为面试做准备,我希望这篇文章能够帮你在将来的面试中准备地更加充分一些。
问题
一条简短的提示:我是不会提供这些问题的完整答案的(只有一些我个人预期你至少应该了解的基础),因为我认为,如果你自己去寻找答案,应该能准备得更好。阅读有关特定主题的文章会是一个不错的选择,可以为你提供更多信息,从而助你在面试中发挥得更好。
不过,或早或晚,我很可能会在自己的博客中对所有内容进行讲解。
一般性问题
iOS(或任何其他平台)的面试不会只涉及技术问题的。尽管你会在简历上写一些有关以前的工作和项目的信息,但空间有限,无法把所有细节展开讲。为这类问题做一些准备——避免自己在面试时手足无措。
1.到目前为止,你参与过哪些项目?都用到了哪些技术?你就详细讲讲某些有趣的项目,谈谈这些项目用的是什么技术和框架就可以。
2.你是否参与过开源项目?你在其中担任什么角色?即使你只是某个开源项目的贡献者,这也会为你大大加分。如果你现在想得到第一份工作,开源项目是一种能证明你技术的好方法。如果你是某个广受欢迎repo的维护者,那就更好了。
3.你以前所在的团队有多大?你有过在任何一个项目中担任leader的经历吗?这个问题只是为了了解你过去是独自工作,在一个较大的团队中工作,远程协作,还是仅与本地的开发者合作。
4.你知识储备的来源是什么?平时会看一些博客或者播客(podcast)内容吗?举几个例子吧。如果你平时确实看,那就表明编程不仅是你的工作,还是你的一个业余爱好,所以相比其他不做这些事情的人,你就是更好的人选。
5.你是否曾有过Scrum / Agile开发经历?你是怎么看待这种开发方式的?在你看来,什么时候该用,什么时候不该用?这只是一个简短的问题,是为了了解你是否懂得Scrum自有其适用的地方,但不必一直使用。
6.你对code review有何看法?这也是一个简短的问题,用来测试你是否属于看重code review的那一类人。
7.你是否会去参加一些会议或一些当地的见面会?这些活动中你最喜欢哪个?我认为这并不是什么严肃的问题——如果你不喜欢参加,并不能代表你是一个糟糕的开发者,你可能只是内向而已。但是,如果你可以在那种会议上为公司做宣传,或者发表个演讲的话,那公司是会很喜闻乐见的。
Swift和Objective-C
iOS开发并不仅仅意味着Swift。很多项目的某些部分仍然在用Objective-C——甚至有的项目完全是用Objective-C编写的。而且,在某些(不太常见)的情况下,C和C ++技能也可能会有用。
但是,我倒不是很担心这一点,因为如果面试针对某些特定的需要这些知识的项目,那么职位描述里面很有可能会提到。另外,一些有关Bash和Ruby的基础知识可能会给你带来巨大的优势,因为有时我们可能需要写一些脚本。
下面这些问题的顺序是随机的。
8.你如何描述Swift这门语言?这是一种面向协议编程的类型安全语言。然后对方可能会追问一些后续的问题,可能会让你进一步解释“面向协议”的含义等等。
9. var和let有什么区别?你会在struct中选择哪个作为property?为什么?第一个是非常基础的问题,而第二个可能比较棘手,因为它需要视情况而定。
10.什么是lazy property?如果你写Swift代码已经有一段时间了,那至少要使用过一次lazy property,这样才能更好地了解它的优点和缺点。
11.什么是optional?其背后的机制是什么?什么是unconditional unwrapping ?这个的答案你大概知道。
12.如何unwrap一个optional值?附加问题:什么是optional chaining,optional binding以及nil-coalescing operator?举例说明你会在何种情况下选用哪种方法。这道题很简单,但目的只在于看你是否知道其中的区别,而guard并不总是首选。
13. class和struct有什么区别?举例说明分别什么情况下应该选用。你可以深入讲讲这一点,例如,它们是如何保存在内存中的(栈/堆)等等。
14.什么是closure?这个你应该知道的吧?
15.weak和unowned是什么意思?二者有什么不同?最好知道它们之间的区别以及何时使用unowned。虽然你每次都用weak可能也ok,但unowned也是自有其用途的。
16.举例说明从标准库进行收集操作的方法。map和reduce各有什么作用,以及这些方法如何在日常开发中发挥作用?
17.什么是autoclosure?这可能看起来像是一个简单的问题,但autoclosure的内容可能比你想象的要多——比如,它是如何影响性能的。
18. mutating关键字是什么意思?举一些例子说明一下。
19.escaping和unescaping分别是什么意思?这是个简单的问题。但你还记得哪个是默认选项吗?
20.(如果求职者会Objective-C):Swift和Objective-C有什么区别?你分别喜欢这两种语言的什么特性?随便说点什么都行——如果你对两种语言都有经验,你就会知道在每种语言身上你最喜欢或最想念的是什么。
21.你听说过method swizzling吗?是什么意思?在Swift中可以用吗?魔法……随便说点你知道的信息——例如为什么这样是危险的,等等。
22. NSArray和NSSet有什么区别?不要只说集合包含独特的元素,还要进一步说明它在不同情况下是如何表现的。
23.(如果求职者会Objective-C):Objective-C中的atomic/nonatomic是什么?
24.什么是KVO和KVC?这是另外一个很大的题目,所以至少要了解一些基本知识。
25.什么是应用程序和控制器生命周期?这是一个简短的问题。
26.什么是核心数据(Core Data)?这是一个很大的话题——你可能永远都用不到Core Data,但也要了解一些基本知识。
27.你在架构上有什么经验?你最喜欢哪种,为什么?你可能需要了解MVVM(这是最常见的一种),因此,请阅读此类内容以及与coordinator有关的内容。
28.你是否会将struct或class用在MVVM中的视图模型里?视图模型不一定总是一个class。
29.如何管理代码中的依赖(dependency)?你是使用依赖注入框架/工具,还是手动进行?或者你只使用单例(singleton)?
30.什么是REST?POST和GET方法有什么区别?这个是必备知识。
31.你是否编写过网络层?简要说明你以前是如何做的/将要如何做?你只需根据你自己的经验来解释即可。
32.你是否使用过Alamofire,或其他任何网络框架?有些人用网络框架,有些人不用。你平时用不用?
33.如何在Swift中解析JSON?如何在Swift中解析XML?JSON的那一题很简单。如果你知道的话,也顺便讨论一下你的密钥解码策略。XML有点被人们遗忘了,但是对于某些项目,你可能仍然需要XML。
34.什么是certificate pinning?如何在iOS应用程序中实现?这题不会也没关系,但是如果知道会很加分!
35.如何允许不安全的连接?这是可行的吗?能通过应用审查吗?知道什么说什么就行——例如,可以谈谈在什么情况下是可以做到这一点的。
36.我们应该如何处理后台操作?至少要知道一种方法。
37. GCD和NSOperation之间有什么异同?GCD可能使用起来更方便,但是知道应该何时使用NSOperation是加分项。
38.什么是串行/并行队列(serial/concurrent queue)?分别举例说明应该何时使用。
39.什么是dispatch group?知道会加分。
40.你最喜欢创建视图的方式是什么?为什么?界面生成器(Interface builder),代码约束(constraints in code),手动框架计算(manual frame calculating)——每种都各有什么优点和缺点?你倾向于用哪一种,为什么?
41.如何构建能够支持不同屏幕尺寸的视图?size class,用户界面的术语等等
42.单元中的reuseIdentifier是什么,prepareForReuse方法是做什么的?这题你必须会,因为table和collection通常都是应用程序中的核心元素。
43.什么是UIStackView?你以前用过吗?到目前为止你可能已经了解堆栈视图(stack view)了,只需说出你对这个问题的了解即可,例如如何制作动画,等等。
44.什么是internalContentSize?它真的很好用。
45.你是否曾经写过自定义控制器转换?怎样才能做到?这个不经常用到,但是如果用到了,最好记得back gesture以及如何实现。
46.frame和bounds有什么区别?这是“面试必问”的一道题。但是有些开发人员并不知道答案。
47. Constraint priorities, hugging priority, 以及compression resistance priority:它们是什么,以及它们是如何工作的?希望你不是那类一遇到约束冲突,就把所有约束的优先级都设置为999的人。
48.你在iOS中制作过的最复杂的动画是什么?你是怎么做的?如果你做过一些精美的东西,那就自信地说出来吧。
49.在不嵌套多个UIView.animate方法的前提下,如何做出复杂的动画?我想我们虽然很少有机会(有需求)去做这件事,但是也许有一天你会需要去做。
50.(如果求职者懂Objective-C):copy和retain之间有什么区别?也许知道答案的人会逐年减少,但是无论如何,如果你懂Objective-C,大概就会知道。
51.你会怎样存储敏感的用户数据?这题只是为了检查你对数据安全性是否有所了解。
52.什么是defer?它们以怎样的顺序被执行等等。
53.什么是泛化(generics)?描述一下泛化是如何改善我们的生活的,并从Swift标准库里举例说明。
面试问题可能并不仅仅与代码有关,因此也需要在有关iOS开发的一般性知识方面做好准备。
54.请说出在创建一个新项目后,需要先做哪几件事。拜托了,请不要说你要先设置CocoaPods。不要误会,把CocoaPods作为依赖管理器(dependency manager)并没有错,但首先应该做的不是这个。
55.你用依赖管理器吗?你更喜欢哪个?这里的答案并没有好坏——只是为了知道你的偏好而已。但是,最好了解每个管理器都各有什么优缺点。
56.什么是配置(debug,发布)?你是否会创建自定义的配置?我不清楚有多少项目会用自定义配置。无论如何,在某些情况下我们可能会用到。
57.你使用Xcode的工具吗?哪些?希望你至少会用Leaks。
58.你如何找到并解决内存泄漏问题?如果你有响应性编程的经验,那么你可以谈谈,并顺便说说在响应式编程中这种问题更难解决。
59.什么是持续集成(continuous integration,CI)?你用过吗?配置过吗?有很多种类型,最好知道一两个。
60.你是否有向App Store发布应用程序的经验?你是否曾经在app审查方面遇到过任何问题?只需说自己的经验之谈即可。如果你曾经用IAP发布过应用程序,就可以提一提。如果你曾经与审查人员有过什么有趣的对话,也可以说说。
61.你在单元测试方面有过经验吗?UI测试呢?每个人都说他们会写测试,但真的如此吗?如果你曾使用过任何框架来帮助测试,也提一下。
62.你是否曾经开发过测试驱动项目(TDD)?做过的同学请举手。
63.你每天都使用的开发工具有哪些?可以是一些代码格式化程序或同步器之类的——随便什么都行。如果你的工具是自己写的,请务必提及!
64.什么是SOLID?你能列出并描述这些原则吗?如果你知道它的含义,就说明你已经为面试做好了准备。但是,如果能举出一些例子,那会更好。
65.什么是声明式编程(declarative programming)?你是否尝试过SwiftUI?SwiftUI越来越受欢迎了,谁知道呢,也许几年后,它将变成写iOS应用程序的唯一方法。因此,最好至少知道它的存在。我认为现在还没有人会要求开发者拥有丰富的SwiftUI开发经验,但是如果你曾尝试过,那会是加分项。
66.什么是响应式编程(reactive programming)?你是否有使用Rx框架和/或Apple’s Combine的经验?Apple也采用这种方式,所以如果你还没有尝试过,那么现在正是去了解一些基础知识的好时机。
67.你为什么要离开原先的工作?你对新工作有什么期待吗?坦诚地说就可以。你所追求的大概率是更高的薪水,但除此之外,一定还有其他东西吧!
以上只是面试官可能会提的一些问题。
结论
最好能始终跟上iOS世界的变化,那对你来说是很有利的。阅读博客,收听播客,关注有关Apple开发人员的新闻,即使你有很大概率在新工作中并不需要知道所有那些知识,对新知识持开放态度总是好的,并且,你可以让面试官知道你一直在不断的学习。
祝你在面试中能有好运气——不管你是面试者还是面试官。
感谢阅读。
原文:https://medium.com/better-programming/67-useful-ios-developer-interview-questions-3cb15973b71
作者:Artur Rymarz,iOS高级开发工程师,Ordinary Coding创始人。
本文为 CSDN 翻译,转载请注明来源出处。

发表评论