首先來(lái)看看幾個(gè)定義:
編譯型語(yǔ)言:
概念:需要編輯器將源代碼編譯成機(jī)器碼之后才能執(zhí)行的語(yǔ)言。一般分兩個(gè)步驟 編譯(compile)、鏈接(linker)編譯是把各個(gè)文件源代碼編譯成機(jī)器碼,鏈接是把各個(gè)文件的機(jī)器碼和依賴庫(kù)串連起來(lái)生成可執(zhí)行文件。
流程:源代碼->匯編代碼->機(jī)器碼->CPU執(zhí)行
優(yōu)點(diǎn): 編譯器一般會(huì)有預(yù)編譯的過(guò)程對(duì)代碼進(jìn)行優(yōu)化。因?yàn)榫幾g只做一次,運(yùn)行時(shí)不需要編譯,所以編譯型語(yǔ)言的程序執(zhí)行效率高??梢悦撾x語(yǔ)言環(huán)境獨(dú)立運(yùn)行(執(zhí)行效率高)
缺點(diǎn): 編譯之后如果需要修改就需要整個(gè)模塊重新編譯。編譯的時(shí)候根據(jù)對(duì)應(yīng)的運(yùn)行環(huán)境生成機(jī)器碼,不同的操作系統(tǒng)之間移植就會(huì)有問(wèn)題,需要根據(jù)運(yùn)行的操作系統(tǒng)環(huán)境編譯不同的可執(zhí)行文件(跨平臺(tái)性能差)
代表語(yǔ)言:C、C++、Pascal、Object-C、swift。
解釋型語(yǔ)言:
概念:不需要編譯,比編譯型語(yǔ)言省了道工序,運(yùn)行的時(shí)候逐行進(jìn)行解釋,生成機(jī)器代碼。
流程:源代碼->字節(jié)碼->解釋器->機(jī)器碼->CPU執(zhí)行
優(yōu)點(diǎn): 有良好的平臺(tái)兼容性,在任何環(huán)境中都可以運(yùn)行,前提是安裝了解釋器(虛擬機(jī))。靈活,修改代碼的時(shí)候直接修改就可以,可以快速部署,不用停機(jī)維護(hù)。(跨平臺(tái)性強(qiáng))
缺點(diǎn): 程序不需要編譯,程序在運(yùn)行時(shí)才翻譯成機(jī)器碼,每執(zhí)行一次就要翻譯一次,不可脫離語(yǔ)言環(huán)境獨(dú)立運(yùn)行(需要虛擬機(jī))(執(zhí)行效率差)
代表語(yǔ)言:JavaScript、Python、Erlang、PHP、Perl、Ruby。

混合型語(yǔ)言:
既然編譯型和解釋型各有缺點(diǎn)就會(huì)有人想到把兩種類型整合起來(lái),取其精華去其糟粕。就出現(xiàn)了半編譯型語(yǔ)言。比如C#,C#在編譯的時(shí)候不是直接編譯成機(jī)器碼而是中間碼,.NET平臺(tái)提供了中間語(yǔ)言運(yùn)行庫(kù)運(yùn)行中間碼,中間語(yǔ)言運(yùn)行庫(kù)類似于Java虛擬機(jī)。.net在編譯成IL代碼后,保存在dll中,首次運(yùn)行時(shí)由JIT在編譯成機(jī)器碼緩存在內(nèi)存中,下次直接執(zhí)行
動(dòng)態(tài)語(yǔ)言:
是一類在運(yùn)行時(shí)可以改變其結(jié)構(gòu)的語(yǔ)言:例如新的函數(shù)、對(duì)象、甚至代碼可以被引進(jìn),已有的函數(shù)可以被刪除或是其他結(jié)構(gòu)上的變化。通俗點(diǎn)說(shuō)就是在運(yùn)行時(shí)代碼可以根據(jù)某些條件改變自身結(jié)構(gòu)。
主要?jiǎng)討B(tài)語(yǔ)言:Object-C、C#、JavaScript、PHP、Python。
靜態(tài)語(yǔ)言:
與動(dòng)態(tài)語(yǔ)言相對(duì)應(yīng)的,運(yùn)行時(shí)結(jié)構(gòu)不可變的語(yǔ)言就是靜態(tài)語(yǔ)言。如Java、C、C++
動(dòng)態(tài)類型語(yǔ)言:
動(dòng)態(tài)類型語(yǔ)言是指在運(yùn)行期間才去做數(shù)據(jù)類型檢查的語(yǔ)言
這里需要跟動(dòng)態(tài)語(yǔ)言區(qū)別開(kāi),動(dòng)態(tài)類型語(yǔ)言說(shuō)的是數(shù)據(jù)類型,動(dòng)態(tài)語(yǔ)言說(shuō)的是運(yùn)行時(shí)改變結(jié)構(gòu),說(shuō)的是代碼結(jié)構(gòu)。
靜態(tài)類型語(yǔ)言:
靜態(tài)語(yǔ)言的數(shù)據(jù)類型是在編譯其間確定的或者說(shuō)運(yùn)行之前確定的,編寫代碼的時(shí)候要明確確定變量的數(shù)據(jù)類型
強(qiáng)類型語(yǔ)言:
強(qiáng)類型語(yǔ)言也稱為強(qiáng)類型定義語(yǔ)言。是一種總是強(qiáng)制類型定義的語(yǔ)言,要求變量的使用要嚴(yán)格符合定義,所有變量都必須先定義后使用。
弱類型語(yǔ)言:
與上正好相反,像vb、php、js(也就是說(shuō),一個(gè)變量,你可以直接給他賦值字符串,也可以直接給他賦值數(shù)值,你還可以直接讓字符串類型的變量和數(shù)值類型的變量相加,雖然得出的最終結(jié)果未必是你想象的那樣,但一定不會(huì)報(bào)錯(cuò))
基于上面的例子,你可以說(shuō)swift允許我們不聲明類型并且讓編譯器自己檢測(cè)類型
var a = 10
看上去像是弱類型語(yǔ)言,但是swift推出它是int類型的,所以不能像其賦值其他類型的值

