iOS開(kāi)發(fā) 之 KVO底層原理

首先來(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。

image.png

混合型語(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類型的,所以不能像其賦值其他類型的值


image.png

這說(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í)的類型
image.png

此時(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)地址:

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i

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)景:

  1. 實(shí)現(xiàn)上下拉刷新控件 content offset;
  2. webview 混合排版 content size;
  3. 監(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)原理探索

image1.png

上述代碼中可以看出,在添加監(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)系,如下圖所示


image.png

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

image.png

從下往上分別是:
實(shí)例對(duì)象 --> 類對(duì)象 --> 元類對(duì)象 --> 根元類對(duì)象
父類的實(shí)例對(duì)象 --> 父類的類對(duì)象 --> 父類的元類對(duì)象 --> 根元類對(duì)象
根類的實(shí)例對(duì)象 --> 根類的類對(duì)象 --> 根元類對(duì)象
根元類對(duì)象 --> 自己本身

類的繼承
從經(jīng)典圖中可知,繼承關(guān)系如下:

image.png

類對(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指針如下圖所示


image2.png

通過(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)。如下圖所示


image3.png

但是剛才我們發(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前后有什么變化。

image4.png

我們發(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重新打印一遍


image5.png

我們發(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ù)


image6.png

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ì)象方法

image7.png

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


image8.png

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


image9.png

如果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:方法。


image10.png

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í)不需要移除觀察者。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容