iOS-底層原理21:KVO底層原理

上一篇文章iOS-底層原理20:KVC底層原理中了解了KVC底層原理筋蓖,本文將講解KVO底層原理蒸播。

結(jié)合上一篇文章有個(gè)問題

問題: 通過KVC修改屬性會(huì)觸發(fā)KVO嗎

帶著問題開始探索

1. 定義

KVO迫皱,全稱為Key-Value observing创橄,中文名為鍵值觀察询刹,KVO是一種機(jī)制棋恼,它允許將其他對象的指定屬性的更改通知給對象官方文檔

2. KVO 使用注意事項(xiàng)

2.1 基本使用

KVO的基本使用主要分為3步:

step1: 向觀察對象注冊觀察者 addObserver:forKeyPath:options:context:

  • Options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,    //提供更改后的值
    NSKeyValueObservingOptionOld = 0x02,    //提供更改前的值
    NSKeyValueObservingOptionInitial,     //初始值脂崔,在注冊觀察服務(wù)時(shí)會(huì)調(diào)用一次觸發(fā)方法
    NSKeyValueObservingOptionPrior   //分別在值修改前后觸發(fā)方法滤淳,NSKeyValueChangeNotificationIsPriorKey可以來判斷在前調(diào)用還是在后調(diào)用
}
  • Context

翻譯成中文

大致含義就是:addObserver:forKeyPath:options:context: 方法中的上下文context指針包含任意數(shù)據(jù),這些數(shù)據(jù)將在相應(yīng)的更改通知中傳遞回觀察者砌左〔备溃可以通過指定context為NULL,從而依靠keyPath鍵路徑字符串傳來確定更改通知的來源汇歹,但是這種方法可能會(huì)導(dǎo)致對象的父類由于不同的原因也觀察到相同的鍵路徑而導(dǎo)致問題屁擅。所以可以為每個(gè)觀察到的keyPath創(chuàng)建一個(gè)不同的context,從而完全不需要進(jìn)行字符串比較产弹,從而可以更有效地進(jìn)行通知解析

通俗的講派歌,context上下文主要是用于區(qū)分不同對象的同名屬性,從而在KVO回調(diào)方法中可以直接使用context進(jìn)行區(qū)分痰哨,可以大大提升性能胶果,以及代碼的可讀性

context使用總結(jié)
  • 不使用context,使用keyPath區(qū)分通知來源
//context的類型是 nullable void *斤斧,應(yīng)該是NULL早抠,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
  • 使用context區(qū)分通知來源
//定義context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;

//注冊觀察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    
    
//KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"%@",change);
    }else if (context == PersonNameContext){
        NSLog(@"%@",change);
    }
}

step2: 接收更改通知消息 observeValueForKeyPath:ofObject:change:context:

  • change
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
     NSKeyValueChangeSetting = 1,      //賦值
     NSKeyValueChangeInsertion = 2,     //添加值
     NSKeyValueChangeRemoval = 3,     //移除值
     NSKeyValueChangeReplacement = 4,  //替換值
};
//kind,表示動(dòng)作的種類撬讽,值為上面這個(gè)枚舉
NSKeyValueChangeKey const NSKeyValueChangeKindKey; 
//new, 更改后的值
NSKeyValueChangeKey const NSKeyValueChangeNewKey; 
//old, 更改前的值
NSKeyValueChangeKey const NSKeyValueChangeOldKey;  
//indexes蕊连,數(shù)組等改變的index信息
NSKeyValueChangeKey const NSKeyValueChangeIndexesKey; 
//notificationIsPrior,這個(gè)用于NSKeyValueObservingOptionPrior用于區(qū)分是更改前的觸發(fā)通知還是更改后的觸發(fā)通知
NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey; 

step3: 注銷觀察者 removeObserver:forKeyPath:context:

刪除觀察者時(shí)游昼,請記住以下幾點(diǎn):

  • 如果觀察者未注冊調(diào)用移除將會(huì)導(dǎo)致NSRangeException甘苍。 成對調(diào)用addObserver:forKeyPath:options:context:removeObserver:forKeyPath:context:,或?qū)?code>removeObserver:forKeyPath:context:放入try/catch塊內(nèi)處理潛在的異常烘豌。

  • 釋放后羊赵,觀察者不會(huì)自動(dòng)將其自身移除。被觀察對象繼續(xù)發(fā)送通知,而忽略了觀察者的狀態(tài)昧捷。但是闲昭,與發(fā)送到已釋放對象的任何其他消息一樣,更改通知會(huì)觸發(fā)內(nèi)存訪問異常靡挥。因此序矩,你可以確保觀察者在從內(nèi)存中消失之前將自己刪除

  • 該協(xié)議無法詢問對象是觀察者還是被觀察者跋破。構(gòu)造代碼以避免發(fā)布相關(guān)的錯(cuò)誤簸淀。一種典型的模式是在觀察者初始化期間(例如,在init或viewDidLoad中注冊為觀察者毒返,并在釋放過程中(通常在dealloc中注銷租幕,以確保成對和有序地添加和刪除消息,并確保觀察者在注冊之前被取消注冊拧簸,從內(nèi)存中釋放出來劲绪。

移除觀察者Demo

step1: 新建一個(gè)LBHPerson類和一個(gè)LBHStudent

/********LBHPerson********/
//.h
@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@end

//.m
@implementation LBHPerson

@end

/********LBHStudent********/

//.h
@interface LBHStudent : LBHPerson

+ (instancetype)shareInstance;

@end

//.m
@implementation LBHStudent

static LBHStudent* _instance = nil;
+ (instancetype)shareInstance{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init] ;
    }) ;
    return _instance ;
}
@end

step2: 有一個(gè)嵌套在UINavigationController中的ViewController類,有一個(gè)導(dǎo)航按鈕可以push到下一個(gè)類LBHViewController

@interface ViewController ()

@property (nonatomic, strong) LBHStudent *student;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.student = [LBHStudent shareInstance];
}

step3: LBHViewController

@interface LBHViewController ()

@property (nonatomic, strong) LBHStudent *student;

@end

@implementation LBHViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.student = [LBHStudent shareInstance];

    [self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = [NSString stringWithFormat:@"%@+",self.student.name];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    // fastpath
    // 性能 + 代碼可讀性
    NSLog(@"%@",change);
}

@end

step4: 運(yùn)行盆赤,跳轉(zhuǎn)到LBHViewController贾富,返回,繼續(xù)跳轉(zhuǎn)到LBHViewController牺六,點(diǎn)擊屏幕颤枪,崩潰了

問題: 為什么會(huì)崩潰?

解答: 第一次進(jìn)入LBHViewController注冊了KVO觀察者,返回上一個(gè)頁面時(shí)沒有移除監(jiān)聽淑际,再次進(jìn)入會(huì)重新注冊KVO觀察者胶征,而且第一次的通知對象還在內(nèi)存中磷瘤,沒有進(jìn)行釋放缝其,點(diǎn)擊屏幕屬性發(fā)生變化涝缝,此時(shí)接收到屬性值變化的通知,會(huì)出現(xiàn)找不到原有的通知對象淡溯,只能找到現(xiàn)有的通知對象读整,即第二次KVO注冊的觀察者簿训,所以導(dǎo)致了類似野指針的崩潰咱娶,即一直保持著一個(gè)野通知,且一直在監(jiān)聽

【注】:這里的崩潰案例是通過單例對象實(shí)現(xiàn)强品,因?yàn)閱卫龑ο笤趦?nèi)存是常駐的膘侮,針對一般的類對象,貌似不移除也是可以的的榛,但是為了防止線上意外琼了,建議還是移除比較好

解決方法

LBHViewControllerdealloc方法中加上移除監(jiān)聽

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

2.2 KVO的自動(dòng)觸發(fā)與手動(dòng)觸發(fā)

KVO觀察的開啟關(guān)閉有兩種方式,自動(dòng)手動(dòng)

  • 自動(dòng)開關(guān),返回NO雕薪,就監(jiān)聽不到昧诱,返回YES,表示監(jiān)聽
// 自動(dòng)開關(guān)
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}
  • 自動(dòng)開關(guān)關(guān)閉的時(shí)候所袁,可以通過手動(dòng)開關(guān)監(jiān)聽盏档,重寫監(jiān)聽屬性的set方法
- (void)setName:(NSString *)name{
    //手動(dòng)開關(guān)
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}
測試

step1: 在上面demo中,LBHPerson中添加automaticallyNotifiesObserversForKey方法返回一個(gè)NO

// 自動(dòng)開關(guān)
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

step2: 運(yùn)行

沒有任何打印燥爷,說明并沒有監(jiān)聽

step3:LBHPerson中添加如下方法

- (void)setName:(NSString *)name
{
    [self willChangeValueForKey:@"name"];   //即將改變
    _name = name;
    [self didChangeValueForKey:@"name"];   //已改變
}

