iOS 底层探索系列html
Key Value Coding
也即 KVC
是 iOS
开发中一个很重要的概念,中文翻译过来是 键值编码
,关于这个概念的具体定义能够在 Apple
的官方文档处找到。前端
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties.
【译】KVC
是经过NSKeyValueCoding
这个非正式协议启用的一种机制,而遵循了这个协议的对象就提供了对其属性的间接访问。
咱们一般使用访问器方法来访问对象的属性,即便用 getter
来获取属性值,使用 setter
来设置属性值。而在 Objective-C
中,咱们还能够直接经过实例变量的方式来获取属性值和设置属性值。以下面的代码所示:git
// JHPerson.h @interface JHPerson : NSObject { @public NSString *myName; } @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @end // ViewController.m - (void)viewDidLoad { [super viewDidLoad]; JHPerson *person = [[JHPerson alloc] init]; person.name = @"leejunhui"; person.age = 20; person->myName = @"leejunhui"; NSLog(@"%@ - %ld - %@",person.name, person.age,person->myName); }
这种方式咱们再熟悉不过了,关于属性会由编译器自动生成 getter
和 setter
以及对应的实例变量前面咱们已经探索过了,咱们能够在 ro
中来找到它们的踪迹,感兴趣的读者能够翻阅前面的文章。github
这里再明确下实例变量、成员变量、属性之间的区别:
在 @interface 括号里面声明的变量统称为 成员变量
而成员变量实际上由两部分组成: 实例变量 + 基本数据类型变量
而 属性 = 成员变量 + getter方法 + setter方法
那其实这里分两种状况,本身实现和编译器帮咱们实现。objective-c
getter
和 setter
这里咱们以 JHPerson
类的 name
属性为例,咱们分别重写 name
的 getter
和 setter
方法,这里还有个注意点,咱们须要在 @interface
中声明一下实例变量 _name
,具体代码以下所示:设计模式
// JHPerson.h @interface JHPerson : NSObject { @public NSString *myName; NSString *_name; } @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @end // JHPerson.m @implementation JHPerson - (NSString *)name { return _name; } - (void)setName:(NSString *)name { _name = name; } @end
接着,咱们在 main.m
中使用点语法对 name
进行赋值,而后打印 name
的值:api
#import <Foundation/Foundation.h> #import "JHPerson.h" int main(int argc, const char * argv[]) { @autoreleasepool { JHPerson *person = [[JHPerson alloc] init]; person.name = @"leejunhui"; NSLog(@"person 姓名为:%@", person.name); } return 0; }
打印结果以下:数组
-[JHPerson setName:] - leejunhui -[JHPerson name] - leejunhui person 姓名为:leejunhui
显然,这里的结果就代表了 person.name = @"leejunhui";
实际上是调用了 JHPerson
类的 setName
方法,而 NSLog(@"person 姓名为:%@", person.name);
则是调用了 name
方法。安全
这块的逻辑我相信读者应该都比较熟悉了,接下来咱们再分析编译器自动生成 getter
和 setter
的场景。bash
getter
和 setter
咱们探索前先思考一个问题,按照咱们如今的认知,若是咱们不去重写属性的 getter
和 setter
方法以及声明对应的实例变量,那么编译器就会帮咱们作这件事,那么是否是说有多少个属性,就会生成多少个对应的 getter
和 setter
呢?显然,编译器不会这么傻,这样作不管是从性能上仍是设计上都十分笨拙,咱们在 libObjc
源码中能够找到这么一个源文件:objc-accessors.mm
,这个文件中有许多从字面意思上看起来像是设置属性的方法,以下图所示:
咱们聚焦这个方法: objc_setProperty_nonatomic_copy
,为何呢?由于 name
属性声明为 @property (nonatomic, copy) NSString *name;
,两者都包含 nonatomic
和 copy
关键字,咱们不妨在 objc_setProperty_nonatomic_copy
方法处打上断点,注意,此时咱们须要注释掉咱们刚才本身添加的 getter
和 setter
方法。
Bingo~,objc_setProperty_nonatomic_copy
方法果真被调用了,而且咱们赋的值也是对的,咱们来到这个方法内部实现:
void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty(self, _cmd, newValue, offset, false, true, false); }
能够看到这里又包裹了一层,真正的实现为 reallySetProperty
:
这个方法不是很复杂,咱们简单过一下这个方法的参数。
1.首先是这个方法的
offset
参数,前面咱们已经探索过关于内存偏移的内容,这里再也不赘述。咱们知道,对象的isa
指针占8
个字节,还寄的咱们的JHPerson
类的声明中有一个实例变量myName
吗,这是一个字符串类型的实例变量,也占用8
个字节,因此这里的offset
为16
,意思就是偏移16
个字节来设置属性name
。2.而后是
atomic
参数,这个参数取决于属性声明时是atomic
仍是nonatomic
,这个关键字表示是操做的原子性,而网上不少资料都说atomic
是来保证对象的多线程安全,其实否则,它只是能保证你访问的时候给你返回一个无缺无损的Value
而已,Realm官方对此相关的解释,举个例子:若是线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,有3种可能:多是 B、C set 以前原始的值,也多是 B set 的值,也多是 C set 的值。同时,最终这个属性的值,多是 B set 的值,也有多是 C set 的值。因此atomic
并不能保证对象的线程安全。也就是说atomic
所说的线程安全只是保证了getter
和setter
存取方法的线程安全,并不能保证整个对象是线程安全的。
nonatomic
关键字就没有这个保证了,nonatomic
返回你的对象可能就不是完整的value
。所以,在多线程的环境下原子操做是很是必要的,不然有可能会引发错误的结果。但仅仅使用atomic
并不会使得对象线程安全,咱们还要为对象线程添加lock
来确保线程的安全。
nonatomic
对象setter
和getter
方法的实现:- (void)setCurrentImage:(UIImage *)currentImage { if (_currentImage != currentImage) { [_currentImage release]; _currentImage = [currentImage retain]; } } - (UIImage *)currentImage { return _currentImage; }
atomic
对象setter
和getter
方法的实现:- (void)setCurrentImage:(UIImage *)currentImage { @synchronized(self) { if (_currentImage != currentImage) { [_currentImage release]; _currentImage = [currentImage retain]; } } } - (UIImage *)currentImage { @synchronized(self) {return _currentImage;} }3.最后是
copy
和mutableCopy
参数,说到copy
关键字不妨来复习下iOS
中的属性标识符以及相应的变量标识符。
在 ARC
中与内存管理有关的变量标识符,有下面几种:
__strong
__weak
__unsafe_unretained
__autoreleasing
变量标识符 | 做用 |
---|---|
__strong |
默认使用的标识符。只有还有一个强指针指向某个对象,这个对象就会一直存活 |
__weak |
声明这个引用不会保持被引用对象的存活,若是对象没有强引用了,弱引用会被置为 nil |
__unsafe_unretained |
声明这个引用不会保持被引用对象的存活,若是对象没有强引用了,它不会被置为 nil。若是它引用的对象被回收掉了,该指针就变成了野指针 |
__autoreleasing |
用于标示使用引用传值的参数(id *),在函数返回时会被自动释放掉 |
变量标识符的用法以下:
Number* __strong num = [[Number alloc] init];
注意 __strong
的位置应该放到 *
和变量名中间,放到其余的位置严格意义上说是不正确的,只不过编译器不会报错。
属性标识符
@property (atomic/nonatomic/assign/retain/strong/weak/unsafe_unretained/copy) Number* num
属性标识符 | 做用 |
---|---|
atomic |
代表该属性的读写操做是原子性的,但不保证对象的多线程安全 |
nonatomic |
代表该属性的读写操做是非原子性的,性能强于atomic ,由于没有锁的开销 |
assign |
代表 setter 仅仅是一个简单的赋值操做,一般用于基本的数值类型,例如 CGFloat 和 NSInteger |
strong |
代表属性定义一个拥有者关系。当给属性设定一个新值的时候,首先这个值进行 retain ,旧值进行 release ,而后进行赋值操做 |
weak |
代表属性定义了一个非拥有者关系。当给属性设定一个新值的时候,这个值不会进行 retain ,旧值也不会进行 release , 而是进行相似 assign 的操做。不过当属性指向的对象被销毁时,该属性会被置为nil。 |
unsafe_unretained |
语义和 assign 相似,不过是用于对象类型的,表示一个非拥有(unretained )的,同时也不会在对象被销毁时置为 nil 的(unsafe )关系。 |
copy |
相似于 strong ,不过在赋值时进行 copy 操做而不是 retain 操做。一般在须要保留某个不可变对象( NSString 最多见),而且防止它被意外改变时使用。 |
错误使用属性标识符的后果
若是咱们给一个原始类型设置strong\weak\copy
,编译器会直接报错:Property with 'retain (or strong)' attribute must be of object type设置为
unsafe_unretained
却是能够经过编译,只是用起来跟assign
也没有什么区别。
反过来,咱们给一个NSObject
属性设置为 assign,编译器会报警:Assigning retained object to unsafe property; object will be released after assignment正如警告所说的,对象在赋值以后被当即释放,对应的属性也就成了野指针,运行时跑到属性有关操做会直接崩溃掉。和设置成
unsafe_unretained
是同样的效果(设置成weak
不会崩溃)。
unsafe_unretained
的用处unsafe_unretained
差很少是实际使用最少的一个标识符了,在使用中它的用处主要有下面几点:
1.兼容性考虑。iOS4
以及以前尚未引入weak
,这种状况想表达弱引用的语义只能使用unsafe_unretained
。这种状况如今已经不多见了。
2.性能考虑。使用weak
对性能有一些影响,所以对性能要求高的地方能够考虑使用unsafe_unretained
替换weak
。一个例子是 YYModel 的实现,为了追求更高的性能,其中大量使用unsafe_unretained
做为变量标识符。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } if (!atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); }
咱们把目光转移到 reallySetProperty
中来,这里先判断的 offset
是否为 0
。
0
,直接调用方法 object_setClass
设置当前对象的 class
,显然就是设置对象的 isa
指针。oldValue
。self
先强转为字符串指针,而后进行内存平移获得要设置的属性的内存偏移值,而后将其强转为 id*
类型。判断要设置的属性的标识符是否须要进行 copy
操做
newValue
也就是要设置的属性值发送 copyWithZone
消息,这一步的目的是拿到 newValue
的副本,而后覆写 newValue
,使得传入的 newValue
以后再发生了改变都不会影响到属性值。判断要设置的属性的标识符是否须要进行 mutableCopy
操做
newValue
也就是要设置的属性值发送 mutableCopyWithZone
消息若是要设置的属性既不执行 copy
也不执行 mutableCopy
,那么就先判断要设置的值是否相等
objc_retain
消息进行 retain
操做,而后将返回值覆写到 newValue
上接着判断属性赋值操做是不是原子操做
oldValue
,而后将新值赋上去atomic
是保证属性的读写操做线程安全oldValue
也就是旧值进行内存的释放PS: 并非全部属性的自动setter
都会来到objc_setProperty
![]()
那么,具体是哪些状况下的属性才会来到这里呢?咱们不妨作一下简单的测试
// JHTest.h @interface JHTest @property (nonatomic, strong) NSMutableArray *arrayNonatomicAndStrong; @property (nonatomic, copy) NSMutableArray *arrayNonatomicAndCopy; @property (nonatomic, strong) NSString *stringNonatomicAndStrong; @property (nonatomic, copy) NSString *stringNonatomicAndCopy; @property (nonatomic, assign) int ageNonatomicAndAssign; @property (nonatomic, weak) NSString *stringNonatomicAndWeak; @property (nonatomic, retain) NSString *stringNonatomicAndRetain; @property (atomic, strong) NSMutableArray *arrayAtomicAndStrong; @property (atomic, copy) NSMutableArray *arrayAtomicAndCopy; @property (atomic, strong) NSString *stringAtomicAndStrong; @property (atomic, copy) NSString *stringAtomicAndCopy; @property (atomic, assign) int ageAtomicAndAssign; @property (atomic, weak) NSString *stringAtomicAndWeak; @property (atomic, retain) NSString *stringAtomicAndRetain; @end // main.m JHTest *test = [[JHTest alloc] init]; NSMutableArray *testMutableArray = @[].mutableCopy; test.arrayNonatomicAndStrong = testMutableArray; test.arrayNonatomicAndCopy = testMutableArray; test.stringNonatomicAndStrong = @"呵呵哒"; test.stringNonatomicAndCopy = @"呵呵哒"; test.ageNonatomicAndAssign = 18; test.stringNonatomicAndWeak = @"呵呵哒"; test.stringNonatomicAndRetain = @"呵呵哒"; test.arrayAtomicAndStrong = testMutableArray; test.arrayAtomicAndCopy = testMutableArray; test.stringAtomicAndStrong = @"呵呵哒"; test.stringAtomicAndCopy = @"呵呵哒"; test.ageAtomicAndAssign = 18; test.stringAtomicAndWeak = @"呵呵哒"; test.stringAtomicAndRetain = @"呵呵哒";
咱们经过断点调试,每执行到一个属性的时候,看断点是否会来到 reallySetProperty
,测试结果以下:
属性 | 是否进入reallySetProperty |
---|---|
arrayNonatomicAndStrong | 否 |
arrayNonatomicAndCopy | 是 |
stringNonatomicAndStrong | 否 |
stringNonatomicAndCopy | 是 |
ageNonatomicAndAssign | 否 |
stringNonatomicAndWeak | 否 |
stringNonatomicAndRetain | 否 |
属性 | 是否进入reallySetProperty |
---|---|
arrayAtomicAndStrong | 是 |
arrayAtomicAndCopy | 是 |
stringAtomicAndStrong | 是 |
stringAtomicAndCopy | 是 |
ageAtomicAndAssign | 否 |
stringAtomicAndWeak | 否 |
stringAtomicAndRetain | 是 |
从这两组测试结果不难看出,由于 reallySetProperty
内部实际上进行了原子性的写操做以及 copy
或 mutableCopy
的操做和 retain
操做,而对于属性标识符为 nonatomic
而且非 copy
的属性来讲,其实并不须要进行原子操做以及 copy
或 mutableCopy
操做。
咱们前面所展现的属性标识符对应做用的内容在这里也印证了只有当属性须要进行 copy
或 mutableCopy
操做或原子操做时或 retain
操做才会被编译器优化来到 objc_setProperty_xxx => reallySetProperty
的流程。换句话说,在 Clang
编译的时候,编译器确定会对属性进行判断,对有须要的属性才触发这一流程。
咱们用一个表格来总结:
底层方法 | 对应属性标识符 |
---|---|
objc_setProperty_nonatomic_copy | nonatomic + copy |
objc_setProperty_atomic_copy | atomic + copy |
objc_setProperty_atomic | atomic + retain/strong |
咱们分析完 reallySetProperty
后不由有一个疑问,那就是系统是在哪一步调用了 objc_setProperty_xxx
之类的方法呢?答案就是 LLVM
。咱们能够在 LLVM
的源码中进行搜索关键字 objc_setProperty
:
咱们能够看到在 clang
编译器前端的 RewriteModernObjC
命名空间下的 RewritePropertyImplDecl
方法中:
而后咱们在 CodeGen
目录下的匿名命名空间下的 ObjcCommonTypesHelper
的 getOptimizedSetPropertyFn
处能够看到如下代码:
咱们接着以 getOptimizedSetPropertyFn
为关键字来搜索:
llvm::FunctionCallee GetOptimizedPropertySetFunction(bool atomic, bool copy) override { return ObjCTypes.getOptimizedSetPropertyFn(atomic, copy); }
而后咱们搜索 GetOptimizedPropertySetFunction
:
关于 LLVM
这块咱们先探索到这里,接下来让咱们回顾一下 KVC
经常使用的几种使用场景。
valueForKey:
和 setValue:ForKey:
来间接的获取和设置属性值JHPerson *person = [[JHPerson alloc] init]; [person setValue:@"leejunhui" forKey:@"name"]; NSLog(@"person 的姓名为: %@", [person valueForKey:@"name"]); // 打印以下 person 的姓名为: leejunhui
valueForKey
: - Returns the value of a property named by the key parameter. If the property named by the key cannot be found according to the rules described in Accessor Search Patterns, then the object sends itself a valueForUndefinedKey: message. The default implementation of valueForUndefinedKey: raises an NSUndefinedKeyException, but subclasses may override this behavior and handle the situation more gracefully.【译】
valueForKey
: 返回由key
参数命名的属性的值。若是根据访问者搜索模式中描述的规则找不到由key
命名的属性,则该对象将向自身发送valueForUndefinedKey:
消息。valueForUndefinedKey:
的默认实现会抛出NSUndefinedKeyException
异常,可是子类能够重写此行为并更优雅地处理这种状况。
setValue:forKey:
: Sets the value of the specified key relative to the object receiving the message to the given value. The default implementation of setValue:forKey: automatically unwraps NSNumber and NSValue objects that represent scalars and structs and assigns them to the property. See Representing Non-Object Values for details on the wrapping and unwrapping semantics.If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a setValue:forUndefinedKey: message. The default implementation of setValue:forUndefinedKey: raises an NSUndefinedKeyException. However, subclasses may override this method to handle the request in a custom manner.
【译】
setValue:forKey:
: 将该消息接收者的指定key
的值设置为给定值。默认实现会自动把表示标量和结构体的NSNumber
和NSValue
对象解包而后赋值给属性。若是指定key
所对应的属性没有对应的setter
实现,则该对象将向自身发送setValue:forUndefinedKey:
消息,而该消息的默认实现会抛出一个NSUndefinedKeyException
的异常。可是子类能够重写此方法以自定义方式处理请求。
2.valueForKeyPath:
和 setValue:ForKeyPath:
Storyboard 或 xib 中使用 KVC
如上图所示,Storyboard
中的一个视图的属性菜单能够设置该视图的 Key Path
,这就引出了基于路由的另一种 KVC
方式,那就是 valueForKeyPath:
和 setValue:ForKeyPath:
A key path is a string of dot-separated keys used to specify a sequence of object properties to traverse. The property of the first key in the sequence is relative to the receiver, and each subsequent key is evaluated relative to the value of the previous property. Key paths are useful for drilling down into a hierarchy of objects with a single method call.【译】
keypath
是一个以点分隔开来的字符串,表示了要遍历的对象属性序列。序列中第一个key
相对于接受者,然后续的每一个key
都与前一级key
相关联。keypath
对于单个方法调用来深刻对象内部结构来讲颇有用。
经过 layer.cornerRadius
这个 Key Path
,实现了对左侧 View
的 layer
属性的 cornerRadius
属性的访问。
valueForKeyPath:
- Returns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation of valueForKey: cannot find an accessor method—receives a valueForUndefinedKey: message.【译】
valueForKeyPath:
: 返回相对于接受者的指定key path
上的值。key path
路径序列中不符合特定键的键值编码的任何对象(即valueForKey:
的默认实现没法找到访问器方法的对象)都会接收到valueForUndefinedKey:
消息。
setValue:forKeyPath:
- Sets the given value at the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key receives a setValue:forUndefinedKey: message.【译】
setValue:forKeyPath:
: 将该消息接收者的指定key path
的值设置为给定值。key path
路径序列中不符合特定键的键值编码的任何对象都将收到setValue:forUndefinedKey:
消息
// JHPerson.h @property (nonatomic, strong) JHAccount *account; // JHAccount.h @property (nonatomic, copy) NSString *balance; // main.m person.account = [[JHAccount alloc] init]; [person setValue:@"666" forKeyPath:@"account.balance"]; NSLog(@"person 的帐户余额为: %@", [person valueForKeyPath:@"account.balance"]); // 打印输出 person 的帐户余额为: 666
3.dictionaryWithValuesForKeys:
和 setValuesForKeysWithDictionary:
dictionaryWithValuesForKeys:
- Returns the values for an array of keys relative to the receiver. The method calls valueForKey: for each key in the array. The returned NSDictionary contains values for all the keys in the array.【译】返回相对于接收者的
key
数组的值。该方法会为数组中的每一个key
调用valueForKey:
。 返回的NSDictionary
包含数组中全部键的值。
setValuesForKeysWithDictionary:
- Sets the properties of the receiver with the values in the specified dictionary, using the dictionary keys to identify the properties. The default implementation invokes setValue:forKey: for each key-value pair, substituting nil for NSNull objects as required.【译】使用字典键标识属性,而后使用字典中的对应值来设置该消息接收者的属性值。默认实现会对每个键值对调用
setValue:forKey:
。设置时须要将nil
替换成NSNull
。
[person setValuesForKeysWithDictionary:@{@"name": @"junhui", @"age": @(18)}]; NSLog(@"%@", [person dictionaryWithValuesForKeys:@[@"name", @"age"]]); // 打印输出 { age = 18; name = junhui; }
Collection objects, such as NSArray, NSSet, and NSDictionary, can’t contain nil as a value. Instead, you represent nil values using the NSNull object. NSNull provides a single instance that represents the nil value for object properties. The default implementations of dictionaryWithValuesForKeys: and the related setValuesForKeysWithDictionary: translate between NSNull (in the dictionary parameter) and nil (in the stored property) automatically.
集合对象(例如NSArray
,NSSet
和NSDictionary
)不能包含nil
做为值。 而是使用NSNull
对象表示nil
值。NSNull
提供了单个实例表示对象属性的nil值。dictionaryWithValuesForKeys:
和setValuesForKeysWithDictionary:
的默认实现会自动在NSNull
(在dictionary
参数中)和nil
(在存储的属性中)之间转换。
![]()
咱们先看下面这样的一份代码,首先给 JHPerson
类增长一个属性 array
,类型为不可变数组,而后修改这个属性:
// JHPerson.h @property (nonatomic, strong) NSArray *array; // main.m person.array = @[@"1", @"2", @"3"]; NSArray *tempArray = @[@"0", @"1", @"2"]; [person setValue:tempArray forKey:@"array"]; NSLog(@"%@", [person valueForKeyPath:@"array"]); // 打印输出 ( 0, 1, 2 )
虽然这种方式能达到效果,但其实还有一种更好的方式:
// main.m NSMutableArray *mutableArray = [person mutableArrayValueForKey:@"array"]; mutableArray[0] = @"-1"; NSLog(@"%@", [person valueForKeyPath:@"array"]); // 打印输出 ( "-1", 1, 2 )
这里咱们用到了一个叫作 mutableArrayValueForKey:
的实例方法,这个方法会经过传入的 key
返回对应属性的一个可变数组的代理对象。
其实对集合对象来讲,咱们使用上一节的各类读取和设置方法均可以,可是对于操做集合对象内部的元素来讲,更高效的方式是使用 KVC
提供的可变代理方法。KVC
为咱们提供了三种不一样的可变代理方法:
mutableArrayValueForKey:
和 mutableArrayValueForKeyPath:
NSMutableArray
对象mutableSetValueForKey:
和 mutableSetValueForKeyPath:
NSMutableSet
对象mutableOrderedSetValueForKey:
and mutableOrderedSetValueForKeyPath:
NSMutableOrderedSet
对象在使用 valueForKeyPath:
的时候,可使用集合运算符来实现一些高效的运算操做。
A collection operator is one of a small list of keywords preceded by an at sign (@) that specifies an operation that the getter should perform to manipulate the data in some way before returning it.
【译】一个集合运算符是一小部分关键字其后带有一个at符号(@),该符号指定getter
在返回数据以前以某种方式处理数据应执行的操做。
集合运算符的结构以下图所示:
简单解释一下:
valueForKeyPath:
消息,left key path
能够省略@count
运算符外,全部的集合运算符的 right key path
都不能省略而集合运算符能够分为三大类:
聚合操做符
@avg
: 返回操做对象指定属性的平均值 @count
: 返回操做对象指定属性的个数 @max
: 返回操做对象指定属性的最大值 @min
: 返回操做对象指定属性的最小值 @sum
: 返回操做对象指定属性值之和 数组操做符
@distinctUnionOfObjects
: 返回操做对象指定属性的集合--去重 @unionOfObjects
: 返回操做对象指定属性的集合 嵌套操做符
@distinctUnionOfArrays
: 返回操做对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray
@unionOfArrays
: 返回操做对象(集合)指定属性的集合 @distinctUnionOfSets
: 返回操做对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet
非对象属性分为两类,一类是基本数据类型也就是所谓的标量(scalar),一类是结构体(struct)。
如图所示,经常使用的基本数据类型须要在设置属性的时候包装成 NSNumber
类型,而后在读取值的时候使用各自对应的读取方法,如 double
类型的标量读取的时候使用 doubleValue
结构体的话就须要转换成 NSValue
类型,如上图所示。
除了 NSPoint
, NSRange
, NSRect
, 和 NSSize
,对于自定义的结构体,也须要进行 NSValue
的转换操做,举个🌰:
typedef struct { float x, y, z; } ThreeFloats; @interface MyClass @property (nonatomic) ThreeFloats threeFloats; @end // 获取结构体属性 NSValue* result = [myClass valueForKey:@"threeFloats"]; // 设置结构体属性 ThreeFloats floats = {1., 2., 3.}; NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)]; [myClass setValue:value forKey:@"threeFloats"]; // 提取结构体属性 ThreeFloats th; [reslut getValue:&th];
KVC
支持属性验证,而这一特性是经过validateValue:forKey:error:
(或 validateValue:forKeyPath:error:
) 方法来实现的。这个验证方法的默认实现是去收到这个验证消息的对象(或keyPath
中最后的对象)中根据 key
查找是否有对应的 validate<Key>:error:
方法实现,若是没有,验证默认成功,返回 YES
。
而因为 validate<Key>:error:
方法经过引用接收值和错误参数,因此会有如下三种结果:
YES
,对属性值不作任何改动。NO
,但对属性值不作改动,若是调用者提供了 NSError
的话,就把错误引用设置为指示错误缘由的NSError对象。YES
,建立一个新的,有效的属性值做为替代。在返回以前,该方法将值引用修改成指向新值对象。 进行修改时,即便值对象是可变的,该方法也老是建立一个新对象,而不是修改旧对象。Person* person = [[Person alloc] init]; NSError* error; NSString* name = @"John"; if (![person validateValue:&name forKey:@"name" error:&error]) { NSLog(@"%@",error); }
那么是否系统会自动进行属性验证呢?
一般,KVC
或其默认实现均未定义任何机制来自动的执行属性验证,也就是说须要在适合你的应用的时候本身提供属性验证方法。
某些其余 Cocoa
技术在某些状况下会自动执行验证。 例如,保存 managed object context
时,Core Data
会自动执行验证。另外,在 macOS
中,Cocoa Binding
容许你指定验证应自动进行。
KVC
取值和设值原理getter
valueForKey:
方法会在调用者传入 key
以后会在对象中按下列的步骤进行模式搜索:
1.以 get<Key>
, <key>
, is<Key>
以及 _<key>
的顺序查找对象中是否有对应的方法。
2.查找是否有 countOf<Key>
和 objectIn<Key>AtIndex:
方法(对应于 NSArray
类定义的原始方法)以及 <key>AtIndexes:
方法(对应于 NSArray
方法 objectsAtIndexes:
)
countOf<Key>
),再找到其余两个中的至少一个,则建立一个响应全部 NSArray
方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key>
+ objectIn<Key>AtIndex:
,要么是 countOf<Key>
+ <key>AtIndexes:
,要么是 countOf<Key>
+ objectIn<Key>AtIndex:
+ <key>AtIndexes:
)3.查找名为 countOf<Key>
,enumeratorOf<Key>
和 memberOf<Key>
这三个方法(对应于NSSet类定义的原始方法)
NSSet
方法的代理集合对象,并返回该对象4.判断类方法 accessInstanceVariablesDirectly
结果
YES
,则以 _<key>
, _is<Key>
, <key>
, is<Key>
的顺序查找成员变量,若是找到了,将成员变量带上跳转到第 5 步,若是没有找到则跳转到第 6 步NO
,跳转到第 6 步5.判断取出的属性值
NSNumber
类型,则将属性值转化为 NSNumber
类型返回NSNumber
类型,则将属性值转化为 NSValue
类型返回valueForUndefinedKey:
。 默认状况下,这会引起一个异常,可是 NSObject
的子类能够提供特定于 key
的行为。这里能够用简单的流程图来表示
setter
setValue:forKey:
方法默认实现会在调用者传入 key
和 value
(若是是非对象类型,则指的是解包以后的值) 以后会在对象中按下列的步骤进行模式搜索:
set<Key>:
, _set<Key>
的顺序在对象中查找是否有这样的方法,若是找到了,则把属性值传给方法来完成属性值的设置。2.判断类方法 accessInstanceVariablesDirectly
结果
YES
,则以 _<key>
, _is<Key>
, <key>
, is<Key>
的顺序查找成员变量,若是找到了,则把属性值传给方法来完成属性值的设置。NO
,跳转到第 3 步setValue:forUndefinedKey:
。 默认状况下,这会引起一个异常,可是NSObject
的子类能够提供特定于 key
的行为。KVC
了解了 KVC
底层原理以后,咱们是否能够本身来实现一下 KVC
呢?这里咱们要先明确一下 iOS
中对于属性的分类:
NSNumber
和其它一些不可变类型好比 NSColor
也能够被认为是简单属性Account
对象可能具备一个 owner
属性,该属性是 Person
对象的实例,而 Person
对象自己具备 address
属性。owner
的地址能够更改,但却而无需更改 Account
持有的 owner
属性。也就是说 Account
的 owner
属性未被更改,只是 address
被更改了。NSArray
或 NSSet
的实例来持有此集合。咱们经过代码来演示上述三种类型的属性:
// Person.h @interface Person @property (nonatomic, copy) NSString *name; // Attributes @property (nonatomic, strong) Account *account; // To-one relationships @property (nonatomic, strong) NSArray *subjects; // To-many relationships @end // Account.h @interface Account @property (nonatomic, assign) NSInteger balance; @end
咱们实现聚焦于最经常使用的 valueForKey:
方法的声明,咱们发现该方法是位于 NSKeyValueCoding
这个分类里面的,这种设计模式能够实现解耦的功能。
打个比方,咱们在实际开发中会在 AppDelegate
源文件里面去作各类诸如第三方组件的注册和初始化,时间久了,随着项目功能不断迭代,堆积在 AppDelegate
中的代码就会愈来愈多,致使难以维护。这个时候若是采起把这些初始化和注册逻辑放在不一样的 AppDelegate
的分类中就能够大大减轻 AppDelegate
自身维护的成本,同时,也让整个业务流更加清晰。
那么,咱们若是要自定义 KVC
实现的话,也应该按照这种设计模式来操做。咱们直接新建一个 NSObject
的分类,而后咱们先着眼于 setValue:ForKey:
方法,为了不与系统自带的 KVC
方法冲突,咱们加一个前缀
// NSObject+JHKVC.h @interface NSObject (JHKVC) - (void)jh_setValue:(nullable id)value forKey:(NSString *)key; @end
而后要实现这个方法,根据咱们前面探索的 setValue:ForKey:
流程,咱们判断一下传入的 key
是否为空:
// 1.判断 key if (key == nil || key.length == 0) return;
key
为 nil
或者 key
长度为 0 ,直接退出。接着咱们要判断是否存在 setKey
,_setKey
,这里有个小插曲,由于苹果官方文档上只说了这两种方法,但其实,iOS
底层还处理了 setIsKey
,这是由于 key
能够被重写成 isKey
的形式,因此这里咱们就再加上对 setIsKey
的判断。
// 2.判断 setKey,_setKey,setIsKey 是否存在,若是存在,直接调用相应的方法来设置属性值 NSString *Key = key.capitalizedString; NSString *setKey = [NSString stringWithFormat:@"set%@:",Key]; NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key]; NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key]; if ([self jh_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*********%@**********",setKey); return; }else if ([self jh_performSelectorWithMethodName:_setKey value:value]) { NSLog(@"*********%@**********",_setKey); return; }else if ([self jh_performSelectorWithMethodName:setIsKey value:value]) { NSLog(@"*********%@**********",setIsKey); return; }
key
进行一下首字母大写化,而后拼接三个不一样的 set
方法名,而后判断响应的方法可否实现,若是实现了就直接调用响应的方法来设置属性值这里先经过
respondsToSelector
来判断当前对象是否能响应传入的方法,若是能响应,则执行方法- (BOOL)jh_performSelectorWithMethodName:(NSString *)methodName value:(id)value{ if ([self respondsToSelector:NSSelectorFromString(methodName)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self performSelector:NSSelectorFromString(methodName) withObject:value]; #pragma clang diagnostic pop return YES; } return NO; }
这里若是按照系统的 KVC
设值流程,应该还有对 NSArray
,NSSet
之类的处理,为了简化,就暂时忽略掉这些流程。咱们直接往下面走,下一个流程应该就是判断类方法 accessInstanceVariablesDirectly
了:
// 3.判断是否能直接读取成员变量 if (![self.class accessInstanceVariablesDirectly] ) { @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; }
若是能够读取成员变量,那么就须要咱们按照 _key
,_isKey
, key
, isKey
的顺序去查找了:
// 4.按照 _key,is_key,key,isKey 顺序查询实例变量 NSMutableArray *mArray = [self getIvarListName]; NSString *_key = [NSString stringWithFormat:@"_%@",key]; NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key]; NSString *isKey = [NSString stringWithFormat:@"is%@",Key]; if ([mArray containsObject:_key]) { // 4.2 获取相应的 ivar Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String); // 4.3 对相应的 ivar 设置值 object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:_isKey]) { Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String); object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:key]) { Ivar ivar = class_getInstanceVariable([self class], key.UTF8String); object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:isKey]) { Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String); object_setIvar(self , ivar, value); return; }
- (NSMutableArray *)getIvarListName{ // 初始化数组容器 NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1]; unsigned int count = 0; // 获取到当前类的成员变量 Ivar *ivars = class_copyIvarList([self class], &count); // 遍历全部的成员变量 for (int i = 0; i<count; i++) { Ivar ivar = ivars[i]; const char *ivarNameChar = ivar_getName(ivar); // 将静态字符串指针转换为 NSString 类型 NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar]; NSLog(@"ivarName == %@",ivarName); [mArray addObject:ivarName]; } // 释放掉成员变量指针数组 free(ivars); return mArray; }
这里用到了 Runtime
的两个 api
,class_copyIvarList
和 ivar_getName
Ivar _Nonnull * class_copyIvarList(Class cls, unsigned int *outCount);返回类结构中成员变量的指针数组,可是不包括父类中声明的成员变量。该数组包含
*outCount
指针,后跟一个NULL
终止符。使用完毕后您必须使用free()
释放成员变量的指针数组。若是该类未声明任何实例变量,或者cls
为Nil,则返回NULL
,而且*outCount
为 0。const char * ivar_getName(Ivar v);返回成员变量的名称
// 5.若是前面的流程都失败了,则抛出异常 @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: setValue:forUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key] userInfo:nil];
setValue:forUndefinedKey
的异常至此,咱们的 setValue:forKey:
流程就结束了,固然,整个内容和系统真正的 KVC
比起来还差得很远,包括线程安全、可变数组之类的都没涉及,不过这不是重点,咱们只须要触类旁通便可。
接着咱们须要自定义的是 valueForKey:
,咱们声明以下的方法:
- (nullable id)jh_valueForKey:(NSString *)key;
而后一样的,根据咱们前面探索的 valueForKey:
底层流程,仍是要先判断 key
:
// 1.判断 key if (key == nil || key.length == 0) { return nil; }
key
为 nil
或者 key
长度为 0 ,直接退出。而后就是判断是否有相应的 getter
方法,查找顺序是按照 getKey
, key
, isKey
, _key
:
// 2.判断 getKey,key,isKey,_key 是否存在,若是存在,直接调用相应的方法来返回属性值 NSString *Key = key.capitalizedString; NSString *getKey = [NSString stringWithFormat:@"get%@:",Key]; NSString *isKey = [NSString stringWithFormat:@"is%@:",Key]; NSString *_key = [NSString stringWithFormat:@"_%@:",Key]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([self respondsToSelector:NSSelectorFromString(getKey)]) { return [self performSelector:NSSelectorFromString(getKey)]; } else if ([self respondsToSelector:NSSelectorFromString(key)]){ return [self performSelector:NSSelectorFromString(key)]; } else if ([self respondsToSelector:NSSelectorFromString(isKey)]){ return [self performSelector:NSSelectorFromString(isKey)]; } else if ([self respondsToSelector:NSSelectorFromString(_key)]){ return [self performSelector:NSSelectorFromString(_key)]; } #pragma clang diagnostic pop
若是这四种 getter
方法都没有找到,那么一样的就须要读取类方法:
// 3.判断是否能直接读取成员变量 if (![self.class accessInstanceVariablesDirectly] ) { @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; }
若是能够读取成员变量,那么就须要咱们按照 _key
,_isKey
, key
, isKey
的顺序去查找了:
// 4.按照 _key,_iskey,key,isKey 顺序查询实例变量 NSMutableArray *mArray = [self getIvarListName]; _key = [NSString stringWithFormat:@"_%@",key]; NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key]; isKey = [NSString stringWithFormat:@"is%@",Key]; if ([mArray containsObject:_key]) { Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String); return object_getIvar(self, ivar);; }else if ([mArray containsObject:_isKey]) { Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String); return object_getIvar(self, ivar);; }else if ([mArray containsObject:key]) { Ivar ivar = class_getInstanceVariable([self class], key.UTF8String); return object_getIvar(self, ivar);; }else if ([mArray containsObject:isKey]) { Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String); return object_getIvar(self, ivar);; }
// 5.抛出异常 @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: valueForUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key] userInfo:nil];
valueForUndefinedKey:
的异常取值过程的自定义也结束了,其实这里也有不严谨的地方,好比取得属性值返回的时候须要根据属性值类型来判断是否要转换成 NSNumber
或 NSValue
,以及对 NSArray
和 NSSet
类型的判断。
KVC
探索完了,其实咱们探索的大部份内容都是基于苹果的官方文档,咱们在探索 iOS
底层的时候,文档思惟十分重要,有时候说不定在文档的某个角落里就隐藏着追寻的答案。KVC
用起来不难,理解起来也不难,可是这不意味着咱们能够轻视它。在 iOS 13
以前,咱们能够经过 KVC
去获取和设置系统的私有属性,但从 iOS 13
以后,这种方式被禁用掉了。建议对 KVC
理解还不透彻的读者去多几遍官方文档,相信我,你会有新的收获。最后,咱们简单总结一下本文的内容。
KVC
是一种 NSKeyValueCoding
隐式协议所提供的机制。KVC
经过 valueForKey:
和 valueForKeyPath:
来取值,不考虑集合类型的话具体的取值过程以下:
get<Key>
, <key>
, is<Key>
, _<key>
的顺序查找方法accessInstanceVariablesDirectly
判断是否能读取成员变量来返回属性值_<key>
, _is<Key>
, <key>
, is<Key>
的顺序查找成员变量KVC
经过 setValueForKey:
和 setValueForKeyPath:
来取值,不考虑集合类型的话具体的设置值过程以下:
set<Key>
, _set<Key>
的顺序查找方法accessInstanceVariablesDirectly
判断是否能经过成员变量来返回设置值_<key>
, _is<Key>
, <key>
, is<Key>
的顺序查找成员变量