15, iOS underlying analysis - KVO


KVC, KVO is often used in iOS is often asked in the interview. Today to explore what KVO.

What is the KVO?

KVO: key observation mechanism, which provides a way to observe a property changes
KVO stands KeyValueObserving, is an event notification mechanism provided by Apple. It allows objects to monitor changes in the properties of another object, and an event is received at the time of change. Since the implementation mechanism of KVO, so the property will play a role, usually inherited from NSObject objects are supported by default KVO.
And KVO iOS NSNotificationCenter are in the observer model one implementation.

The difference is that the relative relationship between the observer and observer:

  • KVO is one of
  • NSNotificationCenter-many

KVO non-invasive to the object being monitored, without modifying its internal code to implement monitoring.
KVO can monitor changes in the individual attributes can also monitor changes in collection of objects. Obtain a proxy object methods, when the internal proxy object changes, the callback method KVO will listen: by KVC's mutableArrayValueForKey. NSArray contains a collection of objects and NSSet.

First, the basic usage of KVO

Use KVO divided into three steps (KVO Trilogy)

  1. Registration observer
  2. Implement a callback
  3. Remove observer
#import "LJLKVOViewController.h"
#import <objc/runtime.h>
#import "LJLKVOPerson.h"
#import "LJLKVODownloader.h"

static void * personNameContext = &personNameContext;//观察person.name 的 context
static void * personKvoArrayContext = &personKvoArrayContext;//观察person.kvoArray 的 context

@interface LJLKVOViewController ()
@property(nonatomic, strong)LJLKVOPerson * person;
@end

1.1, registered observer
    observed object call the following methods to register an observer:

self.person = [[LJLKVOPerson alloc] init];
//注册观察者
[self.person addObserver:self
              forKeyPath:@"name"
                 options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                context:personNameContext];

context effect : to quickly locate key observation.
If two keyPath observed when the same name is not easy to distinguish, on the need to distinguish context.
But also through such a form you can know which property is observed what classes, doing so reduces the nesting is determined in the callback multilayer, safer and more efficient. Function is similar to that tag.
If you do not you can fill in NULL, because this place is so filled nullable void * NULL, if the id is filled nil. You can also fill nil, because xcode will help us to turn to nil NULL when compiled.

1.2, implement the callback
registration, observers need to implement the following callbacks to receive notifications:

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                      context:(void *)context
{
//这里就可以通过注册的时候的context来快速判断出观察的哪个类的哪个属性变化触发的回调
    if (context == personNameContext) {
        NSLog(@"name change : %@",change);
    }else{
        NSLog(@"change - %@",change);
    }

//change - {
//        kind = 1; 这个就是观察的类型,1是set
//        new = "0.100000";
/*
     观察类型枚举 (被观察者变更的类型)
     NSKeyValueChangeSetting = 1,       设置 例如观察的是NSString 修改的时候就是这个类型
     NSKeyValueChangeInsertion = 2,     插入 例如观察的是NSMutableArray 添加数据的时候
     NSKeyValueChangeRemoval = 3,       删除
     NSKeyValueChangeReplacement = 4,   替换
*/
}

1.3, remove the observer
    if no longer continue to observe, be sure to remove the observer, or an exception may occur. Develop good habits to avoid some hidden crash.
    And if you do not remove it, and come again to re-register. This is the case when multiple calls occur. If the viewer is using a single embodiment, it is not removed there is also the observation target will not be released, there will be a pointer field, will collapse.

[self.student removeObserver:self forKeyPath:@"name"];

More complete three steps as follows, of course, this does not necessarily have to be written in a single embodiment observer, according to their needs.

#import "LGPerson.h"
{
//需要设置为公共的,否者不能在外部访问。要在外部赋值这个实例变量必须加 @public 公有.KVO不能监听实例变量
    @public
    NSString *nikcName;
}
@interface LGStudent : LGPerson
+ (instancetype)shareInstance;
@end

//------------------------------------------------
#import "LGStudent.h"
@implementation LGStudent
static LGStudent* _instance = nil;
+ (instancetype)shareInstance{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init] ;
    }) ;
    return _instance ;
}
@end

//------------------------------------------------
#import "LJLKVOViewController.h"
#import "LGStudent.h"

@interface LJLKVOViewController ()
@property (nonatomic, strong) LGStudent *student;
@end