step4: 打印正常

2.3 KVO觀察:一對多

KVO觀察中的一對多蜈亩,意思是通過注冊一個(gè)KVO觀察者,可以監(jiān)聽多個(gè)屬性的變化

以下載進(jìn)度為例前翎,比如目前有一個(gè)需求稚配,需要根據(jù)總的下載量totalData當(dāng)前下載量writtenData 來計(jì)算當(dāng)前的下載進(jìn)度downloadProgress,實(shí)現(xiàn)有兩種方式:

1港华、 分別觀察 總的下載量totalData當(dāng)前下載量writtenData 兩個(gè)屬性道川,當(dāng)其中一個(gè)發(fā)生變化 重新計(jì)算當(dāng)前下載進(jìn)度currentProcess

2、 實(shí)現(xiàn)keyPathsForValuesAffectingValueForKey方法苹丸,將兩個(gè)觀察合為一個(gè)觀察愤惰,即觀察當(dāng)前下載進(jìn)度currentProcess

// LBHPerson
@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end

@implementation LBHPerson

- (NSString *)downloadProgress {
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [NSString stringWithFormat:@"%.2f", 1.0f * self.writtenData / self.totalData];
}

// 下載進(jìn)入 writtenData / totalData
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray * affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
@end

// ViewController
@interface ViewController ()
@property (nonatomic, strong) LBHPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [LBHPerson new];

    // 添加監(jiān)聽
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
    
}

// 處理監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@: %@", keyPath, change);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.writtenData += 20;
    self.person.totalData +=10;
}

-(void)dealloc {
    // 移除監(jiān)聽
    [self.person removeObserver:self forKeyPath:@"downloadProgress" context: NULL];
}

@end

運(yùn)行結(jié)果

兩個(gè)屬性在點(diǎn)擊時(shí)都有賦值,所以每次打印兩條

2.4 KVO 觀察可變數(shù)組

可變數(shù)組如果直接添加數(shù)據(jù)赘理,是不會(huì)調(diào)用setter方法的宦言,所有對可變數(shù)組的KVO觀察下面這種方式不生效的,即直接通過[self.person.dateArray addObject:@"1"];向數(shù)組添加元素,是不會(huì)觸發(fā)kvo通知回調(diào)的

2.4.1 官方文檔說明

翻譯成中文

2.4.2 測試代碼
// LBHPerson
@interface LBHPerson : NSObject
@property (nonatomic, strong) NSMutableArray *dateArray;
@end

@implementation LBHPerson

- (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
    [self.dateArray insertObject:object atIndex:index];
}

@end

// ViewController
@interface ViewController ()
@property (nonatomic, strong) LBHPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [LBHPerson new];

    self.person.dateArray = [NSMutableArray new];
    
    // 添加監(jiān)聽
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];

    [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
}

// 處理監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@: %@", keyPath, change);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
 
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

    //集合相關(guān)API
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"2" atIndex:0];
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"3" atIndexedSubscript:0];
    [[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
}

-(void)dealloc {
    // 移除監(jiān)聽
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
    [self.person removeObserver:self forKeyPath:@"dateArray" context: NULL];
}

@end

運(yùn)行

其中的kind表示鍵值變化的類型商模,是一個(gè)枚舉奠旺,主要有以下4種

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//設(shè)值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替換
};

3 KVO 底層原理探索

  • KVO是使用isa-swizzling的技術(shù)實(shí)現(xiàn)的。

  • 顧名思義施流,isa指針指向維護(hù)分配表的對象的類响疚。該分派表實(shí)質(zhì)上包含指向該類實(shí)現(xiàn)的方法的指針以及其他數(shù)據(jù)。

  • 當(dāng)為對象的屬性注冊觀察者時(shí)瞪醋,將修改觀察對象的isa指針忿晕,指向中間類而不是真實(shí)類。結(jié)果银受,isa指針的值不一定反映實(shí)例的實(shí)際類践盼。

  • 您永遠(yuǎn)不應(yīng)依靠isa指針來確定類成員身份。相反宾巍,您應(yīng)該使用class方法來確定對象實(shí)例的類咕幻。

3.1 KVO 只對屬性觀察

3.1.1 測試

LBHPerson 中創(chuàng)建一個(gè)成員變量nickName 和一個(gè)屬性name,然后分別注冊KVO

// LBHPerson
@interface LBHPerson : NSObject
{
@public NSString * nickName;
}
@property (nonatomic, copy) NSString *name;
@end

