iOS開發·KVO用法,原理與底層實現: runtime模擬實現KVO監聽機制(Blcok及Delgate方式)

本文Demo傳送門:CMKVODemo

摘要:這篇文章首先介紹KVO的基本用法,接著探究 KVO (Key-Value Observing) 實現機制,并利用 runtime 模擬實現 KVO的監聽機制:一種Block方式回調,一種Delegate回調。同時,本文也會總結KVO實現過程中與 runtime 相關的API用法。

1. KVO理論基礎

1.1 KVO的基本用法

步驟

? 注冊觀察者,實施監聽

[self.person addObserver:self
              forKeyPath:@"age"
                 options:NSKeyValueObservingOptionNew
                 context:nil];

? 回調方法,在這里處理屬性發生的變化

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
  //...實現監聽處理
}

? 移除觀察者

[self removeObserver:self forKeyPath:@“age"];

綜合例子

//添加觀察者
_person = [[Person alloc] init];
[_person addObserver:self
          forKeyPath:@"age"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:nil];
//KVO回調方法
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context 
{
    NSLog(@"%@對象的%@屬性改變了,change字典為:%@",object,keyPath,change);
    NSLog(@"屬性新值為:%@",change[NSKeyValueChangeNewKey]);
    NSLog(@"屬性舊值為:%@",change[NSKeyValueChangeOldKey]);
}
//移除觀察者
- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"age"];
}

利用了KVO實現鍵值監聽的第三方框架

AFNetworkingMJRresh

1.2 KVO的實現原理

KVO 是 Objective-C 對 觀察者模式(Observer Pattern)的實現。當被觀察對象的某個屬性發生更改時,觀察者對象會獲得通知。有意思的是,你不需要給被觀察的對象添加任何額外代碼,就能使用 KVO 。這是怎么做到的?

KVO 的實現也依賴于 Objective-C 強大的 Runtime 。Apple 的文檔有簡單提到過 KVO 的實現。Apple 的文檔唯一有用的信息是:被觀察對象的 isa 指針會指向一個中間類,而不是原來真正的類。Apple 并不希望過多暴露 KVO 的實現細節。

不過,要是你用 runtime 提供的方法去深入挖掘,所有被掩蓋的細節都會原形畢露。Mike Ash 早在 2009 年就做了這么個探究,了解更多 點這里

簡單概述下 KVO 的實現:

當你觀察一個對象時,一個新的類會動態被創建。這個類繼承自該對象的原本的類,并重寫了被觀察屬性的 setter 方法。自然,重寫的 setter 方法會負責在調用原 setter方法之前和之后,通知所有觀察對象值的更改。最后把這個對象的 isa 指針 ( isa 指針告訴 Runtime 系統這個對象的類是什么 ) 指向這個新創建的子類,對象就神奇的變成了新創建的子類的實例。

原來,這個中間類,繼承自原本的那個類。不僅如此,Apple 還重寫了 -class 方法,企圖欺騙我們這個類沒有變,就是原本那個類。更具體的信息,去跑一下 Mike Ash 的那篇文章里的代碼就能明白,這里就不再重復。

1.3 KVO的不足

KVO 很強大,沒錯。知道它內部實現,或許能幫助更好地使用它,或在它出錯時更方便調試。但官方實現的 KVO 提供的 API 實在不怎么樣。

比如,你只能通過重寫 -observeValueForKeyPath:ofObject:change:context:方法來獲得通知。想要提供自定義的 selector ,不行;想要傳一個 block ,門都沒有。而且你還要處理父類的情況 - 父類同樣監聽同一個對象的同一個屬性。但有時候,你不知道父類是不是對這個消息有興趣。雖然 context 這個參數就是干這個的,也可以解決這個問題 - 在 -addObserver:forKeyPath:options:context: 傳進去一個父類不知道的 context。但總覺得框在這個 API 的設計下,代碼寫的很別扭。至少至少,也應該支持 block 吧。