@implementation LJLKVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.student = [LGStudent shareInstance];
//1、注册观察者
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
//触摸事件修改student.name 触发监听回调
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = @"hello word";
//    外部访问实例变量 需要设置为公共
    self.student->nikcName = @"ljl";
}
#pragma mark - KVO回调
//2、实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"LJLKVOViewController :%@",change);
}

//3、移除观察者
- (void)dealloc{
    [self.student removeObserver:self forKeyPath:@"name"];
}
@end

1.4, a set of attributes of type

    When a collection of observations must be based on the KVC. By KVC more convenient to access directly.
    LJLKVOPerson.h add a variable array property

@property(nonatomic, strong) NSMutableArray * kvoArray;

Sign up KVO, and modify kvoArray

//    一定要初始化,否者 kvoArray为nil addObject:的时候就崩溃了
    self.person.kvoArray = [NSMutableArray array];
    [self.person addObserver:self
                  forKeyPath:@"kvoArray"
                     options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                     context:NULL];

    [self.person.kvoArray addObject:@"123"];
//    上面这样这样写是无法触发KVO 回调的,因为addObject:方法是不走setter 的。 而KVO监听是setter方法.需要将上述向数组添加元素的方法修改一下
    [[self.person mutableArrayValueForKey:@"kvoArray"] addObject:@"123"];
//    这样做之后 self.person.kvoArray 指向一个新的数组对象,相当于:
//    NSMutableArray * tmp = [NSMutableArray arrayWithArray:self.person.kvoArray];
//    [tmp addObject:@"123"];
//    self.person.kvoArray = tmp;
//    所以能触发KVO回调

1.5 observe multiple related attributes
    such as a LJLKVODownloader class, used to simulate the download, there are three attributes totalBytes, completedBytes, and the percentage of progress progress:
    in the UI layer we are only concerned about the progress, but progress is subject to two other properties that affect this when required LJLKVODownloader rewrite two methods:

#import <Foundation/Foundation.h>

@interface LJLKVODownloader : NSObject
@property(nonatomic, assign) unsigned long long totalBytes;//总字节
@property(nonatomic, assign) unsigned long long completedBytes;//完成字节
@property(nonatomic, copy) NSString * progress;//进度
@end
#import "LJLKVODownloader.h"

@implementation LJLKVODownloader
//返回属性的一组键路径,这些属性的值会影响键控属性的值
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"progress"]) {
        NSArray * dependKeys = @[@"totalBytes",@"completedBytes"];
//        通过从数组中添加对象来设置
        keyPaths = [keyPaths setByAddingObjectsFromArray:dependKeys];
    }
    return keyPaths;
}

- (NSString *)progress{
    if (self.totalBytes == 0 || self.completedBytes == 0) {
        return @"0%";
    }
    double progress = (double)self.completedBytes/(double)self.totalBytes*100;
    if (progress > 100) {
        progress = 100;
    }
    return [NSString stringWithFormat:@"%d%%",(int)ceil(progress)];
}
@end
    LJLKVODownloader * downloader = [[LJLKVODownloader alloc] init];
    downloader.totalBytes = 205;
    [downloader addObserver:self
                 forKeyPath:@"progress"
                    options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                    context:NULL];
    downloader.completedBytes = 64;
    downloader.completedBytes = 168;

The results are as follows monitor callback

2020-03-08 00:34:53.547235+0800 filedome[31182:820584] change : {
    kind = 1;
    new = "32%";
    old = "0%";
}
2020-03-08 00:34:53.547581+0800 filedome[31182:820584] change : {
    kind = 1;
    new = "82%";
    old = "32%";
}

 

Second, observe the automatic or manual switch

    Before KVO understand it, we need to understand KVC.
    KVC will be called in the setter or getter, if not found, then call the class method + accessInstanceVariablesDirectly (direct access instance variables) if returns YES, go find a member variable
    KVO is similar mechanisms, there are three interfaces in KVO interface:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;(自动通知观察者)

+ automaticallyNotifiesObserversForKey: Default returns YES, the middle class created dynamically rewrite the setter, speculation in the property before and after the modification were called
 -willChangeValueForKey:
 and -didChangeValueForKey: similar methods to achieve the purpose to inform the viewer.
 If the subclass overrides + automaticallyNotifiesObserversForKey: and return to NO, you can not trigger the automatic KVO notification mechanism, but we can manually call -willChangeValueForKey: and -didChangeValueForKey: to trigger the KVO callbacks.