@implementation LBHPerson

@end

// ViewController
@interface ViewController ()
@property (nonatomic, strong) LBHPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [LBHPerson new];

    self.person.name = @"liu";
    
    // 添加監(jiān)聽
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
}

// 處理監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@: %@", keyPath, change);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = [NSString stringWithFormat:@"%@name+", self.person.name];
    self.person->nickName = [NSString stringWithFormat:@"%@nickName+", self.person->nickName];
}

-(void)dealloc {
    // 移除監(jiān)聽
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
    [self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
}

@end

運(yùn)行

多次打印結(jié)果都只有屬性顶霞,并沒有出現(xiàn)成員變量的打印肄程,說明成員變量并沒有監(jiān)聽 。

結(jié)論:KVO對成員變量不觀察,只對屬性觀察蓝厌,屬性和成員變量的區(qū)別在于屬性多一個(gè) setter 方法玄叠,KVO觀察的就是setter 方法

3.2 中間類

根據(jù)官方文檔所述拓提,在注冊KVO觀察者后诸典,觀察對象的isa指針指向會(huì)發(fā)生改變

注冊觀察者之前:

注冊觀察者之后:

在注冊觀察者之后,對象的isa指針發(fā)生變化崎苗,指向的類也發(fā)生了改變

p self.person.class得到的卻是LBHPerson狐粱,NSKVONotifying_LBHPersonLBHPerson是什么關(guān)系?

3.2.1 NSKVONotifying_LBHPersonLBHPerson類 的關(guān)系

可以通過下面封裝的方法胆数,獲取LBHPerson的相關(guān)類

#import <objc/runtime.h>

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    
    // 注冊類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個(gè)數(shù)組肌蜻, 其中包含給定對象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 獲取所有已注冊的類
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

//********調(diào)用********
[self printClasses:[LBHPerson class]];

從結(jié)果中可以說明NSKVONotifying_LBHPersonLBHPerson的子類

3.2.2 中間類有些什么?

我們添加遍歷Ivars必尼、Property蒋搜、Method的函數(shù):

/// 遍歷Ivars
-(void)printIvars: (Class)cls {
    
    // 仿寫Ivar結(jié)構(gòu)
    typedef struct LBH_ivar_t {
        int32_t *offset;
        const char *name;
        const char *type;
        uint32_t alignment_raw;
        uint32_t size;
    }LBH_ivar_t;
    
    // 記錄函數(shù)個(gè)數(shù)
    unsigned int count = 0;
    // 讀取函數(shù)列表
    Ivar * ivars = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i++) {
        LBH_ivar_t * ivar = (LBH_ivar_t *) ivars[i];
        NSLog(@"ivar: %@", [NSString stringWithUTF8String: ivar->name]);
    }
    free(ivars);
    
}

/// 遍歷屬性
-(void) printProperties: (Class)cls {
    
    // 仿寫objc_property_t結(jié)構(gòu)
    typedef struct LBH_property_t{
        const char *name;
        const char *attributes;
    }LBH_property_t;
    
    // 記錄函數(shù)個(gè)數(shù)
    unsigned int count = 0;
    // 讀取函數(shù)列表
    objc_property_t * props = class_copyPropertyList(cls, &count);
    for (int i = 0; i < count; i++) {
        LBH_property_t * prop = (LBH_property_t *)props[i];
        NSLog(@"property: %@", [NSString stringWithUTF8String:prop->name]);
    }
    free(props);
    
}

/// 遍歷方法
-(void) printMethodes: (Class)cls {
    
    // 記錄函數(shù)個(gè)數(shù)
    unsigned int count = 0;
    // 讀取函數(shù)列表
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"method: %@-%p", NSStringFromSelector(sel), imp);
    }
    free(methodList);
}

在注冊KVO觀察者后,分別對 LBHPerson本類NSKVONotifying_LBHPerson派生類Ivars判莉、PropertyMethod進(jìn)行打印

NSLog(@"------- NSKVONotifying_LBHPerson --------");
[self printMethodes: objc_getClass("NSKVONotifying_LBHPerson")];
[self printIvars: objc_getClass("NSKVONotifying_LBHPerson")];
[self printProperties: objc_getClass("NSKVONotifying_LBHPerson")];
NSLog(@"------- LBHPerson --------");
[self printMethodes: self.person.class];
[self printIvars: self.person.class];
[self printProperties: self.person.class];

打印結(jié)果

