Objective-C runtime機制(10)——KVO的實現機制

使用KVO

自動觸發KVO

在平日代碼中,我們通過KVO來監視實例某個屬性的變化。
比如,我們要監視Student 的 age屬性,可以這麼做:

@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@end

@interface ViewController ()

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Student *std = [Student new];
    std.name = @"Tom";
 	[std addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
 }

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
	...
}
@end

我們使用KVO需要遵循以下步驟:

  1. 調用addObserver:forKeyPath:options:context: 方法來註冊觀察者,觀察者可以接收到KeyPath對應屬性的修改通知
  2. 當觀察的屬性發生變化時,系統會在observeValueForKeyPath:ofObject:change:context:方法中回調觀察者
  3. 當觀察者不需要監聽變化是,需要調用removeObserver:forKeyPath:KVO移除。需要注意的是,在觀察者被釋放前,必須要調用removeObserver:forKeyPath:將其移除,否則會crash。

手動觸發KVO

當我們設置了觀察者後,當被觀察的keyPath對應的setter方法調用後,則會自動的觸發KVO的回調函數。那麼,有時候我們想要控制這種自動觸發的機制,該怎麼辦呢?你可以重寫如下方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

automaticallyNotifiesObserversForKey方法聲明在NSObject的Category NSObject(NSKeyValueObservingCustomization)中。

除了在setter方法中,有時候我們想主動觸發一下KVO,該怎麼辦呢?
那就需要使用

willChangeValueForKey:
didChangeValueForKey:

來通知系統Key Value發生了改變。如:

- (void)updateName:(NSString *)name {
	[self willChangeVauleForKey:@"name"];
	_name = name;
	[self didChangeVauleForKey:@"name"];
}

KVO實現機制

那麼,KVO背後是如何實現的呢?在蘋果的官方文檔上,有一個籠統的描述

Automatic key-value observing is implemented using a technique called
isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class
which maintains a dispatch table. This dispatch table essentially
contains pointers to the methods the class implements, among other
data.

When an observer is registered for an attribute of an object the isa
pointer of the observed object is modified, pointing to an
intermediate class rather than at the true class. As a result the
value of the isa pointer does not necessarily reflect the actual class
of the instance.

You should never rely on the isa pointer to determine class
membership. Instead, you should use the class method to determine the
class of an object instance.

主要說了兩件事:

  1. KVO是基於isa-swizzling技術實現的。isa-swizzling會將被觀察對象的isa指針進行替換。
  2. 因爲在實現KVO時,系統會替換掉被觀察對象的isa指針,因此,不要使用isa指針來判斷類的關係,而應該使用class方法。

爲什麼要替換掉isa指針?文檔中說的很清楚,因爲isa指針會指向類實例對應的類的方法列表,而替換掉了isa指針,相當於替換掉了類的方法列表。

那麼爲啥要替換類的方法列表呢?又是怎麼替換的呢?文檔到這裏戛然而止,沒有細說。

下面,我們就用代碼實驗的方式,來窺探一下KVO的實現機制。

準備如下代碼:

@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, strong) NSMutableArray *friends;
@end

@implementation Student
- (void)showObjectInfo {
    NSLog(@"Object instance address is %p, Object isa content is %p", self, *((void **)(__bridge void *)self));
}

@end

我們在Student類中定義了方法- (void)showObjectInfo,主要是用來打印Student實例的地址,以及Student 的isa指針中的內容。這可以用來研究系統是如何做isa-swizzling操作的。

然後準備下面的方法,來打印類的方法列表:

static NSArray * ClassMethodNames(Class c)
{
    NSMutableArray * array = [NSMutableArray array];
    unsigned int methodCount = 0;
    Method * methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }
    
    free(methodList);
    return array;
}

