iOS 底層探索系列html
- iOS 底層探索 - alloc & init
- iOS 底層探索 - calloc 和 isa
- iOS 底層探索 - 類
- iOS 底層探索 - cache_t
- iOS 底層探索 - 方法
- iOS 底層探索 - 消息查找
- iOS 底層探索 - 消息轉發
- iOS 底層探索 - 應用加載
- iOS 底層探索 - 類的加載
- iOS 底層探索 - 分類的加載
- iOS 底層探索 - 類拓展和關聯對象
- iOS 底層探索 - KVC
- iOS 底層探索 - KVO
iOS 查漏補缺系列前端
Key Value Coding
也即 KVC
是 iOS
開發中一個很重要的概念,中文翻譯過來是 鍵值編碼
,關於這個概念的具體定義能夠在 Apple
的官方文檔處找到。git
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. 【譯】
KVC
是經過NSKeyValueCoding
這個非正式協議啓用的一種機制,而遵循了這個協議的對象就提供了對其屬性的間接訪問。github
咱們一般使用訪問器方法來訪問對象的屬性,即便用 getter
來獲取屬性值,使用 setter
來設置屬性值。而在 Objective-C
中,咱們還能夠直接經過實例變量的方式來獲取屬性值和設置屬性值。以下面的代碼所示:objective-c
// 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
中來找到它們的蹤跡,感興趣的讀者能夠翻閱前面的文章。設計模式
這裏再明確下實例變量、成員變量、屬性之間的區別: 在 @interface 括號裏面聲明的變量統稱爲 成員變量 而成員變量實際上由兩部分組成:實例變量 + 基本數據類型變量 而屬性 = 成員變量 + getter方法 + setter方法api
那其實這裏分兩種狀況,本身實現和編譯器幫咱們實現。數組
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
的值:bash
#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
的場景。
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
以後會在對象中按下列的步驟進行模式搜索:
get<Key>
, <key>
, is<Key>
以及 _<key>
的順序查找對象中是否有對應的方法。
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:
)countOf<Key>
,enumeratorOf<Key>
和 memberOf<Key>
這三個方法(對應於NSSet類定義的原始方法)
NSSet
方法的代理集合對象,並返回該對象accessInstanceVariablesDirectly
結果
YES
,則以 _<key>
, _is<Key>
, <key>
, is<Key>
的順序查找成員變量,若是找到了,將成員變量帶上跳轉到第 5 步,若是沒有找到則跳轉到第 6 步NO
,跳轉到第 6 步NSNumber
類型,則將屬性值轉化爲 NSNumber
類型返回NSNumber
類型,則將屬性值轉化爲 NSValue
類型返回valueForUndefinedKey:
。 默認狀況下,這會引起一個異常,可是 NSObject
的子類能夠提供特定於 key
的行爲。這裏能夠用簡單的流程圖來表示
setter
setValue:forKey:
方法默認實現會在調用者傳入 key
和 value
(若是是非對象類型,則指的是解包以後的值) 以後會在對象中按下列的步驟進行模式搜索:
set<Key>:
, _set<Key>
的順序在對象中查找是否有這樣的方法,若是找到了,則把屬性值傳給方法來完成屬性值的設置。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>
的順序查找成員變量