// manual observation, whether it is open or closed automatic automatic observation will observe callback
// callback is triggered didChangeValueForKey:
// automatic observation is, by default, the system is added will help us and did methods. If you write it again in his hand, then it will trigger twice, that is, take the time did triggers a callback method

#import "LJLKVOPerson.h"
@implementation LJLKVOPerson

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;//自动观察
}

- (void)setName:(NSString *)name{
//    手动观察。不管是打开自动观察还是关闭自动观察都会回调
// 触发回调的是 didChangeValueForKey 
// 自动观察 也就是默认情况下,系统是帮我们添加了will 和 did 方法。如果在自己手动写一遍的话就会触发两次,也就是走一次did 方法就会触发一次回调
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];//这个方法里面触发回调
}
@end

 

Third, the realization of the principle of KVO

 KVO official document automatically say KVO observation is to use technology to achieve isa-swizzling keys

  1. Dynamically generated subclass - NSKVONotifying_xxx
  2. Dynamically add setter methods
  3. Dynamically add class method
  4. Dynamically add dealloc method
  5. Open the manual observation
  6. Message to our original class newValue
  7. Message - response callback method

    2.2-principle

A verification:

 self.person = [[LJLKVOPerson alloc] init];
//1、下一行下断点
 [self.person addObserver:self
               forKeyPath:@"name"
                  options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                  context:personNameContext];
//2、下一行下断点
 self.person.name = @"liujilou";

Break point in the code where the top marked then LLBD debugging

//    LLBD调试
//    1位置断点
(lldb) po object_getClassName(self.person)
"LJLKVOPerson"

//    走到2位置断点
(lldb) po object_getClassName(self.person)
"NSKVONotifying_LJLKVOPerson"

Verify II:

    LJLKVOPerson * person = [[LJLKVOPerson alloc] init];
    [self printClasses:[LJLKVOPerson class]];
    [person addObserver:self
             forKeyPath:@"name"
                options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                context:NULL];
    [self printClasses:[LJLKVOPerson class]];
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
//        判断cls 是否等于classes[i] 的父类
        if (cls == class_getSuperclass(classes[i])) {
//            将cls 的所有子类添加进来
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}
2020-03-07 22:31:31.788378+0800 filedome[29720:765937] classes = (
LJLKVOPerson
                                                                  )
2020-03-07 22:31:31.799907+0800 filedome[29720:765937] classes = (
LJLKVOPerson,
"NSKVONotifying_LJLKVOPerson"
                                                                  )
    

Two verification by the above, we can know:

  1. Indeed generated a middle class: NSKVONotifying_LJLKVOPerson
  2. NSKVONotifying_LJLKVOPerson inheritance and LJLKVOPerson
  3. Isa self.person but also the object points to the middle class.

Began to study the subclass, the subclass dynamic studies: isa superclass cache_t bits - Method - Variable

Let's continue the process of verifying the back:

    self.person = [[LJLKVOPerson alloc] init];
    [self printClasses:[LJLKVOPerson class]];
    [self printClassAllMethod:[LJLKVOPerson class]];
    [self.person addObserver:self
             forKeyPath:@"name"
                options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                context:NULL];
    [self printClasses:[LJLKVOPerson class]];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LJLKVOPerson")];
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************");
    unsigned int count = 0;
//  需要导入  #import <objc/runtime.h>
    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);
}
    2020-03-07 22:59:18.260869+0800 filedome[30061:779679] classes = (
    LJLKVOPerson
                                                                      )
    2020-03-07 22:59:18.261029+0800 filedome[30061:779679] *********************
    2020-03-07 22:59:18.261129+0800 filedome[30061:779679] kvoArray-0x10b30e410
    2020-03-07 22:59:18.261252+0800 filedome[30061:779679] setKvoArray:-0x10b30e430
    2020-03-07 22:59:18.261348+0800 filedome[30061:779679] .cxx_destruct-0x10b30e470
    2020-03-07 22:59:18.261437+0800 filedome[30061:779679] name-0x10b30e3e0
    2020-03-07 22:59:18.261522+0800 filedome[30061:779679] setName:-0x10b30e340
    2020-03-07 22:59:18.272516+0800 filedome[30061:779679] classes = (
     LJLKVOPerson,
     "NSKVONotifying_LJLKVOPerson"
                                                                      )
    2020-03-07 22:59:18.272853+0800 filedome[30061:779679] *********************
    2020-03-07 22:59:18.273394+0800 filedome[30061:779679] setName:-0x10bfb0b5e
    2020-03-07 22:59:18.273505+0800 filedome[30061:779679] class-0x10bfaf592
    2020-03-07 22:59:18.273672+0800 filedome[30061:779679] dealloc-0x10bfaf336
    2020-03-07 22:59:18.273793+0800 filedome[30061:779679] _isKVOA-0x10bfaf32e