運行如下代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    Student *std = [Student new];
    // 1. 初始值
    std.name = @"Tom";
    NSLog(@"std->isa:%@", object_getClass(std));
    NSLog(@"std class:%@", [std class]);
    NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
    [std showObjectInfo];
    
    // 2. 添加KVO
    [std addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [std addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    NSLog(@"std->isa:%@", object_getClass(std));
    NSLog(@"std class:%@", [std class]);
    NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
    [std showObjectInfo];
    std.name = @"Jack";

    // 3. 移除KVO
    [std removeObserver:self forKeyPath:@"name"];
    [std removeObserver:self forKeyPath:@"friends"];
    NSLog(@"std->isa:%@", object_getClass(std));
    NSLog(@"std class:%@", [std class]);
    NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
    [std showObjectInfo];
}

輸出爲:
// 1. 初始值

std->isa:Student
std class:Student
ClassMethodNames:(
    showObjectInfo,
    "setFriends:",
    friends,
    ".cxx_destruct",
    "setName:",
    name
)
Object address is 0x28194fe80, Object isa content is 0x1a1008090cd

// 2. 添加KVO

std->isa:NSKVONotifying_Student
std class:Student
ClassMethodNames:(
    "setFriends:",
    "setName:",
    class,
    dealloc,
    "_isKVOA"
)

Object address is 0x28194fe80, Object isa content is 0x1a282b5bf05

// 3. 移除KVO

std->isa:Student
std class:Student
ClassMethodNames:(
    showObjectInfo,
    "setFriends:",
    friends,
    ".cxx_destruct",
    "setName:",
    name
)

Object address is 0x28194fe80, Object isa content is 0x1a1008090cd

通過觀察添加KVO前、添加KVO後,移除KVO後這三個實際的Object地址信息可以知道,Object的地址並沒有改變,但是其isa指針中的內容,卻經歷瞭如下變化:0x1a1008090cd->0x1a282b5bf05->0x1a1008090cd
對應的,通過object_getClass(std)方法來輸出std的類型是:Student->NSKVONotifying_Student->Student

這就是所謂的isa-swizzling,當KVO時,系統會將被觀察對象的isa指針內容做替換,讓其指向新的類NSKVONotifying_Student,而在移除KVO後,系統又會將isa指針內容還原。

那麼,NSKVONotifying_Student這個類又是什麼樣的呢?
通過打印其方法列表,可以知道,NSKVONotifing_Stdent定義或重寫了如下方法:

ClassMethodNames:(
    "setFriends:",
    "setName:",
    class,
    dealloc,
    "_isKVOA"
)

可以看到,系統新生成的類重寫了我們KVO的屬性Friends和Name的set方法

同時,還重寫了class方法。通過runtime的源碼可以知道,class方法實際是調用了object_getClass方法

- (Class)class {
    return object_getClass(self);
}

而在object_getClass方法中,會輸出實例的isa指向的類:

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

按說[std class]object_getClass(std)的輸出應該一致,但是系統會在KVO的時候,悄悄改寫實例的class方法。這也就是爲什麼,當使用[std class]方法打印實例的類時,會輸出Student而不是實際的NSKVONotifing_Student

然後系統還重寫了dealloc方法,估計是爲了在實例銷燬時,做一些檢查及清理工作。

最後,添加了_isKVOA方法,這估計是系統爲了識別是KVO類而添加的。

這裏,細心的同學會發現,在KVO之前,Student的方法列表裏面是包含屬性的get方法showObjectInfo方法以及.cxx_destruct這些方法的。而當系統將Student替換爲NSKVONotifing_Student後,這些方法那裏去了呢?如果這些方法沒有在NSKVONotifing_Student再實現一遍的話,那當KVO後,我們再調用屬性的get方法showObjectInfo方法豈不是會crash?

但平日的編程實踐告訴我們,並不會crash。那這些方法都去那裏了呢?讓我們來看一下NSKVONotifing_Student的父類是什麼:

// 2. 添加KVO
...
Class objectRuntimeClass = object_getClass(std);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"super class is %@", superClass);

輸出爲:

super class is Student

