【OC底層】KVO原理

来源:https://www.cnblogs.com/xgao/archive/2018/11/02/9896769.html
-Advertisement-
Play Games

KVO的原理是什麼?底層是如何實現的? 我們可以通過代碼去探索一下。 創建自定義類:XGPerson 我們的思路就是看看對象添加KVO之前和之後有什麼變化,是否有區別,代碼如下: 輸出: 從上面可以看出,object_getClass 和 class 方式分別獲取到的 類對象竟然不一樣,在對象添加了 ...


KVO的原理是什麼?底層是如何實現的?

我們可以通過代碼去探索一下。

創建自定義類:XGPerson

@interface XGPerson : NSObject

@property (nonatomic,assign) int age;

@property (nonatomic,copy) NSString* name;

@end

我們的思路就是看看對象添加KVO之前和之後有什麼變化,是否有區別,代碼如下:

@interface ViewController ()

@property (strong, nonatomic) XGPerson *person1;
@property (strong, nonatomic) XGPerson *person2;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[XGPerson alloc]init];
    self.person2 = [[XGPerson alloc]init];
    self.person1.age = 1;
    self.person2.age = 10;

    // 添加監聽之前,獲取類對象,通過兩種方式分別獲取 p1 和 p2的類對象
    NSLog(@"before getClass--->> p1:%@  p2:%@",object_getClass(self.person1),object_getClass(self.person2));
    NSLog(@"before class--->> p1:%@  p2:%@",[self.person1 class],[self.person2 class]);
        
    // 添加KVO監聽
    NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];

    // 添加監聽之後,獲取類對象
    NSLog(@"after getClass--->> p1:%@  p2:%@",object_getClass(self.person1),object_getClass(self.person2));
    NSLog(@"after class--->> p1:%@  p2:%@",[self.person1 class],[self.person2 class]);
}

輸出:

2018-11-02 15:16:13.276167+0800 KVO原理[4083:170379] before getClass--->> p1:XGPerson  p2:XGPerson
2018-11-02 15:16:13.276271+0800 KVO原理[4083:170379] before class--->> p1:XGPerson  p2:XGPerson


2018-11-02 15:16:13.276712+0800 KVO原理[4083:170379] after getClass--->> p1:NSKVONotifying_XGPerson  p2:XGPerson
2018-11-02 15:16:13.276815+0800 KVO原理[4083:170379] after class--->> p1:XGPerson  p2:XGPerson

從上面可以看出,object_getClass 和 class 方式分別獲取到的 類對象竟然不一樣,在對象添加了KVO之後,使用object_getClass的方式獲取到的對象和我們自定義的對象不一樣,而是NSKVONotifying_XGPerson,可以懷疑 class 方法可能被篡改了.

最終發現NSKVONotifying_XGPerson是使用Runtime動態創建的一個類,是XGPerson的子類.

看完對象,接下來我們來看下屬性,就是被我們添加了KVO的屬性age,我們要觸發KVO回調就是去給age設置個值,那它肯定就是調用setAge這個方法.