From the above print to know NSKVONotifying_LJLKVOPerson rewrite the parent LJLKVOPerson

  1. setName (setter)
  2. class
  3. dealloc
  4. _isKVOA

These methods. This place can print out their own some real method. Is not no way to print their own parent, it can prove to print out these methods are rewritten

Rewrite dealloc method to release resources.

_isKVOA rewrite this private method is used to indicate the class KVO is a mechanism for claiming class.


    Setter is observed

        when a class instance of a viewer is first registered, the system will do the following
    1, a dynamically generated intermediate of this class inherits from: NSKVONotifying_xxx
    2, isa modify the original object points to the intermediate class (ISA- Swizzling)
    . 3, subclass override -class method still returns to the original class, subclass and not
    4, a method override -dealloc
    5, rewriting the corresponding attribute setter keypath
    6, adding a method -_isKVOA

NSKVONotifying_xxx (LJLKVOPerson) of this class or method returns a LJLKVOPerson (). Tell us operation or LJLKVOPerson ()

dealloc where needed to release the call to release the middle class


Remove isa observe whether to come back?

Then remove the observation isa no longer point NSKVONotifying_xxx a point back xxx

verification:

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

Before and after the break point and then remove the console can print to know:

//在dealloc 移除观察者之前打断点
po object_getClassName(self.person)
"NSKVONotifying_LJLKVOPerson"

//移除观察者之后打断点,然后 LLBD
po object_getClassName(self.person)
"LJLKVOPerson"
//可以发现isa 又指向了 LJLKVOPerson

 

5: dynamic middle class after the removal of the child to see if destroyed?

Not to destroy, to facilitate the next use. If you remove the destroyed every time, registered create too slow.

verification:

Write register and observation of LJLKVOViewController ViewController in succession, and then return to ViewController page when removed observation.

Add the following code in ViewController. Traversal subclass LJLKVOPerson class after removing the observer to see if there NSKVONotifying_LJLKVOPerson.

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self printClasses:[LJLKVOPerson class]];
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    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(@"LJLKVOViewController:classes = %@", mArray);
}
2020-03-08 01:40:23.120880+0800 002---KVO原理探讨[31915:843163] LJLKVOViewController:classes = (
    LJLKVOPerson,
    "NSKVONotifying_LJLKVOPerson",
    LGStudent
)

It can be seen, after removing the observer, dynamic subclass will not be destroyed.

Briefly summarize:

When a class instance of first registration observer, the system will do the following
    1, a dynamically generated intermediate class inherits from the original class: NSKVONotifying_xxx
    2, isa modify the original object points to the intermediate class NSKVONotifying_xxx (isa-Swizzling)
    . 3 subclasses override class methods, still return to the original class, subclass rather than
    4, override dealloc method
    5, the corresponding attribute keypath override the setter (it is possible to observe properties, but not observed instance variables)
    6, was added a _ isKVOA method


     KVO Principle
     1: dynamically generating sub-categories: NSKVONotifying_xxx
     2: the setter is observed (it can be observed properties, but not observed instance variables)
     3: Dynamic Subclasses override many methods setNickName (the setter), class, the dealloc, _isKVOA
     . 4 : remove the observation point to come back when isa
     5: dynamic subclass does not destroy

 

KVO advantages and disadvantages

advantage:

  1. Possible to provide a simple way to achieve synchronization between the two objects
  2. We can create a non-object, that object's internal state change to respond and does not require changing the implementation of internal objects. The latest value can provide a view of the property and its previous value.
  3. With the key path to observe properties, it can also be observed nested objects
  4. Complete abstraction of the observation object, since no additional code to permit observations to be observed

Disadvantages:

  1. Because it is observed setter methods can only observe properties can not be observed instance variables
  2. Reconstruction of the property will lead us to observe the code is no longer available
  3. When released observer observer needs to be removed, or whether some hidden errors
Published 83 original articles · won praise 12 · views 180 000 +

Guess you like

Origin blog.csdn.net/shengdaVolleyball/article/details/104725189