iOS進階-11 KVO

相信讀者對KVO的使用應該已經很熟練了,本文主要講KVO的一些注意點和原理,對詳細的使用不做過多的展示。

日常使用注意點

context 參數(shù)

1.context填NULL還是nil?先看源代碼:

[self addObserver:self.person forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:<#(nullable void *)#>]

下面context應該填什么?日常我們一般不會用到context這個參數(shù),一般會填nil或者NULL;那么到底是應該填nil,還是NULL;答案是:NULL;
why? 看看在context的placehold上顯示的是context:<#(nullable void *)#>,是一個可空void *指針,既然不是oc對象,那么就應該填NULL。我們再看看KVO文檔,文檔中有這么一點段話

Context
,很明確了吧,當我們不需要這個context參數(shù)時,你可以填寫NULL;
2.這個context的作用?show U code

######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;

@end

######Animal
@interface Animal : NSObject
@property (nonatomic,copy) NSString *name;
@end

######ViewController
#import "ViewController.h"
#import "Person.h"
#import "Animal.h"

static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;


@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    
    self.person = [Person new];
    self.animal = [Animal new];
    
    [self.person  addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    [self.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:AnimalNameContext];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    //常規(guī)做法
//    if ([object isEqual:self.person]) {
//        if ([keyPath isEqualToString:@"name"]) {
//            <#statements#>
//        } else if ([keyPath isEqualToString:@"nickName"]) {
//            <#statements#>
//        }
//    } else if ([object isEqual:self.animal]) {
//        if ([keyPath isEqualToString:@"name"]) {
//            <#statements#>
//        }
//    }
    
    //context做法
    if (context == PersonNameContext) {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"name=%@",self.person.name);
        }
    } else if (context == AnimalNameContext ) {
        if ([keyPath isEqualToString:@"name"]) {
            
        }
    } else if (context == PersonNickNameContext) {
        if ([keyPath isEqualToString:@"nickName"]) {
            NSLog(@"nickName=%@",self.person.nickName);
        }
    }
}
static int a = 1;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    a ++;
    self.person.name = [NSString stringWithFormat:@"name+%d",a];
    self.person.nickName = [NSString stringWithFormat:@"nickName+%d",a];
}

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

@end

所以:使用context參數(shù)會更加的便捷高效安全

觀察者要移除

日常開發(fā)中,一定要寫移除觀察者的代碼,如果沒有移除,會有造成野指針,成為崩潰隱患。

多次修改代碼高效設置

eg:假設在上面context 參數(shù)內容段中,因為需求Person類中的name屬性昨天是需要觀察的,而今天一上班,產品經理說需求又改了,又不需要再觀察了這個那么屬性。通常遇到這種情況的時候,我們會刪掉(注銷)之前寫好的代碼,然后又過了幾天產品經理要求改回來;遇到這個情況估計你的內心會有一萬匹草泥馬跑過。因為KVO代碼量分散且并不少,這種操作其實讓人很煩;這個時候你可以在Person類中重寫這個方法automaticallyNotifiesObserversForKey:(是否自動觀察屬性)這樣操作:只會觀察nickName,而不會觀察name

#import "Person.h"

@implementation Person

// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

@end

使用automaticallyNotifiesObserversForKey:根據(jù)key去判斷,可以讓程序更加健壯;

多個因素影響

當被觀察的對象受到其他多個因素影響時;
eg:下載進度受當前下載量和總下載量的影響,但是我們需要觀察的是進度,可以使用keyPathsForValuesAffectingValueForKey:

#####DownLoadManager
@interface DownLoadManager : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

@end
#import "DownLoadManager.h"

@implementation DownLoadManager
// 下載進度 -- 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;
}

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

@end

######ViewController
#import "ViewController.h"
#import "DownLoadManager.h"

@interface ViewController ()
@property (nonatomic,strong) DownLoadManager *downLoadManager;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.downLoadManager = [DownLoadManager new];
    self.downLoadManager.writtenData = 10;
    self.downLoadManager.totalData = 100;
    
    //  多個因素影響 - 下載進度 = 當前下載量 / 總量
    [self.downLoadManager addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"downloadProgress"]) {
        NSLog(@"downloadProgress = %@",self.downLoadManager.downloadProgress);
    }
}

//點擊屏幕
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.downLoadManager.writtenData += 10;
    self.downLoadManager.totalData += 5;
}

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