下麵監聽下這個方法在被添加了KVO之後有什麼不一樣.

    NSLog(@"person1添加KVO監聽之前 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);


    // 添加KVO監聽
    NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];

    NSLog(@"person1添加KVO監聽之後 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

輸出:

2018-11-02 15:16:13.276402+0800 KVO原理[4083:170379] person1添加KVO監聽之前 - 0x10277c3e0 0x10277c3e0

2018-11-02 15:16:17.031319+0800 KVO原理[4083:170379] person1添加KVO監聽之後 - 0x102b21f8e 0x10277c3e0

看輸出我們能發現,在監聽之前兩個對象的方法所指向的物理地址都是一樣的,添加監聽後,person1對象的setAge方法就變了,這就說明一個問題,這個方法的實現變了,我們再通過Xcode斷點調試列印看下到底調用什麼方法

斷點後,在調試器中使用 po 列印對象

(lldb) po [self.person1 methodForSelector:@selector(setAge:)]

  (Foundation`_NSSetIntValueAndNotify)

 

(lldb) po [self.person2 methodForSelector:@selector(setAge:)]

  (KVO原理`-[XGPerson setAge:] at XGPerson.m:13)

通過輸出結果可以發現person1的setAge已經被重寫了,改成了調用Foundation框架中C語言寫的 _NSSetIntValueAndNotify 方法,

還有一點,監聽的屬性值類型不同,調用的方法也不同,如果是NSString的,就會調用 _NSSetObjectValueAndNotify 方法,會有幾種類型

大家都知道蘋果的代碼是不開源的,所以我們也不知道 _NSSetIntValueAndNotify 這個方法裡面到底調用了些什麼,那我們可以試著通過其它的方式去猜一下裡面是怎麼調用的。

KVO底層的調用順序

我們先對我們自定義的類下手,重寫下類裡面的幾個方法:

類實現:

#import "XGPerson.h"

@implementation XGPerson

- (void)setAge:(int)age{
    
    _age = age;
    NSLog(@"XGPerson setAge");
}

- (void)willChangeValueForKey:(NSString *)key{
    
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key{
    
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}

重寫上面3個方法來監聽我們的值到底是怎麼被改的,KVO的通知回調又是什麼時候調用的

我們先設置KVO的監聽回調

// KVO監聽回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"監聽到%@的%@屬性值改變了 - %@", object, keyPath, change[@"new"]);
}

 

我們直接修改person1的age值,觸發一下KVO,輸出如下:

2018-11-02 15:38:24.788395+0800 KVO原理[4298:186471] willChangeValueForKey
2018-11-02 15:38:24.788573+0800 KVO原理[4298:186471] XGPerson setAge
2018-11-02 15:38:24.788696+0800 KVO原理[4298:186471] didChangeValueForKey - begin
2018-11-02 15:38:24.788893+0800 KVO原理[4298:186471] 監聽到<XGPerson: 0x60400022f420>的age屬性值改變了 - 2
2018-11-02 15:38:24.789014+0800 KVO原理[4298:186471] didChangeValueForKey - end

從結果中可以看出KVO是在哪個時候觸發回調的,就是在 didChangeValueForKey 這個方法裡面觸發的

NSKVONotifying_XGPerson子類的研究

接下來我們再來研究下之前上面說的那個 NSKVONotifying_XGPerson 子類,可能大家會很好奇這裡面到底有些什麼東西,下麵我們就使用runtime將這個子類的所有方法都列印出來

我們先寫一個方法用來列印一個類對象的所有方法,代碼如下:

// 獲取一個對象的所有方法
- (void)getMehtodsOfClass:(Class)cls{
    
    unsigned int count;
    Method* methods = class_copyMethodList(cls, &count);
    
    NSMutableString* methodList = [[NSMutableString alloc]init];
    for (int i=0; i < count; i++) {
        Method method = methods[i];
        NSString* methodName = NSStringFromSelector(method_getName(method));
        [methodList appendString:[NSString stringWithFormat:@"| %@",methodName]];
    }
    NSLog(@"%@對象-所有方法:%@",cls,methodList);

   // C語言的函數是需要手動釋放記憶體的喔
   free(methods);

}

下麵使用這個方法列印下person1的所有方法,順便我們再對比下 object_getClass 和 class

    // 一定要使用 object_getClass去獲取類對象,不然獲取到的不是真正的那個子類,而是XGPperson這個類
    [self getMehtodsOfClass:object_getClass(self.person1)];

   // 使用 class屬性獲取的類對象 [self getMehtodsOfClass:[self.person1 class]];

輸出:

2018-11-02 15:45:07.918209+0800 KVO原理[4369:190437] NSKVONotifying_XGPerson對象-所有方法:| setAge:| class| dealloc| _isKVOA
2018-11-02 15:45:07.918371+0800 KVO原理[4369:190437] XGPerson對象-所有方法:| .cxx_destruct| name| willChangeValueForKey:| didChangeValueForKey:| setName:| setAge:| age

通過結果可以看出,這個子類裡面就是重寫了3個父類方法,還有一個私有的方法,我們XGPerson這個類還有一個name屬性,這裡為什麼沒有setName呢?因為我們沒有給 name 屬性添加KVO,所以就不會重寫它,這裡面確實有那個 class 方法,確實被重寫了,所以當我們使用 [self.person1 class] 的方式的時候它內部怎麼返回的就清楚了。

NSKVONotifying_XGPerson 偽代碼實現

通過上面的研究,我們大概也能清楚NSKVONotifying_XGPerson這個子類裡面是如何實現的了,大概的代碼如下:

頭文件:

@interface NSKVONotifying_XGPerson : XGPerson

@end

實現:

#import "NSKVONotifying_XGPerson.h"

// KVO的原理偽代碼實現
@implementation NSKVONotifying_XGPerson

- (void)setAge:(int)age{
    
    _NSSetIntValueAndNotify();
}

- (void)_NSSetIntValueAndNotify{
    
    // KVO的調用順序
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    // KVO會在didChangeValueForKey裡面調用age屬性變更的通知回調
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key{
// 通知監聽器,某某屬性值發生了改變 [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil]; } // 會重寫class返回父類的class // 原因:1.為了隱藏這個動態的子類 2.為了讓開發者不那麼迷惑 - (Class)class{ return [XGPerson class]; } - (void)dealloc{ // 回收工作 } - (BOOL)_isKVOA{ return YES; }

 

如何手動調用KVO

其實通過上面的代碼大家已經知道了KVO是怎麼觸發的了,那怎麼手動調用呢?很簡單,只要調用兩個方法就行了,如下:

 

    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];

但是上面說調用順序的時候,好像明明KVO是在 didChangeVlaueForKey 裡面調用的,為什麼還要調用 willChangeVlaueForKey呢?

那是因為KVO調用的時候會去判斷這個對象有沒有調用 willChangeVlaueForKey 只有調用了這個之後,再調用 didChangeVlaueForKey 才能真正觸發KVO

總結

KVO是通過runtime機制動態的給要添加KVO監聽的對象創建一個子類,並且讓instance對象的isa指向這個全新的子類.

當修改instance對象的屬性時,會調用Foundation的_NSSetXXXValueAndNotify函數,順序如下:

  • willChangeValueForKey:
  • 父類原來的setter
  • didChangeValueForKey:

didChangeValueForKey 內部會觸發監聽器(Oberser)的監聽方法( observeValueForKeyPath:ofObject:change:context:)

通過這個子類重寫一些父類的方法達到觸發KVO回調的目的.

 

補充

KVO是使用了典型的發佈訂閱者設計模式實現事件回調的功能,多個訂閱者,一個發佈者,簡單的實現如下:

1> 訂閱者向發佈者進行訂閱.

2> 發佈者將訂閱者信息保存到一個集合中.

3> 當觸發事件後,發佈者就遍歷這個集合分別調用之前的訂閱者,從而達到1對多的通知.

 

以上已全部完畢,如有什麼不正確的地方大家可以指出~~ ^_^ 下次再見~~

  


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在安裝和測試HBase之前,我們有必要先瞭解一下HBase是什麼 我們可以通過下麵的資料對其有一定的瞭解: HBase 官方文檔中文版 HBase 深入淺出 我想把我知道的分享給大家,方便大家交流。 ...
  • 作者:天山老妖S 鏈接:http://blog.51cto.com/9291927 一、自定義函數簡介 自定義函數(user-defined function UDF)是一種對MySQL擴展的途徑,其用法和內置函數相同。 自定義函數的兩個必要條件: A、參數 B、返回值(必須有)。函數可以返回任意類 ...
  • 環境 操作系統:Ubuntu 18.04 MongoDB: 4.0.3 伺服器 首先部署3台伺服器,1台主節點 + 2台從節點 3台伺服器的內容ip分別是: 10.140.0.5 (主節點) 10.140.0.6 (從節點01) 10.140.0.7 (從節點02) 安裝MongoDB 接下來,需要 ...
  • mysql 8.0.13預設有一個data文件夾,這個文件夾得刪了,不然安裝服務時候會有日誌文件提示報錯: Failed to find valid data directory. Data Dictionary initialization failed. 還有,要在mysql文件夾里新建個my. ...
  • array 結構 (1)語法:array(val1,val2,val3,…) 操作類型:array array類型的數據可以通過'數組名[index]'的方式訪問,index從0開始: (2)建表: create external table temp.array_20181101_v2 ( did ...
  • Mysql 二進位安裝方法 下載mysql https://dev.mysql.com/downloads/mysql/ 1.解壓包 tar xf mysql-5.7.24-linux-glibc2.12-x86_64.tar.gz 2.實際生產環境 mv mysql-5.7.24-linux-gl ...
  • 修改存儲過程註意事項 只能修改先前在 SQL Server 中通過執行 CREATE PROCEDURE 語句創建的過程。 Transact-SQL 存儲過程修改為 CLR 存儲過程,反之亦然。 ALTER PROCEDURE 不會更改許可權,也不影響相關的存儲過程或觸發器。 但是,當修改存儲過程時, ...
  • 前言 前面介紹了redis持久化和容災備份,這篇會介紹redis主從複製和redis持久化在主從複製中的一些應用。因為本人沒有那麼多伺服器或機器,所以這裡主要介紹下如何在docker容器中搭建主從複製以及搭建過程中遇到的一些問題。關於redis的深入講解,這邊博客《深入學習Redis(3):主從複製 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...