哈哈,很有意思吧,原來NSKVONotifing_Student的父類竟然是Student。那根據OC的消息實現機制,當在NSKVONotifing_Student中沒有找到方法實現時,會自動到其父類Student中尋找相應的實現。因此,在NSKVONotifing_Student中,僅僅需要定義或重寫KVO相關的方法即可,至於Student中定義的其他方法,則會在消息機制中在被自動找到。

以上,便是KVO的isa-swizzling技術的大體實現流程。讓我們總結一下:

  1. 當類實例被KVO後,系統會替換實例的isa指針內容。讓其指向NSKVONotifing_XX類型的新類。
  2. NSKVONotifing_XX類中,會:重寫KVO屬性的set方法,支持KVO。重寫class方法,來僞裝自己仍然是XX類。添加_isKVOA方法,來說明自己是一個KVO類。重寫dealloc方法,讓實例下析構時,好做一些檢查和清理工作
  3. 爲了讓用戶在KVO isa-swizzling後,仍然能夠調用原始XX類中的方法,系統還會將NSKVONotifing_XX類設置爲原始XX類的子類
  4. 當移除KVO後,系統會將isa指針中的內容復原。

手動實現KVO

既然知道了KVO背後的實現原理,我們能不能利用runtime方法,模擬實現一下KVO呢?
當然可以,下來看下效果:

#import "ViewController.h"
#import "NSObject+KVOBlock.h"
#import <objc/runtime.h>
@implementation Student
@end
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Student *std = [Student new];
    // 直接用block回調來接受 KVO
    [std sw_addObserver:self forKeyPath:@"name" callback:^(id  _Nonnull observedObject, NSString * _Nonnull observedKeyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
        NSLog(@"old value is %@, new vaule is %@", oldValue, newValue);
    }];
    
    std.name = @"Hello";
    std.name = @"Lilhy";
    NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
    [std sw_removeObserver:self forKeyPath:@"name"];
    NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
    
}
@end

爲了模擬的和系統KVO實現類似,我們也改寫了class方法,在KVO移除前後,打印std的類信息爲:

class is Student, object_class is sw_KVONotifing_Student
// 移除KVO後
class is Student, object_class is Student

在這裏我手動實現了KVO,並通過Block的方式來接受KVO的回調信息。接下來我們就一步步的分析是如何做到的。我們應該重點觀察所使用到的runtime方法。

首先,我們新建一個NSObject的分類NSObject (KVOBlock),並聲明如下方法:

typedef void(^sw_KVOObserverBlock)(id observedObject, NSString *observedKeyPath, id oldValue, id newValue);

@interface NSObject (KVOBlock)
- (void)sw_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
              callback:(sw_KVOObserverBlock)callback;

- (void)sw_removeObserver:(NSObject *)observer
               forKeyPath:(NSString *)keyPath;

@end

在關鍵的sw_addObserver:forKeyPath:callback:中,是這麼實現的:

static void *const sw_KVOObserverAssociatedKey = (void *)&sw_KVOObserverAssociatedKey;
static NSString *sw_KVOClassPrefix = @"sw_KVONotifing_";

