
KVC
KVC定義
KVC(Key-value coding)鍵值編碼,就是指iOS的開發中,可以允許開發者通過Key名直接通路對象的屬性,或者給對象的屬性指派。而不需要調用明确的存取方法。這樣就可以在運作時動态地通路和修改對象的屬性。而不是在編譯時确定,這也是iOS開發中的黑魔法之一。很多進階的iOS開發技巧都是基于KVC實作的。
在實作了通路器方法的類中,使用點文法和KVC通路對象其實差别不大,二者可以任意混用。但是沒有通路起方法的類中,點文法無法使用,這時KVC就有優勢了。
KVC的定義都是對NSObject的擴充來實作的,Objective-C中有個顯式的NSKeyValueCoding類别名,是以對于所有繼承了NSObject的類型,都能使用KVC(一些純Swift類和結構體是不支援KVC的,因為沒有繼承NSObject),下面是KVC最為重要的四個方法:
- (nullable id)valueForKey:(NSString *)key; //直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通過Key來設值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設值
NSKeyValueCoding類别中其他的一些方法:
+ (BOOL)accessInstanceVariablesDirectly;
//預設傳回YES,表示如果沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜尋成員,設定成NO就不這樣搜尋
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正确性�驗證的API,它可以用來檢查set的值是否正确、為不正确的值做一個替換值或者拒絕設定新值并傳回錯誤原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,裡面還有一系列這樣的API,如果屬性是一個NSMutableArray,那麼可以用這個方法來傳回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且沒有KVC無法搜尋到任何和Key有關的字段或者屬性,則會調用這個方法,預設是抛出異常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個方法一樣,但這個方法是設值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時面給Value傳nil,則會調用這個方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,傳回該組key對應的Value,再轉成字典傳回,用于将Model轉到字典。
同時蘋果對一些容器類比如NSArray或者NSSet等,KVC有着特殊的實作。
有序集合對應方法如下:
-countOf<Key>//必須實作,對應于NSArray的基本方法count:2 -objectIn<Key>AtIndex:
-<key>AtIndexes://這兩個必須實作一個,對應于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必須實作的,但實作後可以提高性能,其對應于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://兩個必須實作一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://兩個必須實作一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://可選的,如果在此類操作上有性能問題,就需要考慮實作之
無序集合對應方法如下:
-countOf<Key>//必須實作,對應于NSArray的基本方法count:
-objectIn<Key>AtIndex:
-<key>AtIndexes://這兩個必須實作一個,對應于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必須實作的,但實作後可以提高性能,其對應于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://兩個必須實作一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://兩個必須實作一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://這兩個都是可選的,如果在此類操作上有性能問題,就需要考慮實作之
通過以下幾個方面講解KVC相關的技術概念以及使用:
- KVC設值
- KVC取值
- KVC使用keyPath
- KVC處理異常
- KVC處理數值和結構體類型屬性
- KVC鍵值驗證(Key-Value Validation)
- KVC處理集合
- KVC處理字典
KVC相關技術概念
KVC設值
KVC要設值,那麼就要對象中對應的key,KVC在内部是按什麼樣的順序來尋找key的。當調用setValue:屬性值 forKey:@”name“的代碼時,底層的執行機制如下:
- 程式優先調用set<Key>:屬性值方法,代碼通過setter方法完成設定。注意,這裡的<key>是指成員變量名,首字母大小寫要符合KVC的命名規則,下同
- 如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有傳回YES,預設該方法會傳回YES,如果你重寫了該方法讓其傳回NO的話,那麼在這一步KVC會執行setValue:forUndefinedKey:方法,不過一般開發者不會這麼做。是以KVC機制會搜尋該類裡面有沒有名為<key>的成員變量,無論該變量是在類接口處定義,還是在類實作處定義,也無論用了什麼樣的通路修飾符,隻在存在以<key>命名的變量,KVC都可以對該成員變量指派。
- 如果該類即沒有set<key>:方法,也沒有_<key>成員變量,KVC機制會搜尋_is<Key>的成員變量。
- 和上面一樣,如果該類即沒有set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機制再會繼續搜尋<key>和is<Key>的成員變量。再給它們指派。
- 如果上面列出的方法或者成員變量都不存在,系統将會執行該對象的setValue:forUndefinedKey:方法,預設是抛出異常。
簡單來說就是
如果沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜尋成員并進行指派操作
。
如果開發者想讓這個類禁用KVC裡,那麼重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其傳回NO即可,這樣的話如果KVC沒有找到set<Key>:屬性名時,會直接用setValue:forUndefinedKey:方法。
下面看例子:
#import <Foundation/Foundation.h>
@interface Test: NSObject {
NSString *_name;
}
@end
@implementation Test
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成對象
Test *obj = [[Test alloc] init];
//通過KVC指派name
[obj setValue:@"xiaoming" forKey:@"name"];
//通過KVC取值name列印
NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
}
return 0;
}
列印結果:
2018-05-05 15:36:52.354405+0800 KVCKVO[35231:6116188] obj的名字是xiaoming
可以看到通過
- (void)setValue:(nullable id)value forKey:(NSString *)key;
和
- (nullable id)valueForKey:(NSString *)key;
成功設定和取出obj對象的name值。
再看一下設定accessInstanceVariablesDirectly為NO的效果:
#import <Foundation/Foundation.h>
@interface Test: NSObject {
NSString *_name;
}
@end
@implementation Test
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成對象
Test *obj = [[Test alloc] init];
//通過KVC指派name
[obj setValue:@"xiaoming" forKey:@"name"];
//通過KVC取值name列印
NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
}
return 0;
}
列印結果:
2018-05-05 15:45:22.399021+0800 KVCKVO[35290:6145826] 出現異常,該key不存在name
2018-05-05 15:45:22.399546+0800 KVCKVO[35290:6145826] 出現異常,該key不存在name
2018-05-05 15:45:22.399577+0800 KVCKVO[35290:6145826] obj的名字是(null)
可以看到accessInstanceVariablesDirectly為NO的時候KVC隻會查詢setter和getter這一層,下面尋找key的相關變量執行就會停止,直接報錯。
設定accessInstanceVariablesDirectly為YES,再修改_name為_isName,看看執行是否成功。
#import <Foundation/Foundation.h>
@interface Test: NSObject {
NSString *_isName;
}
@end
@implementation Test
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成對象
Test *obj = [[Test alloc] init];
//通過KVC指派name
[obj setValue:@"xiaoming" forKey:@"name"];
//通過KVC取值name列印
NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
}
return 0;
}
列印結果:
2018-05-05 15:49:53.444350+0800 KVCKVO[35303:6157671] obj的名字是xiaoming
從列印可以看到設定accessInstanceVariablesDirectly為YES,KVC會繼續按照順序查找,并成功設值和取值了。
KVC取值
當調用valueForKey:@”name“的代碼時,KVC對key的搜尋方式不同于setValue:屬性值 forKey:@”name“,其搜尋方式如下:
- 首先按get<Key>,<key>,is<Key>的順序方法查找getter方法,找到的話會直接調用。如果是BOOL或者Int等值類型, 會将其包裝成一個NSNumber對象。
- 如果上面的getter沒有找到,KVC則會查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個方法中的一個被找到,那麼就會傳回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬于NSArray的方法,就會以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes這幾個方法組合的形式調用。還有一個可選的get<Key>:range:方法。是以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的标準命名方法,包括方法簽名。
- 如果上面的方法沒有找到,那麼會同時查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果這三個方法都找到,那麼就傳回一個可以響應NSSet所的方法的代理集合,和上面一樣,給這個代理集合發NSSet的消息,就會以countOf<Key>,enumeratorOf<Key>,memberOf<Key>組合的形式調用。
- 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果傳回YES(預設行為),那麼和先前的設值一樣,會按_<key>,_is<Key>,<key>,is<Key>的順序搜尋成員變量名,這裡不推薦這麼做,因為這樣直接通路執行個體變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly傳回NO的話,那麼會直接調用valueForUndefinedKey:方法,預設是抛出異常。
給Test類添加getAge方法,例如如下:
#import <Foundation/Foundation.h>
@interface Test: NSObject {
}
@end
@implementation Test
- (NSUInteger)getAge {
return 10;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成對象
Test *obj = [[Test alloc] init];
//通過KVC取值age列印
NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
}
return 0;
}
列印結果:
2018-05-05 16:00:04.207857+0800 KVCKVO[35324:6188613] obj的年齡是10
可以看到
[obj valueForKey:@"age"]
,找到了getAge方法,并且取到了值。
下面把getAge改成age,例子如下:
#import <Foundation/Foundation.h>
@interface Test: NSObject {
}
@end
@implementation Test
- (NSUInteger)age {
return 10;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成對象
Test *obj = [[Test alloc] init];
//通過KVC取值age列印
NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
}
return 0;
}
列印結果:
2018-05-05 16:02:27.270954+0800 KVCKVO[35337:6195086] obj的年齡是10
可以看到
[obj valueForKey:@"age"]
,找到了age方法,并且取到了值。
下面把getAge改成isAge,例子如下:
#import <Foundation/Foundation.h>
@interface Test: NSObject {
}
@end
@implementation Test
- (NSUInteger)isAge {
return 10;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成對象
Test *obj = [[Test alloc] init];
//通過KVC取值age列印
NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
}
return 0;
}
列印結果:
2018-05-05 16:03:56.234338+0800 KVCKVO[35345:6201242] obj的年齡是10
可以看到
[obj valueForKey:@"age"]
,找到了isAge方法,并且取到了值。
上面的代碼說明了說明了KVC在調用
valueforKey:@"age"
時搜尋key的機制。
KVC使用keyPath
在開發過程中,一個類的成員變量有可能是自定義類或其他的複雜資料類型,你可以先用KVC擷取該屬性,然後再次用KVC來擷取這個自定義類的屬性,
但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath。顧名思義,就是按照路徑尋找key。
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設值
用代碼實作如下:
#import <Foundation/Foundation.h>
@interface Test1: NSObject {
NSString *_name;
}
@end
@implementation Test1
@end
@interface Test: NSObject {
Test1 *_test1;
}
@end
@implementation Test
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成對象
Test *test = [[Test alloc] init];
//Test1生成對象
Test1 *test1 = [[Test1 alloc] init];
//通過KVC設值test的"test1"
[test setValue:test1 forKey:@"test1"];
//通過KVC設值test的"test1的name"
[test setValue:@"xiaoming" forKeyPath:@"test1.name"];
//通過KVC取值age列印
NSLog(@"test的\"test1的name\"是%@", [test valueForKeyPath:@"test1.name"]);
}
return 0;
}
列印結果:
2018-05-05 16:19:02.613394+0800 KVCKVO[35436:6239788] test的"test1的name"是xiaoming
從列印結果來看我們成功的通過keyPath設定了test1的值。
KVC對于keyPath是搜尋機制第一步就是分離key,用小數點.來分割key,然後再像普通key一樣按照先前介紹的順序搜尋下去。
KVC處理異常
KVC中最常見的異常就是不小心使用了錯誤的key,或者在設值中不小心傳遞了nil的值,KVC中有專門的方法來處理這些異常。
KVC處理nil異常
通常情況下,KVC不允許你要在調用setValue:屬性值 forKey:(或者keyPath)時對非對象傳遞一個nil的值。很簡單,因為值類型是不能為nil的。如果你不小心傳了,KVC會調用setNilValueForKey:方法。這個方法預設是抛出異常,是以一般而言最好還是重寫這個方法。
代碼實作如下:
#import <Foundation/Foundation.h>
@interface Test: NSObject {
NSUInteger age;
}
@end
@implementation Test
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能将%@設成nil", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成對象
Test *test = [[Test alloc] init];
//通過KVC設值test的age
[test setValue:nil forKey:@"age"];
//通過KVC取值age列印
NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
}
return 0;
}
列印結果:
2018-05-05 16:24:30.302134+0800 KVCKVO[35470:6258307] 不能将age設成nil
2018-05-05 16:24:30.302738+0800 KVCKVO[35470:6258307] test的年齡是0
KVC處理UndefinedKey異常
通常情況下,KVC不允許你要在調用setValue:屬性值 forKey:(或者keyPath)時對不存在的key進行操作。
不然,會報錯forUndefinedKey發生崩潰,重寫forUndefinedKey方法避免崩潰。
#import <Foundation/Foundation.h>
@interface Test: NSObject {
}
@end
@implementation Test
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成對象
Test *test = [[Test alloc] init];
//通過KVC設值test的age
[test setValue:@10 forKey:@"age"];
//通過KVC取值age列印
NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
}
return 0;
}
列印結果:
2018-05-05 16:30:18.564680+0800 KVCKVO[35487:6277523] 出現異常,該key不存在age
2018-05-05 16:30:18.565190+0800 KVCKVO[35487:6277523] 出現異常,該key不存在age
2018-05-05 16:30:18.565216+0800 KVCKVO[35487:6277523] test的年齡是(null)
KVC處理數值和結構體類型屬性
不是每一個方法都傳回對象,但是valueForKey:總是傳回一個id對象,如果原本的變量類型是值類型或者結構體,傳回值會封裝成NSNumber或者NSValue對象。
這兩個類會處理從數字,布爾值到指針和結構體任何類型。然後開以者需要手動轉換成原來的類型。
盡管valueForKey:會自動将值類型封裝成對象,但是setValue:forKey:卻不行。你必須手動将值類型轉換成NSNumber或者NSValue類型,才能傳遞過去。
因為傳遞進去和取出來的都是id類型,是以需要開發者自己擔保類型的正确性,運作時Objective-C在發送消息的會檢查類型,如果錯誤會直接抛出異常。
舉個例子,Person類有個NSInteger類型的age屬性,如下:
// Person.m
#import "Person.h"
@interface Person ()
@property (nonatomic,assign) NSInteger age;
@end
@implementation Person
@end
修改值
我們通過KVC技術使用如下方式設定age屬性的值:
[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
我們賦給age的是一個NSNumber對象,KVC會自動的将NSNumber對象轉換成NSInteger對象,然後再調用相應的通路器方法設定age的值。
擷取值
同樣,以如下方式擷取age屬性值:
[person valueForKey:@"age"];
這時,會以NSNumber的形式傳回age的值。
// ViewController.m
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];
[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
NSLog(@"age=%@",[person valueForKey:@"age"]);
}
@end
列印結果:
2017-01-16 16:31:55.709 Test[28586:2294177] age=5
需要注意的是我們不能直接将一個數值通過KVC指派的,我們需要把資料轉為NSNumber和NSValue類型傳入,那到底哪些類型資料要用NSNumber封裝哪些類型資料要用NSValue封裝呢?看下面這些方法的參數類型就知道了:
可以使用NSNumber的資料類型有:
+ (NSNumber*)numberWithChar:(char)value;
+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber*)numberWithShort:(short)value;
+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber*)numberWithInt:(int)value;
+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber*)numberWithLong:(long)value;
+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber*)numberWithLongLong:(longlong)value;
+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber*)numberWithFloat:(float)value;
+ (NSNumber*)numberWithDouble:(double)value;
+ (NSNumber*)numberWithBool:(BOOL)value;
+ (NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);
就是一些常見的數值型資料。
可以使用NSValue的資料類型有:
+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);
NSValue主要用于處理結構體型的資料,它本身提供了如上集中結構的支援。任何結構體都是可以轉化成NSValue對象的,包括其它自定義的結構體。
KVC鍵值驗證(Key-Value Validation)
KVC提供了驗證Key對應的Value是否可用的方法:
- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;
該方法預設的實作是調用一個如下格式的方法:
- (BOOL)validate<Key>:error:
例如:
#import <Foundation/Foundation.h>
@interface Test: NSObject {
NSUInteger _age;
}
@end
@implementation Test
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
NSNumber *age = *ioValue;
if (age.integerValue == 10) {
return NO;
}
return YES;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成對象
Test *test = [[Test alloc] init];
//通過KVC設值test的age
NSNumber *age = @10;
NSError* error;
NSString *key = @"age";
BOOL isValid = [test validateValue:&age forKey:key error:&error];
if (isValid) {
NSLog(@"鍵值比對");
[test setValue:age forKey:key];
}
else {
NSLog(@"鍵值不比對");
}
//通過KVC取值age列印
NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
}
return 0;
}
列印結果:
2018-05-05 16:59:06.111671+0800 KVCKVO[35777:6329982] 鍵值不比對
2018-05-05 16:59:06.112215+0800 KVCKVO[35777:6329982] test的年齡是0
這樣就給了我們一次糾錯的機會。需要指出的是,KVC是不會自動調用鍵值驗證方法的,就是說我們如果想要鍵值驗證則需要手動驗證。但是有些技術,比如CoreData會自動調用。
KVC處理集合
KVC同時還提供了很複雜的函數,主要有下面這些:
簡單集合運算符
簡單集合運算符共有@avg, @count , @max , @min ,@sum5種,都表示什麼直接看下面例子就明白了, 目前還不支援自定義。
#import <Foundation/Foundation.h>
@interface Book : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) CGFloat price;
@end
@implementation Book
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 10;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 20;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 30;
Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 40;
NSArray* arrBooks = @[book1,book2,book3,book4];
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
NSLog(@"sum:%f",sum.floatValue);
NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
NSLog(@"avg:%f",avg.floatValue);
NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
NSLog(@"count:%f",count.floatValue);
NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
NSLog(@"min:%f",min.floatValue);
NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
NSLog(@"max:%f",max.floatValue);
}
return 0;
}
列印結果:
2018-05-05 17:04:50.674243+0800 KVCKVO[35785:6351239] sum:100.000000
2018-05-05 17:04:50.675007+0800 KVCKVO[35785:6351239] avg:25.000000
2018-05-05 17:04:50.675081+0800 KVCKVO[35785:6351239] count:4.000000
2018-05-05 17:04:50.675146+0800 KVCKVO[35785:6351239] min:10.000000
2018-05-05 17:04:50.675204+0800 KVCKVO[35785:6351239] max:40.000000
對象運算符
比集合運算符稍微複雜,能以數組的方式傳回指定的内容,一共有兩種:
- @distinctUnionOfObjects
- @unionOfObjects
它們的傳回值都是NSArray,差別是前者傳回的元素都是唯一的,是去重以後的結果;後者傳回的元素是全集。
例如:
#import <Foundation/Foundation.h>
@interface Book : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) CGFloat price;
@end
@implementation Book
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 40;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 20;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 30;
Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 10;
NSArray* arrBooks = @[book1,book2,book3,book4];
NSLog(@"distinctUnionOfObjects");
NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
for (NSNumber *price in arrDistinct) {
NSLog(@"%f",price.floatValue);
}
NSLog(@"unionOfObjects");
NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
for (NSNumber *price in arrUnion) {
NSLog(@"%f",price.floatValue);
}
}
return 0;
}
列印結果:
2018-05-05 17:06:21.832401+0800 KVCKVO[35798:6358293] distinctUnionOfObjects
2018-05-05 17:06:21.833079+0800 KVCKVO[35798:6358293] 10.000000
2018-05-05 17:06:21.833112+0800 KVCKVO[35798:6358293] 20.000000
2018-05-05 17:06:21.833130+0800 KVCKVO[35798:6358293] 30.000000
2018-05-05 17:06:21.833146+0800 KVCKVO[35798:6358293] 40.000000
2018-05-05 17:06:21.833165+0800 KVCKVO[35798:6358293] unionOfObjects
2018-05-05 17:06:21.833297+0800 KVCKVO[35798:6358293] 40.000000
2018-05-05 17:06:21.833347+0800 KVCKVO[35798:6358293] 20.000000
2018-05-05 17:06:21.833371+0800 KVCKVO[35798:6358293] 30.000000
2018-05-05 17:06:21.833388+0800 KVCKVO[35798:6358293] 10.000000
KVC處理字典
當對NSDictionary對象使用KVC時,valueForKey:的表現行為和objectForKey:一樣。是以使用valueForKeyPath:用來通路多層嵌套的字典是比較友善的。
KVC裡面還有兩個關于NSDictionary的方法:
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
dictionaryWithValuesForKeys:是指輸入一組key,傳回這組key對應的屬性,再組成一個字典。
setValuesForKeysWithDictionary是用來修改Model中對應key的屬性。下面直接用代碼會更直覺一點:
#import <Foundation/Foundation.h>
@interface Address : NSObject
@end
@interface Address()
@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;
@end
@implementation Address
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
//模型轉字典
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把對應key所有的屬性全部取出來
NSLog(@"%@",dict);
//字典轉模型
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value來修改Model的屬性
NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
}
return 0;
}
列印結果:
2018-05-05 17:08:48.824653+0800 KVCKVO[35807:6368235] {
city = "Shen Zhen";
country = China;
district = "Nan Shan";
province = "Guang Dong";
}
2018-05-05 17:08:48.825075+0800 KVCKVO[35807:6368235] country:USA province:california city:Los angle
列印出來的結果完全符合預期。
KVC使用
KVC在iOS開發中是絕不可少的利器,這種基于運作時的程式設計方式極大地提高了靈活性,簡化了代碼,甚至實作很多難以想像的功能,KVC也是許多iOS開發黑魔法的基礎。
下面列舉iOS開發中KVC的使用場景.
動态地取值和設值
利用KVC動态的取值和設值是最基本的用途了。
用KVC來通路和修改私有變量
對于類裡的私有屬性,Objective-C是無法直接通路的,但是KVC是可以的。
Model和字典轉換
這是KVC強大作用的又一次展現,KVC和Objc的runtime組合可以很容易的實作Model和字典的轉換。
修改一些控件的内部屬性
這也是iOS開發中必不可少的小技巧。衆所周知很多UI控件都由很多内部UI控件組合而成的,但是Apple度沒有提供這通路這些控件的API,這樣我們就無法正常地通路和修改這些控件的樣式。
而KVC在大多數情況可下可以解決這個問題。最常用的就是個性化UITextField中的placeHolderText了。
操作集合
Apple對KVC的valueForKey:方法作了一些特殊的實作,比如說NSArray和NSSet這樣的容器類就實作了這些方法。是以可以用KVC很友善地操作集合。
用KVC實作高階消息傳遞
當對容器類使用KVC時,valueForKey:将會被傳遞給容器中的每一個對象,而不是容器本身進行操作。結果會被添加進傳回的容器中,這樣,開發者可以很友善的操作集合來傳回另一個集合。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str in arrCapStr) {
NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length in arrCapStrLength) {
NSLog(@"%ld",(long)length.integerValue);
}
}
return 0;
}
列印結果:
2018-05-05 17:16:21.975983+0800 KVCKVO[35824:6395514] English
2018-05-05 17:16:21.976296+0800 KVCKVO[35824:6395514] Franch
2018-05-05 17:16:21.976312+0800 KVCKVO[35824:6395514] Chinese
2018-05-05 17:16:21.976508+0800 KVCKVO[35824:6395514] 7
2018-05-05 17:16:21.976533+0800 KVCKVO[35824:6395514] 6
2018-05-05 17:16:21.976550+0800 KVCKVO[35824:6395514] 7
方法capitalizedString被傳遞到NSArray中的每一項,這樣,NSArray的每一員都會執行capitalizedString并傳回一個包含結果的新的NSArray。
從列印結果可以看出,所有String都成功以轉成了大寫。
同樣如果要執行多個方法也可以用valueForKeyPath:方法。它先會對每一個成員調用 capitalizedString方法,然後再調用length,因為lenth方法傳回是一個數字,是以傳回結果以NSNumber的形式儲存在新數組裡。
實作KVO
KVO是基于KVC實作的,下面講一下KVO的概念和實作。
KVO
KVO定義
KVO 即 Key-Value Observing,翻譯成鍵值觀察。它是一種觀察者模式的衍生。其基本思想是,對目标對象的某屬性添加觀察,當該屬性發生變化時,通過觸發觀察者對象實作的KVO接口方法,來自動的通知觀察者。
觀察者模式是什麼
一個目标對象管理所有依賴于它的觀察者對象,并在它自身的狀态改變時主動通知觀察者對象。這個主動通知通常是通過調用各觀察者對象所提供的接口方法來實作的。觀察者模式較完美地将目标對象與觀察者對象解耦。
簡單來說KVO可以通過監聽key,來獲得value的變化,用來在對象之間監聽狀态變化。KVO的定義都是對NSObject的擴充來實作的,Objective-C中有個顯式的NSKeyValueObserving類别名,是以對于所有繼承了NSObject的類型,都能使用KVO(一些純Swift類和結構體是不支援KVC的,因為沒有繼承NSObject)。
KVO使用
注冊與解除注冊
如果我們已經有了包含可供鍵值觀察屬性的類,那麼就可以通過在該類的對象(被觀察對象)上調用名為 NSKeyValueObserverRegistration 的 category 方法将觀察者對象與被觀察者對象注冊與解除注冊:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
observer:觀察者,也就是KVO通知的訂閱者。訂閱着必須實作
observeValueForKeyPath:ofObject:change:context:方法
keyPath:描述将要觀察的屬性,相對于被觀察者。
options:KVO的一些屬性配置;有四個選項。
context: 上下文,這個會傳遞到訂閱着的函數中,用來區分消息,是以應當是不同的。
options所包括的内容
NSKeyValueObservingOptionNew:change字典包括改變後的值
NSKeyValueObservingOptionOld:change字典包括改變前的值
NSKeyValueObservingOptionInitial:注冊後立刻觸發KVO通知
NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
這兩個方法的定義在 Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均實作了以上方法,是以我們不僅可以觀察普通對象,還可以觀察數組或結合類對象。在該頭檔案中,我們還可以看到 NSObject 還實作了 NSKeyValueObserverNotification 的 category 方法(更多類似方法,請檢視該頭檔案NSKeyValueObserving.h):
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
這兩個方法在手動實作鍵值觀察時會用到。注意在不用的時候,不要忘記解除注冊,否則會導緻記憶體洩露。
處理變更通知
每當監聽的keyPath發生變化了,就會在這個函數中回調。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
在這裡,change 這個字典儲存了變更資訊,具體是哪些資訊取決于注冊時的 NSKeyValueObservingOptions。
手動KVO(禁用KVO)
KVO的實作,是對注冊的keyPath中自動實作了兩個函數,在setter中,自動調用。
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
可能有時候,我們要實作手動的KVO,或者我們實作的類庫不希望被KVO。
這時候需要關閉自動生成KVO通知,然後手動的調用,手動通知的好處就是,可以靈活加上自己想要的判斷條件。下面看個例子如下:
@interface Target : NSObject
{
int age;
}
// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;
@end
@implementation Target
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
}
return self;
}
// for manual KVO - age
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
首先,需要手動實作屬性的 setter 方法,并在設定操作的前後分别調用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用于通知系統該 key 的屬性值即将和已經變更了;
其次,要實作類方法 automaticallyNotifiesObserversForKey,并在其中設定對該 key 不自動發送通知(傳回 NO 即可)。這裡要注意,對其它非手動實作的 key,要轉交給 super 來處理。
如果需要禁用該類KVO的話直接automaticallyNotifiesObserversForKey傳回NO,實作屬性的 setter 方法,不進行調用willChangeValueForKey: 和 didChangeValueForKey方法。
鍵值觀察依賴鍵
有時候一個屬性的值依賴于另一對象中的一個或多個屬性,如果這些屬性中任一屬性的值發生變更,被依賴的屬性值也應當為其變更進行标記。是以,object 引入了依賴鍵。
觀察依賴鍵
觀察依賴鍵的方式與前面描述的一樣,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加處理變更通知的代碼:
#import "TargetWrapper.h"
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:@"age"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Age changed", className);
NSLog(@" old age is %@", [change objectForKey:@"old"]);
NSLog(@" new age is %@", [change objectForKey:@"new"]);
}
else if ([keyPath isEqualToString:@"information"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Information changed", className);
NSLog(@" old information is %@", [change objectForKey:@"old"]);
NSLog(@" new information is %@", [change objectForKey:@"new"]);
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
實作依賴鍵
在這裡,觀察的是 TargetWrapper 類的 information 屬性,該屬性是依賴于 Target 類的 age 和 grade 屬性。為此,我在 Target 中添加了 grade 屬性:
@interface Target : NSObject
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end
@implementation Target
@synthesize age; // for automatic KVO - age
@synthesize grade;
@end
下面來看看 TragetWrapper 中的依賴鍵屬性是如何實作的。
@class Target;
@interface TargetWrapper : NSObject
{
@private
Target * _target;
}
@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;
-(id) init:(Target *)aTarget;
@end
#import "TargetWrapper.h"
#import "Target.h"
@implementation TargetWrapper
@synthesize target = _target;
-(id) init:(Target *)aTarget
{
self = [super init];
if (nil != self) {
_target = [aTarget retain];
}
return self;
}
-(void) dealloc
{
self.target = nil;
[super dealloc];
}
- (NSString *)information
{
return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}
- (void)setInformation:(NSString *)theInformation
{
NSArray * array = [theInformation componentsSeparatedByString:@"#"];
[_target setGrade:[[array objectAtIndex:0] intValue]];
[_target setAge:[[array objectAtIndex:1] intValue]];
}
+ (NSSet *)keyPathsForValuesAffectingInformation
{
NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
return keyPaths;
}
//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
// NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
// NSArray * moreKeyPaths = nil;
//
// if ([key isEqualToString:@"information"])
// {
// moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
// }
//
// if (moreKeyPaths)
// {
// keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
// }
//
// return keyPaths;
//}
@end
首先,要手動實作屬性 information 的 setter/getter 方法,在其中使用 Target 的屬性來完成其 setter 和 getter。
其次,要實作 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法來告訴系統 information 屬性依賴于哪些其他屬性,這兩個方法都傳回一個key-path 的集合。在這裡要多說幾句,如果選擇實作 keyPathsForValuesAffectingValueForKey,要先擷取 super 傳回的結果 set,然後判斷 key 是不是目标 key,如果是就将依賴屬性的 key-path 結合追加到 super 傳回的結果 set 中,否則直接傳回 super的結果。
在這裡,information 屬性依賴于 target 的 age 和 grade 屬性,target 的 age/grade 屬性任一發生變化,information 的觀察者都會得到通知。
Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];
TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
forKeyPath:@"information"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:[TargetWrapper class]];
[target setAge:30];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];
列印結果:
class: TargetWrapper, Information changed
old information is 0#10
new information is 0#30
class: TargetWrapper, Information changed
old information is 0#30
new information is 1#30
KVO和線程
一個需要注意的地方是,KVO 行為是同步的,并且發生與所觀察的值發生變化的同樣的線程上。沒有隊列或者 Run-loop 的處理。手動或者自動調用 -didChange... 會觸發 KVO 通知。
是以,當我們試圖從其他線程改變屬性值的時候我們應當十分小心,除非能确定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來說,我們不推薦把 KVO 和多線程混起來。如果我們要用多個隊列和線程,我們不應該在它們互相之間用 KVO。
KVO 是同步運作的這個特性非常強大,隻要我們在單一線程上面運作(比如主隊列 main queue),KVO 會保證下列兩種情況的發生:
首先,如果我們調用一個支援 KVO 的 setter 方法,如下所示:
self.exchangeRate = 2.345;
KVO 能保證所有 exchangeRate 的觀察者在 setter 方法傳回前被通知到。
其次,如果某個鍵被觀察的時候附上了 NSKeyValueObservingOptionPrior 選項,直到 -observe... 被調用之前, exchangeRate 的 accessor 方法都會傳回同樣的值。
KVO實作
KVO 是通過 isa-swizzling 實作的。
基本的流程就是編譯器自動為被觀察對象創造一個派生類,并将被觀察對象的isa 指向這個派生類。如果使用者注冊了對某此目标對象的某一個屬性的觀察,那麼此派生類會重寫這個方法,并在其中添加進行通知的代碼。Objective-C 在發送消息的時候,會通過 isa 指針找到目前對象所屬的類對象。而類對象中儲存着目前對象的執行個體方法,是以在向此對象發送消息時候,實際上是發送到了派生類對象的方法。由于編譯器對派生類的方法進行了 override,并添加了通知代碼,是以會向注冊的對象發送通知。注意派生類隻重寫注冊了觀察者的屬性方法。
蘋果官方文檔的說明如下:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The
isa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the
pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
isa
KVO的實作依賴于Runtime的強大動态能力。
即當一個類型為 ObjectA 的對象,被添加了觀察後,系統會生成一個 NSKVONotifying_ObjectA 類,并将對象的isa指針指向新的類,也就是說這個對象的類型發生了變化。這個類相比較于ObjectA,會重寫以下幾個方法。
重寫setter
在 setter 中,會添加以下兩個方法的調用。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
然後在
didChangeValueForKey:
中,去調用:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
包含了新值和舊值的通知。
于是實作了屬性值修改的通知。因為 KVO 的原理是修改 setter 方法,是以使用 KVO 必須調用 setter 。若直接通路屬性對象則沒有效果。
重寫class
當修改了isa指向後,class的傳回值不會變,但isa的值則發生改變。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface ObjectA: NSObject
@property (nonatomic) NSInteger age;
@end
@implementation ObjectA
@end
@interface ObjectB: NSObject
@end
@implementation ObjectB
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//生成對象
ObjectA *objA = [[ObjectA alloc] init];
ObjectB *objB = [[ObjectB alloc] init];
// 添加Observer之後
[objA addObserver:objB forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 輸出ObjectA
NSLog(@"%@", [objA class]);
// 輸出NSKVONotifying_ObjectA(object_getClass方法傳回isa指向)
NSLog(@"%@", object_getClass(objA));
}
return 0;
}
列印結果:
2018-05-06 22:47:05.538899+0800 KVCKVO[38474:13343992] ObjectA
2018-05-06 22:47:05.539242+0800 KVCKVO[38474:13343992] NSKVONotifying_ObjectA
重寫dealloc
系統重寫 dealloc 方法來釋放資源。
重寫_isKVOA
這個私有方法是用來标示該類是一個 KVO 機制聲稱的類。
如何證明被觀察的類被重寫了以上方法
參考用代碼探讨 KVC/KVO 的實作原理這篇文章,通過代碼一步步分析,從斷點截圖來看,可以很好證明以上被重寫的方法。
作者:jackyshan
連結:https://www.jianshu.com/p/b9f020a8b4c9
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。