這說(shuō)明了,Swift 是一門強(qiáng)類型的語(yǔ)言。Swift 的類型聲明,你可以看成是在定義變量的時(shí)候,隱式聲明的(由編譯器推斷出),當(dāng)然也可以顯式的聲明。如下:
var a :Int = 10
綜上所述,可以得出 :
OC 是 動(dòng)態(tài)類型語(yǔ)言&&強(qiáng)類型語(yǔ)言&&動(dòng)態(tài)語(yǔ)言&&編譯型語(yǔ)言;
swift 是 動(dòng)態(tài)類型語(yǔ)言&&強(qiáng)類型語(yǔ)言&&靜態(tài)語(yǔ)言&&編譯型語(yǔ)言;
再來(lái)了解下Objective-C語(yǔ)言的特性:
在Objective-C中
1、所有的類都必須繼承自NSObject;
2、所有對(duì)象都是指針的形式;
3、用self代替this;
4、使用id代替void*;
5、使用nil表示NULL;
6、只支持單繼承,不允許多重繼承;
7、使用YES/NO表示TRUE/FALSE;
8、使用#import代替#include;
9、用消息表示類的方法,并采用[aInstance method:argv]調(diào)用形式;
10、支持反射機(jī)制;
11、支持Dynamic Typing(動(dòng)態(tài)類型), Dynamic Binding(動(dòng)態(tài)綁定)和Dynamic Loading(動(dòng)態(tài)加載);
12、不支持命名空間機(jī)制;
動(dòng)態(tài)特性
OC做為一門面向?qū)ο笳Z(yǔ)言,自然具有面向?qū)ο蟮恼Z(yǔ)言特性,如封裝、繼承、多態(tài)。他具有靜態(tài)語(yǔ)言的特性(如C/C++),又有動(dòng)態(tài)語(yǔ)言的特性(動(dòng)態(tài)綁定、動(dòng)態(tài)加載等)。OC的動(dòng)態(tài)特性表現(xiàn)為了三個(gè)方面:動(dòng)態(tài)類型、動(dòng)態(tài)綁定、動(dòng)態(tài)加載。之所以叫做動(dòng)態(tài),是因?yàn)楸仨毜竭\(yùn)行時(shí)(run time)才會(huì)做一些事情。
(1)動(dòng)態(tài)類型
動(dòng)態(tài)類型,說(shuō)簡(jiǎn)單點(diǎn)就是id類型。動(dòng)態(tài)類型是跟靜態(tài)類型相對(duì)的。像內(nèi)置的明確的基本類型都屬于靜態(tài)類型(int,CGFloat等)。靜態(tài)類型是強(qiáng)類型,而動(dòng)態(tài)類型屬于弱類型,靜態(tài)類型在編譯的時(shí)候就能被識(shí)別出來(lái)。所以,若程序發(fā)生了類型不對(duì)應(yīng),編譯器就會(huì)發(fā)出警告。而動(dòng)態(tài)類型在編譯器編譯的時(shí)候是不能被識(shí)別的,要等到運(yùn)行時(shí)(run time),即程序運(yùn)行的時(shí)候才會(huì)根據(jù)語(yǔ)境來(lái)識(shí)別。所以這里面就有兩個(gè)概念要分清:編譯時(shí)跟運(yùn)行時(shí)。
Hold on!!!
先看一段代碼:
NSString *str = [NSData data];
這段代碼我們command+B編譯發(fā)現(xiàn)程序可以運(yùn)行通過(guò),但是Xcode會(huì)進(jìn)行警告,因?yàn)橹羔樦赶虻念愋蜑镹SString,
但是賦值為NSData對(duì)象,所以在編譯時(shí)會(huì)警告,但是編譯時(shí)其類型依然作為NSString類型來(lái)編譯,
NSString *str = [NSData data];
[str stringByAppendingString:@"字符串"];
在這里進(jìn)行編譯發(fā)現(xiàn)編譯也可以通過(guò),因?yàn)閟tr在編譯時(shí)的類型為NSString,所以它調(diào)用字符串的方法是可以編譯
通過(guò)的,但是我們運(yùn)行程序發(fā)現(xiàn)此時(shí)程序會(huì)崩潰,此時(shí)我們打一個(gè)斷點(diǎn)來(lái)看一下str在運(yùn)行時(shí)的類型