NSKVONotifying_LBHPerson的方法是繼承還是重寫豆挽?

新建一個(gè)LBHPerson的子類LBHStudent

NSLog(@"------- NSKVONotifying_LBHPerson --------");
[self printMethodes: objc_getClass("NSKVONotifying_LBHPerson")];
[self printIvars: objc_getClass("NSKVONotifying_LBHPerson")];
[self printProperties: objc_getClass("NSKVONotifying_LBHPerson")];
NSLog(@"------- LBHPerson --------");
[self printMethodes: self.person.class];
[self printIvars: self.person.class];
[self printProperties: self.person.class];
NSLog(@"------- LBHStudent --------");
[self printMethodes: LBHStudent.class];
[self printIvars: LBHStudent.class];
[self printProperties: LBHStudent.class];

打印

結(jié)論

  • 直接繼承的子類券盅,沒有任何方法和屬性帮哈。
  • KVO派生類繼承自LBHPerosn,重寫了setName锰镀、class娘侍、dealloc方法,新增了_isKVOA方法
3.2.3 KVO派生類給父類屬性賦值

step1:addObserver處添加斷點(diǎn)泳炉,運(yùn)行代碼到此處時(shí)憾筏,lldb輸入:watchpoint set variable self->_person->_name

step2: 設(shè)置成功后,繼續(xù)運(yùn)行代碼花鹅,點(diǎn)擊屏幕觸發(fā)touchesBegan事件氧腰,會(huì)進(jìn)入?yún)R編頁面(觀察到設(shè)置屬性斷點(diǎn)處)

查看堆棧,會(huì)有些發(fā)現(xiàn)

可以觀察到刨肃,當(dāng)派生類在調(diào)用willChangedidChange中間古拴,調(diào)用了[LBHPerson setName]方法,完成了給父類LBHPerson的name屬性賦值之景。(此時(shí)的willChange和didChange方法是繼承自NSObject的)

3.2.4 KVO派生類何時(shí)移除斤富,是否真移除膏潮?

step1:dealloc函數(shù)中锻狗,移除KVO處添加一個(gè)斷點(diǎn)

移除觀察者之前:實(shí)例對象的isa指向仍是NSKVONotifying_LBHPerson中間類

step2: 執(zhí)行一步,繼續(xù)查看實(shí)例對象的isa指向

移除觀察者之后:實(shí)例對象的isa指向更改為LBHPerson

問題: 那中間類是否被刪除?

解答: 我們打印LBHPerson及其子類

打印LBHPerosn類和子類的信息轻纪,發(fā)現(xiàn)NSKVONotifying_LBHPerson派生類并沒有移除油额。

中間類一旦生成,沒有移除刻帚,沒有銷毀潦嘶,這樣可以減少頻繁的添加操作。

【總結(jié)】

1崇众、添加addObserver時(shí)掂僵,創(chuàng)建了派生類,派生類是當(dāng)前類的子類顷歌,重寫了被監(jiān)聽屬性的setter方法锰蓬,并將當(dāng)前類的isa指向了派生類

2眯漩、賦值: 派生類重寫了被監(jiān)聽屬性的setter方法芹扭,在派生類的setter方法觸發(fā)時(shí):在willChange之后didChange之前,調(diào)用父類屬性settter方法赦抖,完成父類屬性的賦值舱卡。

3、移除: 在removeObserver后队萤,isa從派生類指回本類轮锥, 但創(chuàng)建過的派生類,會(huì)一直在內(nèi)存中不會(huì)銷毀要尔。

4 自定義 KVO

相關(guān)代碼下載

自定KVO的目的

1交胚、模擬系統(tǒng)實(shí)現(xiàn)KVO原理
2、自動(dòng)移除觀察者
3盈电、實(shí)現(xiàn)響應(yīng)式+函數(shù)式

核心流程:

  • 1蝴簇、addObserver時(shí):
    • 1.1 驗(yàn)證setter方法是否存在
    • 1.2 注冊KVO派生類
    • 1.3 派生類添加setter、class匆帚、dealloc方法
    • 1.4 isa指向派生類
    • 1.5 保存信息
  • 2熬词、觸發(fā)setter方法時(shí):
    • 2.1 willChange
    • 2.1 消息轉(zhuǎn)發(fā)(設(shè)置原類的屬性值)
    • 2.2 didChange
  • 3、removeObserver
    • 3.1 手動(dòng)移除
    • 3.2 自動(dòng)移除

示例相關(guān)文件說明