- (void)sw_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
              callback:(sw_KVOObserverBlock)callback {
    // 1. 通過keyPath獲取當前類對應的setter方法,如果獲取不到,說明setter 方法即不存在與KVO類,也不存在與原始類,這總情況正常情況下是不會發生的,觸發Exception
    NSString *setterString = sw_setterByGetter(keyPath);
    SEL setterSEL = NSSelectorFromString(setterString);
    Method method = class_getInstanceMethod(object_getClass(self), setterSEL);
    
    if (method) {
        // 2. 查看當前實例對應的類是否是KVO類,如果不是,則生成對應的KVO類,並設置當前實例對應的class是KVO類
        Class objectClass = object_getClass(self);
        NSString *objectClassName = NSStringFromClass(objectClass);
        if (![objectClassName hasPrefix:sw_KVOClassPrefix]) {
            Class kvoClass = [self makeKvoClassWithOriginalClassName:objectClassName]; // 爲原始類創建KVO類
            object_setClass(self, kvoClass); // 將當前實例的類設置爲KVO類
        }
        
        // 3. 在KVO類中查找是否重寫過keyPath 對應的setter方法,如果沒有,則添加setter方法到KVO類中
        // 注意,此時object_getClass(self)獲取到的class應該是KVO class
        if (![self hasMethodWithMethodName:setterString]) {
            class_addMethod(object_getClass(self), NSSelectorFromString(setterString), (IMP)sw_kvoSetter, method_getTypeEncoding(method));
        }
        
        // 4. 註冊Observer
        NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
        if (observerArray == nil) {
            observerArray = [NSMutableArray new];
            objc_setAssociatedObject(self, sw_KVOObserverAssociatedKey, observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        SWKVOObserverItem *item = [SWKVOObserverItem new];
        item.keyPath = keyPath;
        item.observer = observer;
        item.callback = callback;
        [observerArray addObject:item];
        
        
    }else {  
        NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
        NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
        [exception raise];
    }
}

上面的函數重點是:

  1. 調用makeKvoClassWithOriginalClassName方法來生成原始類對應的KVO類
  2. 利用class_addMethod方法,爲KVO類添加改寫的setter實現

完成了上面兩點,一個手工的KVO實現基本就完成了。另一個需要注意的是,如何存儲observer。在這裏是通過一個MutableArray數組,當做Associated object來存儲到類實例中的。

可以看出來,這裏的重點在於如何創建原始類對應的KVO類

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClassName {
    // 1. 檢查KVO類是否已經存在, 如果存在,直接返回
    NSString *kvoClassName = [NSString stringWithFormat:@"%@%@", sw_KVOClassPrefix, originalClassName];
    Class kvoClass = objc_getClass(kvoClassName.UTF8String);
    if (kvoClass) {
        return kvoClass;
    }
    
    // 2. 創建KVO類,並將原始class設置爲KVO類的super class
    kvoClass = objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0);
    objc_registerClassPair(kvoClass);
    
    // 3. 重寫KVO類的class方法,使其指向我們自定義的IMP,實現KVO class的‘僞裝’
    Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
    const char* types = method_getTypeEncoding(classMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);
    return kvoClass;
}

其實實現也不難,調用了runtime的方法

  1. objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0) 動態生成新的KVO類,並設置KVO類的super class是原始類
  2. 註冊KVO類 : objc_registerClassPair(kvoClass)
  3. 爲了實現KVO僞裝成原始類,還爲KVO類添加了我們自己重寫的class方法:
 	Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
    const char* types = method_getTypeEncoding(classMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);

// 自定義的class方法實現
static Class sw_class(id self, SEL selector) {
    return class_getSuperclass(object_getClass(self));  // 因爲我們將原始類設置爲了KVO類的super class,所以直接返回KVO類的super class即可得到原始類Class
}

那麼當我們需要移除Observer時,需要調用sw_removeObserver:forKeyPath: 方法:

- (void)sw_removeObserver:(NSObject *)observer
               forKeyPath:(NSString *)keyPath {
    NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
    [observerArray enumerateObjectsUsingBlock:^(SWKVOObserverItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.observer == observer && [obj.keyPath isEqualToString:keyPath]) {
            [observerArray removeObject:obj];
        }
    }];
    
    if (observerArray.count == 0) { // 如果已經沒有了observer,則把isa復原,銷燬臨時的KVO類
        Class originalClass = [self class];
        Class kvoClass = object_getClass(self);
        object_setClass(self, originalClass);
        objc_disposeClassPair(kvoClass);
    }   
}

注意,這裏當Observer數組爲空時,我們會將當前實例的所屬類復原成原始類,並dispose掉生成的KVO類

完整的源碼在這裏

KVO crash的避免

總結

參考

KVO原理分析及使用進階
如何自己動手實現 KVO

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章