此時(shí)我們可以看到str在程序運(yùn)行時(shí)的類型為NSData,這就是OC的動(dòng)態(tài)類型,將程序的真實(shí)類型推遲到程序運(yùn)行時(shí)才去決定。
(2)動(dòng)態(tài)綁定
動(dòng)態(tài)綁定(dynamic binding)只需記住關(guān)鍵詞@selector/SEL即可。先來(lái)看看“函數(shù)”,對(duì)于其他一些靜態(tài)語(yǔ)言,比如c++,一般在編譯的時(shí)候就已經(jīng)將將要調(diào)用的函數(shù)的函數(shù)簽名都告訴編譯器了。靜態(tài)的,不能改變,而在OC中,其實(shí)是沒(méi)有函數(shù)的概念的,我們叫“消息機(jī)制”,所謂的函數(shù)調(diào)用就是給對(duì)象發(fā)送一條消息,這時(shí),動(dòng)態(tài)綁定的特性就來(lái)了。OC可以先跳過(guò)編譯,到運(yùn)行的時(shí)候才動(dòng)態(tài)地添加函數(shù)調(diào)用,在運(yùn)行時(shí)才決定要調(diào)用什么方法,需要傳什么參數(shù)進(jìn)去。這就是動(dòng)態(tài)綁定,要實(shí)現(xiàn)他就必須用SEL變量綁定一個(gè)方法。最終形成的這個(gè)SEL變量就代表一個(gè)方法的引用。這里要注意一點(diǎn):SEL并不是C里面的函數(shù)指針,雖然很像,但真心不是函數(shù)指針。SEL變量只是一個(gè)整數(shù),他是該方法的ID。以前的函數(shù)調(diào)用,是根據(jù)函數(shù)名,也就是字符串去查找函數(shù)體。但現(xiàn)在,我們是根據(jù)一個(gè)ID整數(shù)來(lái)查找方法,整數(shù)的查找自然要比字符串的查找快得多。所以,動(dòng)態(tài)綁定的特定不僅方便,而且效率更高。
(3)動(dòng)態(tài)加載
動(dòng)態(tài)加載指的有兩方面:1.動(dòng)態(tài)資源的加載 2.部分可執(zhí)行代碼模塊的加載,這些資源在程序運(yùn)行時(shí)動(dòng)態(tài)的選擇性加載。動(dòng)態(tài)資源的加載典型就是程序中不同像素的圖片的加載,例如根據(jù)不同的機(jī)型做適配,最經(jīng)典的例子就是在Retina設(shè)備上加載@2x的圖片,而在老一些的普通屏設(shè)備上加載原圖,程序會(huì)根據(jù)當(dāng)前屏幕的像素來(lái)加載。 部分可執(zhí)行代碼模塊的加載指的程序中典型的懶加載。
了解KVC
概念: 即是指 NSKeyValueCoding,一個(gè)非正式的 Protocol,提供一種機(jī)制來(lái)間接訪問(wèn)對(duì)象的屬性, 可以通過(guò)字符串來(lái)訪問(wèn)對(duì)應(yīng)的屬性方法或成員變量。KVO 就是基于 KVC 實(shí)現(xiàn)的關(guān)鍵技術(shù)之一。
特性:
- KVC是一個(gè)用于間接訪問(wèn)對(duì)象屬性的機(jī)制;
- KVC使用該機(jī)制不需要調(diào)用存取方法和變量實(shí)例就可以訪問(wèn)對(duì)象屬性;
- KVC鍵-值編碼方法在Objective-C非正式協(xié)議(類目)NSKeyValueCoding中被聲明;
- KVC鍵-值編碼支持帶有對(duì)象值的屬性,同時(shí)也支持純數(shù)值類型和結(jié)構(gòu);
- KVC可以用來(lái)訪問(wèn)和設(shè)置實(shí)例變量的值。key是屬性名稱;
屬性的訪問(wèn)和設(shè)置
KVC可以用來(lái)訪問(wèn)和設(shè)置實(shí)例變量的值。key:鍵,用于標(biāo)識(shí)實(shí)例變量
value:實(shí)例變量對(duì)應(yīng)的值
設(shè)置方式:[self setValue:aName forKey:@"name"]
等同于 self.name = aName;
訪問(wèn)方式: aString = [self valueForKey:@"name"]
等同于 aString = self.name;
修改值的Api:
setValue:forKey:
setValue:forKeyPath:
setValue:forUndefinedKey:
setValueForKeysWithDictionary:
獲取值的Api:
valueForKey:
valueForKeyPath:
valueForUndefinedKey:
注意事項(xiàng):
當(dāng)key不存在的時(shí)候,會(huì)執(zhí)行setValue:forUndefinedKey:系統(tǒng)默認(rèn)實(shí)現(xiàn)是拋出一個(gè)異常.
KVC使用
1,大致步驟:
(1)首先找到后面的key有沒(méi)有g(shù)et(set)方法,如果有,則直接調(diào)用
(2)如果沒(méi)有g(shù)et(set)方法,直接找_key這個(gè)屬性,如果沒(méi)有找到key,然后再去找key這個(gè)屬性,然后直接賦值
(3)如果key這個(gè)屬性也沒(méi)有,則報(bào)錯(cuò)重寫
2.設(shè)置的key最好不要加,因?yàn)橄到y(tǒng)會(huì)自動(dòng)的優(yōu)先地尋找_key這個(gè)屬性;
3.捕獲程序設(shè)置方法的異常:- (void)setValue:(id)value forUndefinedKey:(NSString *)key
捕獲程序訪問(wèn)方法的異常:- (id)valueForUndefinedKey:(NSString *)key
KVC鍵值查找
1、setValue:forKey:搜索方式
(1)首先搜索setKey:方法。(key指成員變量名,首字母大寫)。
(2)上面的setter方法沒(méi)找到,如果類方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的順序搜索成員名。(NSKeyValueCodingCatogery中實(shí)現(xiàn)的類方法,默認(rèn)實(shí)現(xiàn)為返回YES)
(3)如果沒(méi)有找到成員變量,調(diào)用setValue:forUnderfinedKey: 。
2、valueForKey:的搜索方式
(1)首先按getKey,key,isKey的順序查找getter方法,找到直接調(diào)用。如果是BOOL、int等內(nèi)建值類型,會(huì)做NSNumber的轉(zhuǎn)換。
(2)上面的getter沒(méi)找到,查找countOfKey、objectInKeyAtindex、KeyAtindexes格式的方法。如果countOfKey和另外兩個(gè)方法中的一個(gè)找到,那么就會(huì)返回一個(gè)可以響應(yīng)NSArray所有方法的代理集合的NSArray消息方法。
(3)還沒(méi)找到,查找countOfKey、enumeratorOfKey、memberOfKey格式的方法。如果這三個(gè)方法都找到,那么就返回一個(gè)可以響應(yīng)NSSet所有方法的代理集合。
(4)還是沒(méi)找到,如果類方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的順序搜索成員名。
(5)再?zèng)]找到,調(diào)用valueForUndefinedKey。
KVC的使用場(chǎng)景
// 可以訪問(wèn)并使用公私有屬性
- (void)kvcTest{
Boy *jack = [[Boy alloc] init];
[jack setValue:@"Jack" forKey:@"name"];
[jack setValue:@"18" forKey:@"age"];
// [jack setValue:@"183" forKeyPath:@"_height"];
[jack setValue:@"football" forKey:@"sport"];
NSLog(@"jack.name : %@",jack.name);
NSLog(@"jack.age : %ld",jack.age);
[jack logTest];
NSLog(@"jack.sport : %@",[jack valueForKey:@"height"]);
}
//復(fù)雜屬性賦值,嵌套賦值
//當(dāng) Boy 有一個(gè)其它類型屬性 Book 的屬性時(shí)候:
- (void)kvcTest1{
Boy *jack = [[Boy alloc] init];
jack.book = [[Book alloc] init];
[jack.book setValue:@"iOS" forKeyPath:@"bookName"]; //方式一
[jack setValue:@"C++" forKeyPath:@"book.bookName"]; //方式二
NSLog(@"book.bookName : %@",[jack valueForKeyPath:@"book.bookName"]);
}
// 字典轉(zhuǎn)模型
- (void)kvcTest2{
NSDictionary *dic = @{@"name":@"LiMing", @"eid" : @"南昌"};
Boy *jack = [[Boy alloc] init];
[jack setValuesForKeysWithDictionary:dic];
NSLog(@"model.name : %@",jack.name);
NSLog(@"model.num : %@",jack.city);
/**
第一種情況,model多一個(gè)屬性:這樣程序沒(méi)問(wèn)題,model多出的屬性會(huì)是nil
第二種情況,model少一個(gè)屬性:程序會(huì)崩潰
第三種情況,model的屬性名字和dic的key不匹配 : 程序會(huì)崩潰
第二種和第三種崩潰的解決辦法是重寫方法 -(void)setValue:(id)value forUndefinedKey:(NSString *)key
**/
}
//模型轉(zhuǎn)字典
- (void)kvcTest3{
NSDictionary *dic = @{@"name":@"LiMing", @"eid" : @"南昌"};
Boy *jack = [[Boy alloc] init];
[jack setValuesForKeysWithDictionary:dic];
NSDictionary *modelDic = [jack dictionaryWithValuesForKeys:@[@"name",@"age"]];
NSLog(@"modelDic : %@", modelDic);
}
KVO簡(jiǎn)述
定義:KVO的全稱 Key-Value Observing,俗稱“鍵值監(jiān)聽(tīng)”,可以用于監(jiān)聽(tīng)某個(gè)對(duì)象屬性值的改變。
蘋果KVO官網(wǎng)地址:
KVO的實(shí)現(xiàn)原理:
當(dāng)某個(gè)類的對(duì)象第一次被觀察時(shí),系統(tǒng)就會(huì)在運(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類,在這個(gè)派生類中重寫原類中被觀察屬性的 setter 方法 , 派生類在被重寫的 setter 方法實(shí)現(xiàn)真正的通知機(jī)制 (Person->NSKVONotifying_Person). 派生類重寫了 class 方法以 “ 欺騙 ” 外部調(diào)用者它就是起初的那個(gè)類。然后系統(tǒng)將這個(gè)對(duì)象的 isa 指針指向這個(gè)新誕生的派生類,因此這個(gè)對(duì)象就成為該派生類的對(duì)象了,因而在該對(duì)象上對(duì) setter 的調(diào)用就會(huì)調(diào)用重寫的 setter ,從而激活鍵值通知機(jī)制。此外,派生類還重寫了 dealloc 方法來(lái)釋放資源。
KVO的一般的使用場(chǎng)景:
- 實(shí)現(xiàn)上下拉刷新控件 content offset;
- webview 混合排版 content size;
- 監(jiān)聽(tīng)模型屬性實(shí)時(shí)更新UI;
帶著問(wèn)題探索
1.iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?(KVO的本質(zhì)是什么?)
答,當(dāng)一個(gè)對(duì)象使用了KVO監(jiān)聽(tīng),iOS系統(tǒng)會(huì)修改這個(gè)對(duì)象的isa指針, 改為指向一個(gè)全新的通過(guò)Runtime動(dòng)態(tài)創(chuàng)建的子類,子類擁有自己的set方法實(shí)現(xiàn), set方法實(shí)現(xiàn)內(nèi)部會(huì)按順序調(diào)用willChangeValueForKey方法、原來(lái)的setter方法實(shí)現(xiàn)、 didChangeValueForKey方法,而didChangeValueForKey方法內(nèi)部 又會(huì)調(diào)用監(jiān)聽(tīng)器的observeValueForKeyPath:ofObject:change:context:監(jiān)聽(tīng)方法。
2.如何手動(dòng)觸發(fā)KVO
答, 被監(jiān)聽(tīng)的屬性的值被修改時(shí),就會(huì)自動(dòng)觸發(fā)KVO。
如果想要手動(dòng)觸發(fā)KVO,則需要我們自己重寫+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key來(lái)禁用自動(dòng)監(jiān)聽(tīng),然后再調(diào)用willChangeValueForKey和
didChangeValueForKey方法即可在不改變屬性值的情況下手動(dòng)觸發(fā)KVO
,并且這兩個(gè)方法缺一不可。
范例代碼:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
- (void)setName:(NSString *)name{
if (_name!=name) {
[self willChangeValueForKey:@"name"];
_name=name;
[self didChangeValueForKey:@"name"];
}
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
KVO實(shí)現(xiàn)原理探索

上述代碼中可以看出,在添加監(jiān)聽(tīng)之后,age屬性的值在發(fā)生改變時(shí),就會(huì)通知到監(jiān)聽(tīng)者,執(zhí)行監(jiān)聽(tīng)者的observeValueForKeyPath方法
1. 探尋KVO底層實(shí)現(xiàn)原理
通過(guò)上述代碼我們發(fā)現(xiàn),一旦age屬性的值發(fā)生改變時(shí),就會(huì)通知到監(jiān)聽(tīng)者,并且我們知道賦值操作都是調(diào)用 set方法,我們可以來(lái)到Person類中重寫age的set方法,觀察是否是KVO在set方法內(nèi)部做了一些操作來(lái)通知監(jiān)聽(tīng)者。 我們發(fā)現(xiàn)即使重寫了set方法,p1對(duì)象和p2對(duì)象調(diào)用同樣的set方法,但是我們發(fā)現(xiàn)p1除了調(diào)用set方法之外還會(huì)另外執(zhí)行監(jiān)聽(tīng)器的observeValueForKeyPath方法。 說(shuō)明KVO在運(yùn)行時(shí)獲取對(duì)p1對(duì)象做了一些改變。相當(dāng)于在程序運(yùn)行過(guò)程中,對(duì)p1對(duì)象做了一些變化,使得p1對(duì)象在調(diào)用setage方法的時(shí)候可能做了一些額外的操作,所以問(wèn)題出在對(duì)象身上,兩個(gè)對(duì)象在內(nèi)存中肯定不一樣,兩個(gè)對(duì)象可能本質(zhì)上并不一樣。接下來(lái)來(lái)探索KVO內(nèi)部是怎么實(shí)現(xiàn)的。
2. KVO底層實(shí)現(xiàn)分析
在分析之前我們先來(lái)了解下實(shí)例對(duì)象、類對(duì)象以及元類對(duì)象的關(guān)系,如下圖所示

isa的指向
上圖中的Root class(class)是根類,即NSObject類。Root class(meta)就是NSObject的元類,即根元類。從圖中可知,isa的指向如下:

從下往上分別是:
實(shí)例對(duì)象 --> 類對(duì)象 --> 元類對(duì)象 --> 根元類對(duì)象
父類的實(shí)例對(duì)象 --> 父類的類對(duì)象 --> 父類的元類對(duì)象 --> 根元類對(duì)象
根類的實(shí)例對(duì)象 --> 根類的類對(duì)象 --> 根元類對(duì)象
根元類對(duì)象 --> 自己本身
類的繼承
從經(jīng)典圖中可知,繼承關(guān)系如下:

類對(duì)象 --> 父類的類對(duì)象 --> 根類的類對(duì)象 --> nil
元類對(duì)象 --> 父類的元類對(duì)象 --> 根元類對(duì)象 --> 根類的類對(duì)象 --> nil
從這個(gè)繼承關(guān)系可知,只有類對(duì)象和元類對(duì)象才有繼承關(guān)系,實(shí)例對(duì)象是沒(méi)有繼承關(guān)系的。且所有對(duì)象都是繼承于NSObject類對(duì)象,NSObject類對(duì)象則繼承于nil。
首先我們對(duì)上述代碼中添加監(jiān)聽(tīng)的地方打斷點(diǎn),看觀察一下,addObserver方法對(duì)p1對(duì)象做了什么處理?也就是說(shuō)p1對(duì)象在經(jīng)過(guò)addObserver方法之后發(fā)生了什么改變,我們通過(guò)打印isa指針如下圖所示

通過(guò)上圖我們發(fā)現(xiàn),p1對(duì)象執(zhí)行過(guò)addObserver操作之后,p1對(duì)象的isa指針由之前的指向類對(duì)象Person變?yōu)橹赶騈SKVONotifying_Person類對(duì)象,而p2對(duì)象沒(méi)有任何改變。也就是說(shuō)一旦p1對(duì)象添加了KVO監(jiān)聽(tīng)以后,其isa指針就會(huì)發(fā)生變化,因此set方法的執(zhí)行效果就不一樣了。
那么我們先來(lái)觀察p2對(duì)象在內(nèi)容中是如何存儲(chǔ)的,然后對(duì)比p2來(lái)觀察p1。
首先我們知道,p2在調(diào)用setAge方法的時(shí)候,首先會(huì)通過(guò)p2對(duì)象中的isa指針找到Person類對(duì)象,然后在類對(duì)象中找到setAge方法。然后找到方法對(duì)應(yīng)的實(shí)現(xiàn)。如下圖所示

但是剛才我們發(fā)現(xiàn)p1對(duì)象的isa指針在經(jīng)過(guò)KVO監(jiān)聽(tīng)之后已經(jīng)指向了NSKVONotifying_Person類對(duì)象,NSKVONotifying_Person其實(shí)是Person的子類,那么也就是說(shuō)其superclass指針是指向Person類對(duì)象的,NSKVONotifying_Person是runtime在運(yùn)行時(shí)生成的。那么p1對(duì)象在調(diào)用setAge方法的時(shí)候,肯定會(huì)根據(jù)p1的isa找到NSKVONotifying_Person,在NSKVONotifying_Person中找setAge的方法及實(shí)現(xiàn)。
經(jīng)過(guò)查閱資料我們可以了解到。
NSKVONotifying_Person中的setAge方法中其實(shí)調(diào)用了 Fundation框架中C語(yǔ)言函數(shù) _NSSetIntValueAndNotify,_NSSetIntValueAndNotify內(nèi)部做的操作相當(dāng)于,首先調(diào)用willChangeValueForKey 將要改變方法,之后調(diào)用父類的setAge方法對(duì)成員變量賦值,最后調(diào)用didChangeValueForKey已經(jīng)改變方法。didChangeValueForKey中會(huì)調(diào)用監(jiān)聽(tīng)器的監(jiān)聽(tīng)方法,最終來(lái)到監(jiān)聽(tīng)者的observeValueForKeyPath方法中。
那么如何驗(yàn)證KVO真的如上面所講的方式實(shí)現(xiàn)?
首先經(jīng)過(guò)之前打斷點(diǎn)打印isa指針,我們已經(jīng)驗(yàn)證了,在執(zhí)行添加監(jiān)聽(tīng)的方法時(shí),會(huì)將isa指針指向一個(gè)通過(guò)runtime創(chuàng)建的Person的子類NSKVONotifying_Person, 另外我們可以通過(guò)打印方法實(shí)現(xiàn)的地址來(lái)看一下p1和p2的setAge的方法實(shí)現(xiàn)的地址在添加KVO前后有什么變化。

我們發(fā)現(xiàn)在添加KVO監(jiān)聽(tīng)之前,p1和p2的setAge方法實(shí)現(xiàn)的地址相同,而經(jīng)過(guò)KVO監(jiān)聽(tīng)之后,p1的setAge方法實(shí)現(xiàn)的地址發(fā)生了變化,我們通過(guò)打印方法實(shí)現(xiàn)來(lái)看一下前后的變化發(fā)現(xiàn),確實(shí)如我們上面所講的一樣,p1的setAge方法的實(shí)現(xiàn)由Person類方法中的setAge方法轉(zhuǎn)換為了C語(yǔ)言的Foundation框架的_NSSetIntValueAndNotify函數(shù)。
Foundation框架中會(huì)根據(jù)屬性的類型,調(diào)用不同的方法。例如我們之前定義的int類型的age屬性,那么我們看到Foundation框架中調(diào)用的_NSSetIntValueAndNotify函數(shù)。那么我們把a(bǔ)ge的屬性類型變?yōu)閐ouble重新打印一遍