本示例中:

  • ViewController:有導(dǎo)航控制器的根視圖吸重,點(diǎn)擊Push按鈕可跳轉(zhuǎn)PushViewController互拾;
  • LGViewController:測試控制器,實(shí)現(xiàn)LBHPerson屬性添加觀察者嚎幸、觸發(fā)屬性變化颜矿、移除觀察者等功能;
  • LBHPerosn:繼承自NSObject嫉晶,具備name和nickName屬性的類
  • NSObject+LBHKVO:重寫KVO的相關(guān)功能

1 添加addObserver

// 添加觀察者
- (void)lbh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LBHKVOBlock)block {
    
    // 1.1 驗(yàn)證setter方法是否存在
    [self judgeSetterMethodFromKeyPath:keyPath];

    // 1.2 + 1.3 注冊KVO派生類(動(dòng)態(tài)生成子類) 添加方法
    Class newClass = [self creatChildClassWithKeyPath:keyPath];

    // 1.4 isa的指向: LBHKVONotifying_LBHPerosn
    object_setClass(self, newClass);

    // 1.5. 保存信息
    LBHInfo * info = [[LBHInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
    [self associatedObjectAddObject:info];
}
1.1 驗(yàn)證setter方法是否存在
//MARK: -  驗(yàn)證是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *) keyPath {
    Class class    = object_getClass(self);
    SEL setterSelector  = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(class, setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName: NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"當(dāng)前%@沒有setter方法", keyPath]
                                     userInfo:nil];
    }
}

因?yàn)槲覀儽O(jiān)聽的是setter方法骑疆,所以當(dāng)前被監(jiān)聽屬性必須具備setter方法

1.1.1 setterForGetter

getter名稱中讀取setter 田篇, key => setKey

static NSString * setterForGetter(NSString * getter) {
   
   if (getter.length <= 0) return nil;
   
   NSString * setterFirstChar = [getter substringToIndex:1].uppercaseString;
   
   return [NSString stringWithFormat:@"set%@%@:", setterFirstChar, [getter substringFromIndex:1]];
   
}
1.1.2 getterForSetter

set方法獲取getter方法, setKey => key

#pragma mark - 從set方法獲取getter方法的名稱 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
1.2 注冊KVO派生類
- (Class)creatChildClassWithKeyPath: (NSString *) keyPath {
    
    // 1. 類名
    NSString * oldClassName = NSStringFromClass([self class]);
    NSString * newClassName = [NSString stringWithFormat:@"%@%@",LBHKVOPrefix,oldClassName];
    
    // 2. 生成類
    Class newClass = NSClassFromString(newClassName);
    
    // 2.1 不存在箍铭,創(chuàng)建類
    if (!newClass) {
        
        // 2.2.1 申請內(nèi)存空間 (參數(shù)1:父類泊柬,參數(shù)2:類名,參數(shù)3:額外大姓┗稹)
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        
        // 2.2.2 注冊類
        objc_registerClassPair(newClass);
        
    }
    
    // 2.2.3 動(dòng)態(tài)添加set函數(shù)
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSel); //為了保證types和原來的類的Imp保持一致兽赁,所以從[self class]提取
    const char * setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSel, (IMP)ht_setter, setterTypes);
    
    // 2.2.4 動(dòng)態(tài)添加class函數(shù) (為了讓外界調(diào)用class時(shí),不看到中間類冷守,看到的時(shí)原來的類刀崖,isa需要指向原來的類)
    SEL classSel = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSel);
    const char * classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSel, (IMP)ht_class, classTypes);
    
    // 2.2.5 動(dòng)態(tài)添加dealloc函數(shù)
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
    const char * deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSel, (IMP)ht_dealloc, deallocTypes);
    
    return newClass;
}
1.2.1 LBHKVO類的命名前綴,關(guān)聯(lián)屬性的key:
static NSString *const kLBHKVOPrefix = @"LBHKVONotifying_";
static NSString *const LBHKVOAssiociakey = @"kLBHKVO_AssiociateKey";
1.3 派生類添加setter拍摇、class蒲跨、dealloc方法
1.3.1 setter方法
static void lbh_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"新值:%@", newValue);
    // 讀取getter方法(屬性名)
    NSString * keyPath = getterForSetter(NSStringFromSelector(_cmd));
    // 獲取舊值
    id oldValue = [self valueForKey:keyPath];

    // 1. willChange在此處觸發(fā)(本示例省略)

    // 2. 調(diào)用父類的setter方法(消息轉(zhuǎn)發(fā))
    // 修改objc_super的值,強(qiáng)制將super_class設(shè)置為父類
    void(* lbh_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;

    // 創(chuàng)建并賦值
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };

    lbh_msgSendSuper(&superStruct, _cmd, newValue);