@end

點擊屏幕打印結果:

2020-02-21 11:08:38.002797+0800 KTest[5745:107244] downloadProgress = downloadProgress =20.00%
2020-02-21 11:08:38.002912+0800 KTest[5745:107244] downloadProgress = downloadProgress =19.05%
2020-02-21 11:08:39.408895+0800 KTest[5745:107244] downloadProgress = downloadProgress =28.57%
2020-02-21 11:08:39.409002+0800 KTest[5745:107244] downloadProgress = downloadProgress =27.27%
2020-02-21 11:08:40.105935+0800 KTest[5745:107244] downloadProgress = downloadProgress =36.36%
2020-02-21 11:08:40.106029+0800 KTest[5745:107244] downloadProgress = downloadProgress =34.78%
可變數(shù)組

觀察可變數(shù)組的增刪改查時,不要直接使用addObject:或者removeObject:直接使用會崩潰,需要先通過mutableArrayValueForKey:獲得數(shù)組對象,才能進一步操作;原因:iOS默認不支持對數(shù)組的KVO,KVO是通過KVC實現(xiàn)的,普通方式監(jiān)聽的對象的地址的變化,而數(shù)組地址不變,而是里面的值發(fā)生了改變;
eg:

######Person
@interface Person : NSObject
@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end

######ViewController
#import "ViewController.h"
#import "Person.h"
#import "Animal.h"
#import "DownLoadManager.h"

static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;


@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;
@property (nonatomic,strong) DownLoadManager *downLoadManager;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [Person new];
    self.animal = [Animal new];
     self.person.studentNameArray = [NSMutableArray arrayWithCapacity:1];
     [self.person addObserver:self forKeyPath:@"studentNameArray" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"studentNameArray"]) {
        NSLog(@"studentNameArray = %@",self.person.studentNameArray);
    }
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {   
    // KVO 建立在 KVC上
//    [self.person.studentNameArray addObject:@"lee"];不要這么做,會崩潰
    //使用mutableArrayValueForKey獲取數(shù)組對象
    [[self.person mutableArrayValueForKey:@"studentNameArray"] addObject:@"lee"];
    [[self.person mutableArrayValueForKey:@"studentNameArray"] removeObject:@"lee"];
}

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

@end

KVO原理

Automatic key-value observing is implemented using a technique called isa-swizzling.
這段話來自官方文檔:KVO是通過isa-swizzling實現(xiàn)的;
那具體是如何isa-swizzling的呢?

1.動態(tài)的生成子類:NSKVONotifying_XXX

驗證:

######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *nickName;
@end

######KVOIMPViewController
#import "KVOIMPViewController.h"
#import <objc/runtime.h>
#import "Person.h"

@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;

@end

@implementation KVOIMPViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];
    
    self.person = [[Person alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    int a ;
}

[self.person addObserver:self forKey...int a兩個地方分別打上斷點;使用LLDB調試:

(lldb) po object_getClassName(self.person)
"Person"
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"

可以看到在運行了addObserver:(NSObject *)observer forKeyPath:...之后,當前Person類變成了NSKVONotifying_Person
上面只能說明生成了NSKVONotifying_Person類但不能說明是Person類的子類;繼續(xù)驗證

- (void)viewDidLoad {
    [super viewDidLoad];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
[self printClasses:[Person class]];
}

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    // 注冊類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個數(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);
}

打印結果

KTest[10265:237411] classes = (
    Person,
    "NSKVONotifying_Person"
)

可以看到Person類下面確實還有子類NSKVONotifying_Person
內部關系圖:

KVO

2.動態(tài)子類生重寫了很多方法

打印觀察前后,類方法的變化

######Person
@interface Person : NSObject
//@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;
//@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end

######KVOIMPViewController
#import "KVOIMPViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;
@end

@implementation KVOIMPViewController

- (void)viewDidLoad {
    [super viewDidLoad];    
    self.person = [[Person alloc] init];
    [self printClassAllMethod:NSClassFromString(@"Person")];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
}

#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************%@類的方法list",NSStringFromClass(cls));
    unsigned int count = 0;
    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(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

打印結果:

KTest[9724:222902] *********************Person類的方法list
KTest[9724:222902] .cxx_destruct-0x106ace0d0
KTest[9724:222902] nickName-0x106ace060
KTest[9724:222902] setNickName:-0x106ace090
KTest[9724:222902] *********************NSKVONotifying_Person類的方法list
KTest[9724:222902] setNickName:-0x7fff25721c7a
KTest[9724:222902] class-0x7fff2572073d
KTest[9724:222902] dealloc-0x7fff257204a2
KTest[9724:222902] _isKVOA-0x7fff2572049a

從打印可以看到:
NSKVONotifying_Person重寫了setNickName class dealloc _isKVOA方法;
1.setNickName:
重寫的setNickName:方法內部大概這么實現(xiàn)的

@implementation Person
- (void)setNickName:(NSString *)nickName {
    [self willChangeValueForKey:@"nickName"];
    _nickName = nickName;
    [self didChangeValueForKey:@"nickName"];
}
@end

2.class
為何重寫class方法:為了讓外界感受不到子類NSKVONotifying_Person的生成

  1. dealloc
    重寫這個方法應該是為了在對象銷毀的時候做一些操作吧,尚未探究
  2. _isKVOA
    _isKVOA:判斷是否是KVO生成的類
3.移除觀察之后 isa指針是否指回來?

斷點調試:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];
    
    self.person = [[Person alloc] init];
    NSLog(@"*******添加觀察者之前");
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"*******添加觀察者之后");
    [self.person removeObserver:self forKeyPath:@"nickName"];
    NSLog(@"*******移除觀察者之后");
}

