天天看點

KVO詳解及底層實作代碼位址

什麼是KVO??

KVO就是

NSKeyValueObserving

,請看官方文檔的解釋:

KVO詳解及底層實作代碼位址

大概翻譯如下:

一種非正式協定,通知其他對象的指定屬性發生了改變。           

簡單了解就是,可以監聽一個對象的某個屬性是否發生改變。

那麼問題來了,什麼是非正式協定??有正式協定嗎??

麻蛋,本來想找官方文檔的,找了半天沒找到。從Stackoverflow找到了答案,貌似原來官方文檔的連結失效了

KVO詳解及底層實作代碼位址

大概翻譯如下:

非正式協定:非正式協定是NSObject的一個類别

Category

,幾乎所有的對象都隐含的采用(類别是OC的語言特性,能夠給類對象添加方法而不需要建立子類),非正式協定的方法是可選的

正式協定: 一個正式協定聲明了類需要實作的方法清單,正式協定有自己的聲明、采用和類型檢查文法。你可以使用

@required

或者

optional

關鍵字指定方法是否必須實作。子類繼承父類采用的協定。正式協定也可以遵守其他協定

KVO實作

  • 監聽某個對象的某個屬性
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;           
  • 實作非正式協定
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;           
  • 移除監聽
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;           

簡單代碼示範:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    self.person = [[ZJPerson alloc] init];

    [self.person setName:@"zhangsan"];

    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person setName:@"lisi"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@", change);
}

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
}           

列印結果:

KVO詳解及底層實作代碼位址

用法其實很簡單,接下來重點來了,KVO為什麼能夠監聽到屬性變化,底層做了什麼??

KVO底層實作探究

首先,我們利用

runtime

在添加監聽之前和之後分别列印一下類對象

NSLog(@"%@", object_getClass(self.person));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"%@", object_getClass(self.person));           

列印結果:

2018-05-19 22:48:18.726028+0800 KVO[33804:3059947] ZJPerson
2018-05-19 22:48:18.726535+0800 KVO[33804:3059947] NSKVONotifying_ZJPerson           

我們發現添加監聽之後,執行個體對象的類對象發生了變化,系統為我們動态添加了一個

NSKVONotifying_+類名

的類,因為我們改變對象屬性的值是通過

setter

方法實作了,是以很明顯是系統動态生成的

NSKVONotifying_ZJPerson

類重寫了

setter

方法。不信的話,我們可以做一個實驗,自己手動添加一個

NSKVONotifying_ZJPerson

類,看下會列印什麼

2018-05-19 22:56:32.223288+0800 KVO[33919:3068985] [general] KVO failed to allocate class pair for name NSKVONotifying_ZJPerson, automatic key-value observing will not work for this class           

錯誤提示很明顯,告訴我們建立

NSKVONotifying_ZJPerson

類失敗,KVO失效

那麼系統自動建立重寫的的

setter

方法内部做了什麼呢??同樣在添加監聽方法之前,利用

runtime

列印下方法的實作,截圖如下:

KVO詳解及底層實作代碼位址

發現方法實作變了,内部調用了系統

Foundation

架構下的

_NSSetObjectValueAndNotify

方法。那麼這個架構内部又是怎麼實作的呢,我們可以下斷點,檢視下函數調用棧:

首先通過設定一個觀察點,觀察屬性的變化:

KVO詳解及底層實作代碼位址

繼續執行,可以看到函數調用棧如下:

KVO詳解及底層實作代碼位址

在結果發生改變的地方繼續下斷點調試:

KVO詳解及底層實作代碼位址
KVO詳解及底層實作代碼位址

由以上函數調用棧,我們大緻可以猜測出,

_NSSetObjectValueAndNotify

函數内部實作過程如下:

1. `-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:
2. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:]:
3. [ZJPerson setName:];
4. `NSKeyValueDidChange:
5. `NSKeyValueNotifyObserver:
6. - (void)observeValueForKeyPath:ofObject:change:context           

簡化成OC的僞代碼大緻如下:

- (void)setName:(NSString *)name{
    _NSSetObjectValueAndNotify();
}

void _NSSetObjectValueAndNotify {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

- (void)didChangeValueForKey:(NSString *)key{
    [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}           

NSKVONotifying_ZJPerson内部都重寫了哪些方法

可以利用

runtime

方法列印一下方法清單:

unsigned int count;
Method *methods = class_copyMethodList(object_getClass(self.person), &count);

for (NSInteger index = 0; index < count; index++) {
   Method method = methods[index];

   NSString *methodStr = NSStringFromSelector(method_getName(method));

   NSLog(@"%@\n", methodStr);
}           

列印結果:

2018-05-20 08:57:07.883400+0800 KVO[35888:3218908] setName:
2018-05-20 08:57:07.883571+0800 KVO[35888:3218908] class
2018-05-20 08:57:07.883676+0800 KVO[35888:3218908] dealloc
2018-05-20 08:57:07.883793+0800 KVO[35888:3218908] _isKVOA           

簡單分析下重寫這些方法的作用:

class

:重寫這個方法,是為了僞裝蘋果自動為我們生成的中間類。

dealloc

:應該是處理對象銷毀之前的一些收尾工作

_isKVOA

:告訴系統使用了

kvo

拓展

學任何東西,通過我們的思考一定會問出一些别的問題,通過深入了解kvo,下面兩個問題,是面試經常會被問到的,也是我所能想到的:

  • 如何動态生成一個類??
  • 知道了原理,能不能自己寫一個KVO??

動态生成一個自己的類

既然是動态生成,肯定是利用了蘋果的

runtime

機制,通過上面對

KVO

的學習,也了解到了

runtime

的強大之處。

  • 建立類
Class customClass = objc_allocateClassPair([NSObject class], "ZJCustomClass", 0);           
  • 添加執行個體變量
// 添加執行個體變量
    class_addIvar(customClass, "age", sizeof(int), 0, "i");           
  • 添加方法,

    [email protected]:

    表示方法的參數和傳回值
// 添加方法
    class_addMethod(customClass, @selector(hahahha), (IMP)hahahha, "[email protected]:");           

需要實作方法:

void hahahha(id self, SEL _cmd)
{
    NSLog(@"hahahha====");
}

- (void)hahahha{

}           
  • 注冊到運作時環境
objc_registerClassPair(customClass);           

列印方法清單和成員變量清單,檢視是否建立成功

#pragma mark - Util

- (NSString *)copyMethodsByClass:(Class)cls{
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);

    NSString *methodStrs = @"";

    for (NSInteger index = 0; index < count; index++) {
        Method method = methods[index];

        NSString *methodStr = NSStringFromSelector(method_getName(method));

        methodStrs = [NSString stringWithFormat:@"%@ ", methodStr];
    }

    free(methods);

    return methodStrs;
}

- (NSString *)copyIvarsByClass:(Class)cls{
    unsigned int count;
    Ivar *ivars = class_copyIvarList(cls, &count);

    NSMutableString *ivarStrs = [NSMutableString string];

    for (NSInteger index = 0; index < count; index++) {
        Ivar ivar = ivars[index];

        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];  //擷取成員變量的名字

        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; //擷取成員變量的資料類型

        [ivarStrs appendString:@"\n"];
        [ivarStrs appendString:ivarName];
        [ivarStrs appendString:@"-"];
        [ivarStrs appendString:ivarType];

    }

    free(ivars);

    return ivarStrs;
}           

調用方法可看到建立成功:

NSLog(@"%@", [self copyMethodsByClass:customClass]);
NSLog(@"%@", [self copyIvarsByClass:customClass]);           
KVO詳解及底層實作代碼位址

動态建立類大緻就這些步驟。。。

自己動手寫一個KVO

KVO

底層實作還是很複雜的,下面我隻是簡單的寫下實作過程:

  • 因為它是一個非正式協定,給

    NSObject

    建立一個

    Category

    NSObject+kvo.h

    ,添加監聽方法:

.h檔案

#import <Foundation/Foundation.h>

@interface NSObject (kvo)

- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

@end           

.m檔案

#import "NSObject+kvo.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (kvo)

- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    //動态添加一個類
    NSString *originClassName = NSStringFromClass([self class]);

    NSString *newClassName = [@"ZJKVO_" stringByAppendingString:originClassName];

    const char *newName = [newClassName UTF8String];

    // 繼承自目前類,建立一個子類
    Class kvoClass = objc_allocateClassPair([self class], newName, 0);

    // 添加setter方法
    class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "[email protected]:@");

    //注冊新添加的這個類
    objc_registerClassPair(kvoClass);

    // 修改isa指針,由ZJPerson指向ZJKVO_Person
    object_setClass(self, kvoClass);

    // 儲存觀察者屬性到目前類中
    objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - 重寫父類方法

void setName(id self, SEL _cmd, NSString *name) {

    // 儲存目前KVO的類
    Class kvoClass = [self class];

    // 将self的isa指針指向父類ZJPerson,調用父類setter方法
    object_setClass(self, class_getSuperclass([self class]));

    // 調用父類setter方法,重新複制
    objc_msgSend(self, @selector(setName:), name);

    // 取出ZJKVO_Person觀察者
    id objc = objc_getAssociatedObject(self, (__bridge const void *)@"observer");

    // 通知觀察者,執行通知方法
    objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:), name, self, nil, name);

    // 重新修改為ZJKVO_Person類
    object_setClass(self, kvoClass);
}           

注意一

要修改下xcode中的一個配置,将它改為NO,否則會報參數太多的錯誤:

KVO詳解及底層實作代碼位址

注意二

解釋下代碼中

[email protected]:@

的意思:

* 第一個

v

表示方法傳回值

void

* 第二三個

@:

一般是一塊的,因為函數至少有兩個參數

self和_cmd

,一般是固定寫法

* 最後一個

@

表示參數類型,是一個對象

下面在代碼中實驗,看下我們自己寫的kvo有沒有執行:

修改添加監聽者的方法,改成我們自己的

[self.person zj_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];           

看下回調中的列印:

KVO詳解及底層實作代碼位址

發現确實監聽到了。。。

代碼位址

總結

kvo用法其實非常簡單,但是深入了解,深入思考的話,知識點非常多。花了一天多的時間,期間查閱了很多文檔(發現官方文檔真的是非常有用),總算是寫完了,對KVO有了一個更深入的認識和了解。今天是520,感謝女朋友的了解,終于可以陪她出去玩了,哈哈。。。