//    objc_msgSendSuper(&superStruct, _cmd, newValue);
    
    // 3. didChange在此處觸發(fā)(本示例省略)
    NSMutableArray * array = objc_getAssociatedObject(self, (__bridge const void * _Nonnull) LBHKVOAssiociakey);
    
    for (LBHKVOInfo * info in array) {
        if([info.keyPath isEqualToString:keyPath] && info.observer){
            // 3.1 block回調(diào)的方式
            if (info.hanldBlock) {
                info.hanldBlock(info.observer, keyPath, oldValue, newValue);
            }
//            // 3.2 調(diào)用方法的方式
            if([info.observer respondsToSelector:@selector(lbh_observeValueForKeyPath: ofObject: change: context:)]) {
                [info.observer lbh_observeValueForKeyPath:keyPath ofObject:self change:@{keyPath: newValue} context:NULL];
            }
        }
    }
    
}

給屬性賦值時(shí)會(huì)觸發(fā)setter授翻,有3個(gè)需要注意的點(diǎn):

  • 1或悲、賦值前: 本案例沒實(shí)現(xiàn)賦值前的willChange事件。因?yàn)榕c下面的didChange方式一樣堪唐,只是狀態(tài)不同巡语;

  • 2、賦值: 調(diào)用父類的setter方法淮菠,我們是通過objc_msgSendSuper進(jìn)行調(diào)用男公。我們重寫objc_super的結(jié)構(gòu)體并完成receiversuper_class的賦值

直接使用objc_msgSendSuper調(diào)用合陵,會(huì)報(bào)參數(shù)錯(cuò)誤

有兩種解決方法

1枢赔、 objc_msgSend的檢查關(guān)閉:target --> Build Setting --> Enable Strict Checking of objc_msgSend Calls 設(shè)置為NO

截屏2020-12-24 上午9.41.41.png

2、 新創(chuàng)建一個(gè)lbh_msgSendSuper引用objc_msgSendSuper拥知,這樣編譯就不會(huì)報(bào)錯(cuò)踏拜,不需要關(guān)閉編譯檢查

  • 3、賦值后:我們有2種方法可以實(shí)現(xiàn)didChange事件低剔,告知外部

1速梗、 和蘋果官方一樣,NSObject+LBHKVO.h文件中對外公開lbh_observeValueForKeyPath函數(shù)

在調(diào)用的地方實(shí)現(xiàn)這個(gè)方法即可

但是此方法方式讓代碼很分散襟齿,開發(fā)者需要在2個(gè)地方同時(shí)實(shí)現(xiàn)lbh_addObserverlbh_observeValueForKeyPath兩個(gè)函數(shù)

2姻锁、 響應(yīng)式 + 函數(shù)式,直接在lbh_addObserver中添加Block回調(diào)代碼塊猜欺,需要響應(yīng)的時(shí)候位隶,我們直接響應(yīng)block即可

.h 文件

.m 文件

在調(diào)用lbh_addObserver函數(shù)時(shí),直接實(shí)現(xiàn)block響應(yīng)就行开皿。這樣完成了代碼的內(nèi)聚涧黄。

1.3.2 class方法

重寫class方法篮昧,主要是讓外界讀取時(shí),看不到KVO派生類弓熏,輸出的是原來的類

Class lbh_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self)); // 返回當(dāng)前類的父類(原來的類)
}
1.3.3 dealloc方法

重寫了dealloc方法,并將isa從KVO衍生類指回了原來的類

// 重寫dealloc方法
void lbh_dealloc(id self, SEL _cmd) {
    
    NSLog(@"%s KVO派生類移除了",__func__);
    
    Class superClass = [self class];
    object_setClass(self, superClass);
}
1.4 isa指向派生類
// 1.4 isa的指向: LBHKVONotifying_LBHPerosn
object_setClass(self, newClass);
1.5 保存信息