我們發(fā)現(xiàn)調(diào)用的函數(shù)變?yōu)榱薩NSSetDoubleValueAndNotify,那么這說(shuō)明Foundation框架中有許多此類型的函數(shù),通過(guò)屬性的不同類型調(diào)用不同的函數(shù)。 那么我們可以推測(cè)Foundation框架中還有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數(shù)。 我們可以找到Foundation框架文件,通過(guò)命令行查詢關(guān)鍵字找到相關(guān)函數(shù)

NSKVONotifying_Person內(nèi)部結(jié)構(gòu)是怎樣的?
首先我們知道,NSKVONotifying_Person作為Person的子類,其superclass指針指向Person類,并且NSKVONotifying_Person內(nèi)部一定對(duì)setAge方法做了單獨(dú)的實(shí)現(xiàn),那么NSKVONotifying_Person同Person類的差別可能就在于其內(nèi)存儲(chǔ)的對(duì)象方法及實(shí)現(xiàn)不同。
我們通過(guò)runtime分別打印Person類對(duì)象和NSKVONotifying_Person類對(duì)象內(nèi)存儲(chǔ)的對(duì)象方法

上述打印內(nèi)容如下:
通過(guò)上述代碼我們發(fā)現(xiàn)NSKVONotifying_Person中有4個(gè)對(duì)象方法。分別為setAge: class dealloc _isKVOA,那么至此我們可以畫出NSKVONotifying_Person的內(nèi)存結(jié)構(gòu)以及方法調(diào)用順序。