打印結果:

KTest[10766:251988] *******添加觀察者之前
(lldb) po object_getClassName(self.person)
"Person"

KTest[10766:251988] *******添加觀察者之后
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"

KTest[10766:251988] *******移除觀察者之后
(lldb) po object_getClassName(self.person)
"Person"

可以看到,person對象的類又指回了Person

移除觀察者后動態(tài)子類會被銷毀嗎?不會。

驗證:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    NSLog(@"*******添加觀察者之前");
    [self printClasses:[Person class]];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"*******添加觀察者之后");
    [self printClasses:[Person class]];
    [self.person removeObserver:self forKeyPath:@"nickName"];
    NSLog(@"*******移除觀察者之后");
    [self printClasses:[Person class]];
}

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    // 注冊類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個數(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);
}

打印結果:

KTest[11026:259919] *******添加觀察者之前
KTest[11026:259919] classes = (
    Person
)
KTest[11026:259919] *******添加觀察者之后
KTest[11026:259919] classes = (
    Person,
    "NSKVONotifying_Person"
)
KTest[11026:259919] *******移除觀察者之后
KTest[11026:259919] classes = (
    Person,
    "NSKVONotifying_Person"
)

可以看到在移除觀察者之后沒有移除動態(tài)子類NSKVONotifying_Person

總結

日常注意點

1.context參數(shù)的在不使用時推薦填寫NULL,其功能:程序更加便捷、高效、安全
2.觀察者要移除
3.多個因素影響時可以使用:keyPathsForValuesAffectingValueForKey:
4.針對經常需要改動的代碼可以使用:automaticallyNotifiesObserversForKey:方法對key進行選擇處理
5.觀察可變數(shù)組時,要使用mutableArrayValueForKey:

原理

1.KVO通過isa-swizzling實現(xiàn)
2.動態(tài)的生成子類:NSKVONotifying_XXX
3.主要是觀察setXXX方法;內部重寫
4.還重寫了setXXX class dealloc _isKVOA方法;
5.移除觀察之后 isa指針會重新指回來
6.移除觀察者后動態(tài)子類會被銷毀嗎?不會。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • KVO概述 鍵值觀察Key-Value-Observer就是觀察者模式。 觀察者模式的定義:一個目標對象管理所有依...
    小希嘻閱讀 694評論 0 0
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進度條,當時做android時候處理的不太好,由于...
    DaZenD閱讀 3,096評論 0 26
  • 面試驅動技術合集(初中級iOS開發(fā)),關注倉庫,及時獲取更新 Interview-series KVO KVO是k...
    小蠢驢打代碼閱讀 764評論 0 8
  • 一、概述 KVO,即:Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,198評論 11 33
  • KVC和KVO在實際的運用中是很常見的。所以了解它的底層實現(xiàn)原理是非常不錯的一件事。 KVC(NSKeyValue...
    HelloAda閱讀 2,606評論 0 3

友情鏈接更多精彩內容