創(chuàng)建Info實(shí)例保存觀察數(shù)據(jù)
讀取關(guān)聯(lián)屬性數(shù)組(當(dāng)前所有觀察對象) --->
如果關(guān)聯(lián)屬性數(shù)組不存在糠睡,就創(chuàng)建一個(gè)
(使用OBJC_ASSOCIATION_RETAIN_NONATOMIC沒關(guān)系挽鞠,因?yàn)殛P(guān)聯(lián)屬性不存在強(qiáng)引用,只是記錄類名和屬性名)
case1 如果被監(jiān)聽對象已存在狈孔,直接跳出
case2 添加監(jiān)聽對象

LBHKVOInfo * info = [[LBHKVOInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
[self associatedObjectAddObject:info];


//MARK: - 關(guān)聯(lián)屬性添加對象
- (void)associatedObjectAddObject:(LBHKVOInfo *)info {
    
    NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey);
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self,  (__bridge const void * _Nonnull)LBHKVOAssiociakey, mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    for (LBHKVOInfo * tempInfo in mArray) {
        if ([tempInfo isEqual:info]) return;
    }
    
    [mArray addObject:info];
}

2. 觸發(fā)setter方法時(shí)

1.3.1 setter方法中已描述清晰信认。
主要是三步:willChange --> 設(shè)置原類屬性 --> didChange

3. removeObserver移除觀察者

3.1 手動(dòng)移除

移除指定被監(jiān)聽屬性,如果都被移除了均抽,就將isa指回父類

- (void)lbh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    NSMutableArray * observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey);
    
    if (observerArr.count <= 0) return;
    
    for (LBHKVOInfo * info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            // 移除當(dāng)前info
            [observerArr removeObject:info];
            // 重新設(shè)置關(guān)聯(lián)對象的值
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)LBHKVOAssiociakey, observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    
    // 全部移除后嫁赏,isa指回父類
    if (observerArr.count <= 0) {
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

問題:手動(dòng)把所有被監(jiān)聽屬性都移除,觸發(fā)isa指回本類油挥,那dealloc觸發(fā)lbh_dealloc觸發(fā)時(shí)潦蝇,isa會(huì)不會(huì)指向父類的父類了?

解答:不會(huì)深寥。因?yàn)閕sa指回本類后攘乒,KVO派生類對象已被釋放。不會(huì)再進(jìn)入ht_dealloc惋鹅。
這也是為什么將isa指回本類,會(huì)自動(dòng)移除觀察者闰集。因?yàn)榕缮悓ο笠驯会尫殴炼铮涗浀年P(guān)聯(lián)屬性也自動(dòng)被釋放。

3.2 自動(dòng)移除

1.3.3 dealloc方法中已描述清晰

資料

FBKVOController

FaceBook出品武鲁,支持blockaction回調(diào)爽雄,支持自動(dòng)移除觀察者

分析參考

GNU源碼

可參閱它推測還原的Foundation庫

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沐鼠,隨后出現(xiàn)的幾起案子盲链,更是在濱河造成了極大的恐慌,老刑警劉巖迟杂,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刽沾,死亡現(xiàn)場離奇詭異,居然都是意外死亡排拷,警方通過查閱死者的電腦和手機(jī)侧漓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來监氢,“玉大人布蔗,你說我怎么就攤上這事藤违。” “怎么了纵揍?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵顿乒,是天一觀的道長。 經(jīng)常有香客問我泽谨,道長璧榄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任吧雹,我火速辦了婚禮骨杂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雄卷。我一直安慰自己搓蚪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布丁鹉。 她就那樣靜靜地躺著妒潭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪揣钦。 梳的紋絲不亂的頭發(fā)上杜耙,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天,我揣著相機(jī)與錄音拂盯,去河邊找鬼佑女。 笑死,一個(gè)胖子當(dāng)著我的面吹牛谈竿,可吹牛的內(nèi)容都是我干的团驱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼空凸,長吁一口氣:“原來是場噩夢啊……” “哼嚎花!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起呀洲,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤紊选,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后道逗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兵罢,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年滓窍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卖词。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吏夯,死狀恐怖此蜈,靈堂內(nèi)的尸體忽然破棺而出即横,到底是詐尸還是另有隱情,我是刑警寧澤裆赵,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布东囚,位于F島的核電站,受9級特大地震影響战授,放射性物質(zhì)發(fā)生泄漏页藻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一陈醒、第九天 我趴在偏房一處隱蔽的房頂上張望惕橙。 院中可真熱鬧瞧甩,春花似錦钉跷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至朦促,卻和暖如春膝晾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背务冕。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工血当, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人禀忆。 一個(gè)月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓臊旭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親箩退。 傳聞我的和親對象是個(gè)殘疾皇子离熏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容