有不少人都覺得官方 KVO 不好使的。Mike Ash 的 Key-Value Observing Done Right,以及獲得不少分享討論的 KVO Considered Harmful 都把 KVO 拿出來吊打了一番。所以在實際開發中 KVO 使用的情景并不多,更多時候還是用 Delegate 或 NotificationCenter。

2. Block實現KVO

2.1 模擬實現

注意:以下都是同一個文件:NSObject+Block_KVO.m中寫的

  • 導入頭文件,并定義兩個靜態變量
#import "NSObject+Block_KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>

//as prefix string of kvo class
static NSString * const kCMkvoClassPrefix_for_Block = @"CMObserver_";
static NSString * const kCMkvoAssiociateObserver_for_Block = @"CMAssiociateObserver";
  • 暴露給調用者為被觀察對象添加KVO方法
- (void)CM_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CM_ObservingHandler)observedHandler
{
    //step 1 get setter method, if not, throw exception
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %@", self] userInfo: nil];
        return;
    }
    
    //自己的類作為被觀察者類
    Class observedClass = object_getClass(self);
    NSString * className = NSStringFromClass(observedClass);
    
    //如果被監聽者沒有CMObserver_,那么判斷是否需要創建新類
    if (![className hasPrefix: kCMkvoClassPrefix_for_Block]) {
        //【代碼①】
        observedClass = [self createKVOClassWithOriginalClassName: className];
        //【API注解①】
        object_setClass(self, observedClass);
    }
    
    //add kvo setter method if its class(or superclass)hasn't implement setter
    if (![self hasSelector: setterSelector]) {
        const char * types = method_getTypeEncoding(setterMethod);
        //【代碼②】
        class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);
    }
    
    
    //add this observation info to saved new observer
    //【代碼③】
    CM_ObserverInfo_for_Block * newInfo = [[CM_ObserverInfo_for_Block alloc] initWithObserver: observer forKey: key observeHandler: observedHandler];
    
    //【代碼④】【API注解③】
    NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge void *)kCMkvoAssiociateObserver_for_Block);
    
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge void *)kCMkvoAssiociateObserver_for_Block, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject: newInfo];
}
  • 其中【代碼①】的意思是,被觀察的類如果是被觀察對象本來的類,那么,就要專門依據本來的類新建一個新的子類,區分是否這個子類的標記是帶有kCMkvoClassPrefix_for_Block的前綴。怎樣新建一個子類?代碼如下所示:
- (Class)createKVOClassWithOriginalClassName: (NSString *)className
{
    NSString * kvoClassName = [kCMkvoClassPrefix stringByAppendingString: className];
    Class observedClass = NSClassFromString(kvoClassName);
    
    if (observedClass) { return observedClass; }
    
    //創建新類,并且添加CMObserver_為類名新前綴
    Class originalClass = object_getClass(self);
    //【API注解②】
    Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
    
    //獲取監聽對象的class方法實現代碼,然后替換新建類的class實現
    Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
    const char * types = method_getTypeEncoding(classMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)kvo_Class, types);
    objc_registerClassPair(kvoClass);
    return kvoClass;
}
  • 另外【代碼②】的意思是,將原來的setter方法替換一個新的setter方法(這就是runtime的黑魔法,Method Swizzling)。那么新的setter方法又是什么呢?如下所示:
#pragma mark -- Override setter and getter Methods
static void KVO_setter(id self, SEL _cmd, id newValue)
{
    NSString * setterName = NSStringFromSelector(_cmd);
    NSString * getterName = getterForSetter(setterName);
    if (!getterName) {
        @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil];
        return;
    }
    
    id oldValue = [self valueForKey: getterName];
    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    [self willChangeValueForKey: getterName];
    void (*objc_msgSendSuperKVO)(void *, SEL, id) = (void *)objc_msgSendSuper;
    objc_msgSendSuperKVO(&superClass, _cmd, newValue);
    [self didChangeValueForKey: getterName];
    
    //獲取所有監聽回調對象進行回調
    NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge const void *)kCMkvoAssiociateObserver_for_Block);
    for (CM_ObserverInfo_for_Block * info in observers) {
        if ([info.key isEqualToString: getterName]) {
            dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                info.handler(self, getterName, oldValue, newValue);
            });
        }
    }
}
  • 【代碼③】是新建一個觀察者類。這個類的實現寫在同一個class,相當于導入一個類:CM_ObserverInfo_for_Block。這個類的作用是觀察者,并在初始化的時候負責調用者傳過來的Block回調。如下,self.handler = handler;即負責回調。
@interface CM_ObserverInfo_for_Block : NSObject

@property (nonatomic, weak) NSObject * observer;
@property (nonatomic, copy) NSString * key;
@property (nonatomic, copy) CM_ObservingHandler handler;

@end

@implementation CM_ObserverInfo_for_Block

- (instancetype)initWithObserver: (NSObject *)observer forKey: (NSString *)key observeHandler: (CM_ObservingHandler)handler
{
    if (self = [super init]) {
        
        _observer = observer;
        self.key = key;
        self.handler = handler;
    }
    return self;
}

@end
  • 【代碼④】的作用是,以及已知的“屬性名”,類型為NSString的靜態變量kCMkvoAssiociateObserver_for_Block來獲取這個“屬性”觀察者數組(這個其實并不是真正意義的屬性,屬于runtime關聯對象的知識范疇,可理解成 觀察者數組 這樣一個屬性)。其中,關于(__bridge void *)的知識后面會講到。

調用者:利用上面的API為被觀察者添加KVO

  • VC調用API
#import "NSObject+Block_KVO.h"
//...........

- (void)viewDidLoad {
    [super viewDidLoad];
    
    ObservedObject * object = [ObservedObject new];
    object.observedNum = @8;
    
#pragma mark - Observed By Block
    [object CM_addObserver: self forKey: @"observedNum" withBlock: ^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
        NSLog(@"Value had changed yet with observing Block");
        NSLog(@"oldValue---%@",oldValue);
        NSLog(@"newValue---%@",newValue);
    }];
    
    object.observedNum = @10;
}

2.2 runtime關鍵API解析

【API注解①】:object_setClass

我們可以在運行時創建新的class,這個特性用得不多,但其實它還是很強大的。你能通過它創建新的子類,并添加新的方法。

但這樣的一個子類有什么用呢?別忘了Objective-C的一個關鍵點:object內部有一個叫做isa的變量指向它的class。這個變量可以被改變,而不需要重新創建。然后就可以添加新的ivar和方法了。可以通過以下命令來修改一個object的class

object_setClass(myObject, [MySubclass class]);

這可以用在Key Value Observing。當你開始observing an object時,Cocoa會創建這個object的class的subclass,然后將這個object的isa指向新創建的subclass。

【API注解②】:objc_allocateClassPair

objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, 
                       size_t extraBytes) 
  • 看起來一切都很簡單,運行時創建類只需要三步:
    1、為"class pair"分配空間(使用objc_allocateClassPair).
    2、為創建的類添加方法和成員(上例使用class_addMethod添加了一個方法)。
    3、注冊你創建的這個類,使其可用(使用objc_registerClassPair)。

為什么這里1和3都說到pair,我們知道pair的中文意思是一對,這里也就是一對類,那這一對類是誰呢?他們就是Class、MetaClass。

  • 需要配置的參數為:
    1、第一個參數:作為新類的超類,或用Nil來創建一個新的根類。
    2、第二個參數:新類的名稱
    3、第三個參數:一般傳0

【API注解③】:(__bridge void *)

在 ARC 有效時,通過 (__bridge void *)轉換 id 和 void * 就能夠相互轉換。為什么轉換?這是因為objc_getAssociatedObject的參數要求的。先看一下它的API:

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