這里NSKVONotifying_Person重寫class方法是為了隱藏NSKVONotifying_Person。不被外界所看到。我們?cè)趐1添加過(guò)KVO監(jiān)聽(tīng)之后,分別打印p1和p2對(duì)象的class可以發(fā)現(xiàn)他們都返回Person。

如果NSKVONotifying_Person不重寫class方法,那么當(dāng)對(duì)象要調(diào)用class對(duì)象方法的時(shí)候就會(huì)一直向上找到NSObject,而NSObject的class的實(shí)現(xiàn)大致為返回自己isa指向的類,返回的p1的isa指向的類那么打印出來(lái)的類就是NSKVONotifying_Person,但是Apple不希望將NSKVONotifying_Person類暴露出來(lái),并且不希望我們知道NSKVONotifying_Person內(nèi)部實(shí)現(xiàn), 所以在內(nèi)部重寫了class類, 直接返回Person類,所以外界在調(diào)用p1的class對(duì)象方法時(shí),是Person類。這樣p1給外界的感覺(jué)p1還是Person類,并不知道NSKVONotifying_Person子類的存在。
那么我們可以猜測(cè)NSKVONotifying_Person內(nèi)重寫的class內(nèi)部實(shí)現(xiàn)大致為:
- (Class) class {
// 得到類對(duì)象,在找到類對(duì)象父類
return class_getSuperclass(object_getClass(self));
}
驗(yàn)證didChangeValueForKey:內(nèi)部會(huì)調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法.
我們?cè)赑erson類中重寫willChangeValueForKey:和didChangeValueForKey:方法,模擬他們的實(shí)現(xiàn)。
- (void)setAge:(int)age {
NSLog(@"setAge:");
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
再次運(yùn)行來(lái)查看didChangeValueForKey的方法內(nèi)運(yùn)行過(guò)程,通過(guò)打印內(nèi)容可以看到,確實(shí)在didChangeValueForKey方法內(nèi)部已經(jīng)調(diào)用了observer的observeValueForKeyPath:ofObject:change:context:方法。

KVO底層實(shí)現(xiàn)代碼
自己通過(guò)代碼來(lái)模擬KVO內(nèi)部實(shí)現(xiàn)監(jiān)聽(tīng)
#import "NSObject+EXKVO.h"
#import <objc/message.h>
@implementation NSObject (EXKVO)
- (void)EX_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 1.創(chuàng)建一個(gè)類 -- self.class 就是Person
NSString *oldname = NSStringFromClass(self.class);
NSString *newNem = [@"NSKVONotifying_" stringByAppendingString:oldname];
Class myclass = objc_allocateClassPair(self.class, newNem.UTF8String, 0);
// 注冊(cè)類
objc_registerClassPair(myclass);
// 2.重寫子類set方法 -- 所謂的重寫就是給子類添加這個(gè)方法 setAge,因?yàn)樽宇悰](méi)有父類的setAge方法
/* class :給那個(gè)類添加方法
*sel:方法編號(hào)
*imp :方法實(shí)現(xiàn)(函數(shù)指針)
*type :返回值類型
*/
class_addMethod(myclass, @selector(setAge:), (IMP)setAge, "v@:@");
// 3.修改isa指針
object_setClass(self, myclass);
// 4.將觀察保存到當(dāng)前對(duì)象
objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_RETAIN);
}
void setAge(id self,SEL _cmd,int newAge){
NSLog(@"來(lái)了--%d",newAge);
// 調(diào)用父類的setName方法
Class class = [self class];
object_setClass(self, class_getSuperclass(class));//改成父類
objc_msgSend(self,@selector(setAge:),newAge);//發(fā)送消息給父類
// 觀察者
id observer = objc_getAssociatedObject(self, @"observer");
if (observer) {
objc_msgSend(observer, @selector(lg_observeValueForKeyPath:ofObject:newValue:),@"age",self,@{@"new:":@(newAge),@"kind:":@"1"});
}
// 改回子類
object_setClass(self, class);
}
@end
KVO 和線程
一個(gè)需要注意的地方是,KVO 行為是同步的,并且發(fā)生與所觀察的值發(fā)生變化的同樣的線程上。沒(méi)有隊(duì)列或者 Run-loop 的處理。手動(dòng)或者自動(dòng)調(diào)用 -didChange... 會(huì)觸發(fā) KVO 通知。
所以,當(dāng)我們?cè)噲D從其他線程改變屬性值的時(shí)候我們應(yīng)當(dāng)十分小心,除非能確定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來(lái)說(shuō),我們不推薦把 KVO 和多線程混起來(lái)。如果我們要用多個(gè)隊(duì)列和線程,我們不應(yīng)該在它們互相之間用 KVO。
KVO 是同步運(yùn)行的這個(gè)特性非常強(qiáng)大,只要我們?cè)趩我痪€程上面運(yùn)行(比如主隊(duì)列 main queue),KVO 會(huì)保證下列兩種情況的發(fā)生:
首先,如果我們調(diào)用一個(gè)支持 KVO 的 setter 方法,如下所示:
self.exchangeRate = 2.345;
KVO 能保證所有 exchangeRate 的觀察者在 setter 方法返回前被通知到。
其次,如果某個(gè)鍵被觀察的時(shí)候附上了 NSKeyValueObservingOptionPrior 選項(xiàng),直到 -observe... 被調(diào)用之前, exchangeRate 的 accessor 方法都會(huì)返回同樣的值。
KVO的優(yōu)缺點(diǎn)
一、KVO優(yōu)點(diǎn)
1.能夠提供一種簡(jiǎn)單的方法實(shí)現(xiàn)兩個(gè)對(duì)象間的同步。例如:model和view之間同步;
2.能夠?qū)Ψ俏覀儎?chuàng)建的對(duì)象,即內(nèi)部對(duì)象的狀態(tài)改變作出響應(yīng),而且不需要改變內(nèi)部對(duì)象;
3.能夠提供觀察的屬性的最新值以及先前值;
4.用key paths來(lái)觀察屬性,因此也可以觀察嵌套對(duì)象;
二、KVO缺點(diǎn):
1.我們觀察的屬性必須使用strings來(lái)定義。因此在編譯器不會(huì)出現(xiàn)警告以及檢查;
2.對(duì)屬性重構(gòu)將導(dǎo)致我們的觀察代碼不再可用;
3.復(fù)雜的“IF”語(yǔ)句要求對(duì)象正在觀察多個(gè)值。這是因?yàn)樗械挠^察代碼通過(guò)一個(gè)方法來(lái)指向;
4.當(dāng)釋放觀察者時(shí)不需要移除觀察者。