可以知道,這個“屬性名”的key是必須是一個void *類型的參數。所以需要轉換。關于這個轉換,下面給一個轉換的例子:

id obj = [[NSObject alloc] init];

void *p = (__bridge void *)obj;
id o = (__bridge id)p;

關于這個轉換可以了解更多:ARC 類型轉換:顯示轉換 id 和 void *

當然,如果不通過轉換使用這個API,就需要這樣使用:

  • 方式1:
objc_getAssociatedObject(self, @"AddClickedEvent");
  • 方式2:
static const void *registerNibArrayKey = &registerNibArrayKey;
NSMutableArray *array = objc_getAssociatedObject(self, registerNibArrayKey);
  • 方式3:
static const char MJErrorKey = '\0';
objc_getAssociatedObject(self, &MJErrorKey);
  • 方式4:
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    //省略
}

其中objc_property_t是runtime的類型

typedef struct objc_property *objc_property_t;

2.3 runtime其它API解析

剩下的就是runtime的比較常見API了,這里就不按照上面代碼的順序的講解了。這里只做按runtime的知識范疇將這些API做一個分類:

  • runtime:關聯對象相關API
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
  • runtime:方法替換相關API
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
object_getClass(id _Nullable obj) 
Method class_getInstanceMethod(Class cls, SEL name);
const char * method_getTypeEncoding(Method m);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
  • runtime:消息機制相關API
objc_msgSendSuper
  • KVO
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

3. 拓展:Delegate實現KVO

注意:以下都是同一個文件:NSObject+Block_Delegate.m中寫的

  • 觀察類CM_ObserverInfo需要改一個屬性,將Block改為一個Delegate。
@interface CM_ObserverInfo : NSObject

@property (nonatomic, weak) NSObject * observer;
@property (nonatomic, copy) NSString * key;
//修改這里
@property (nonatomic, assign) id <ObserverDelegate> observerDelegate;

@end
  • 同樣,觀察類CM_ObserverInfo初始化的時候也需要相應初始這個新屬性。
@implementation CM_ObserverInfo

- (instancetype)initWithObserver: (NSObject *)observer forKey: (NSString *)key
{
    if (self = [super init]) {
        
        _observer = observer;
        self.key = key;
        //修改這里
        self.observerDelegate = (id<ObserverDelegate>)observer;
    }
    return self;
}
@end

  • 暴露給調用者為被觀察對象添加KVO方法:不需要傳Block了。
#pragma mark -- NSObject Category(KVO Reconstruct)
@implementation NSObject (Block_KVO)

- (void)CM_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CM_ObservingHandler)observedHandler
{
   //...省略
   //add this observation info to saved new observer
   //修改這里
   CM_ObserverInfo * newInfo = [[CM_ObserverInfo alloc] initWithObserver: observer forKey: key];
   //...省略
}

調用者:利用上面的API為被觀察者添加KVO

  • VC調用API
#import "NSObject+Delegate_KVO.h"
//...........

- (void)viewDidLoad {
    [super viewDidLoad];
    
    ObservedObject * object = [ObservedObject new];
    object.observedNum = @8;
    
#pragma mark - Observed By Delegate
    [object CM_addObserver: self forKey: @"observedNum"];
    
    object.observedNum = @10;
}
  • VC實現代理方法
#pragma mark - ObserverDelegate
-(void)CM_ObserveValueForKeyPath:(NSString *)keyPath ofObject:(id)object oldValue:(id)oldValue newValue:(id)newValue{
    NSLog(@"Value had changed yet with observing Delegate");
    NSLog(@"oldValue---%@",oldValue);
    NSLog(@"newValue---%@",newValue);
}

4. runtime了解更多

筆者另外寫了runtime的原理與實踐。如果想了解runtime的更多知識,可以選擇閱讀這些文章:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容