iOS面试总结

KVO本质

2019-01-08  本文已影响48人  dandelionYD

面试题:

1.ios是用什么方式实现对一个对象的KVO?(KVO的本质是啥)
析:利用runtimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
    当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
    - willChangeValueForKey;
    - 父类原来的setter
    - didChangeValueForKey;
    内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObjectchange:context)

2.如何手动的触发KVO?
析:手动调用willChangeValueForKey和didChangeValueForKey;
 
3直接修改成员变量会 触发KVO?
 不会触发

简单使用:

#import "ViewController.h"

@interface Person : NSObject{
    @public
    int _age;
}
@property (nonatomic,strong)NSString  *name;
@property (nonatomic,assign)CGFloat  height;
@end

@implementation Person
@end


@interface ViewController ()
@property (nonatomic,strong)Person  *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc]init];
    self.person1.name = @"Jack";
    self.person1.height  = 175;
    self.person1->_age = 18;
   
     //添加KVO监听
     NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
     [self.person1 addObserver:self forKeyPath:@"height" options:options context:@"height-contenxt"];
     [self.person1 addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
     [self.person1 addObserver:self forKeyPath:@"_age" options:options context:@"_age-contenxt"];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.name = @"Jack2";
    self.person1.height = 180;
    self.person1->_age = 20;
}

// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
     NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

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


控制台Log:
======================
监听到<Person: 0x600003962700>的name属性值改变了 - {
    kind = 1;
    new = Jack2;
    old = Jack;
} - name-contenxt
监听到<Person: 0x600003962700>的height属性值改变了 - {
    kind = 1;
    new = 180;
    old = 175;
} - height-contenxt

发现:直接修改成员变量会 是不会触发KVO的.
//分析:NSDictionary<NSKeyValueChangeKey,id> *)


下面来一步步分析:

1.首先新建一个myPerson类如下:

//.h
#import <Foundation/Foundation.h>
@interface myPerson : NSObject
@property (nonatomic,strong)NSString  *name;
@end

//.m
#import "myPerson.h"
@implementation myPerson
-(void)willChangeValueForKey:(NSString *)key{
    NSLog(@"%s-%d",__FUNCTION__,__LINE__);
    [super willChangeValueForKey:key]
}

-(void)didChangeValueForKey:(NSString *)key{
     NSLog(@"%s-%d",__FUNCTION__,__LINE__);
    [super didChangeValueForKey:key]
}
@end

//viewcontroller.m
#import "ViewController2.h"
#import "myPerson.h"
#import <objc/runtime.h>

@interface ViewController2 ()
@property (nonatomic,strong)myPerson  *p;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"KVO本质";
    
    self.p  = [[myPerson alloc]init];
    self.p.name = @"jack";
    
    NSLog(@"添加之前“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
    NSLog(@"添加之前的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);

    //添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.p addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
    
    NSLog(@"添加之后“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
    NSLog(@"添加之后的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
    NSLog(@"End");
    
}

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

// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

- (void)dealloc {
    [self.p removeObserver:self forKeyPath:@"name"];
    NSLog(@"%s-%d",__FUNCTION__,__LINE__);
}
@end

分析:在 NSLog(@"End");处打一个断点
打印log日志:

添加之前“类(isa)“
myPerson
添加之前的name的set方法
0x10290c5f0
添加之后“类(isa)“
NSKVONotifying_myPerson
添加之后的name的set方法
0x102c6663a

我们可以得出:

输入:p (IMP)0x10ec655f0   
结果:(IMP) $0 = 0x000000010ec655f0 (KVO`-[myPerson setName:] at myPerson.h:14)
输入:p (IMP)0x10efbf63a 
结果:(IMP) $1 = 0x000000010efbf63a (Foundation`_NSSetObjectValueAndNotify)

我们猜测:
(1).在添加之后:底层会动态的创建一个myPerson的子类(继承了类myPerson), NSKVONotifying_myPerson
(2).然后会去重新实现setName的方法

2.我们在修改下viewcontroller文件

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"KVO本质";
    
    self.p  = [[myPerson alloc]init];
    self.p.name = @"jack";
    
    NSLog(@"添加之前“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
    NSLog(@"添加之前的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
    [self printMethodNamesOfClass:object_getClass(self.p)];
    
    //添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.p addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];

    NSLog(@"添加之后“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
    NSLog(@"添加之后的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
    [self printMethodNamesOfClass:object_getClass(self.p)];
    
    NSLog(@"End");
    
}

//利用runtime来打印类的属性和方法值
- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[I];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

发现:

 [self printMethodNamesOfClass:object_getClass(self.p)];第一次:
 myPerson .cxx_destruct, name, setName:,
 
 [self printMethodNamesOfClass:object_getClass(self.p)];第二次:
 NSKVONotifying_myPerson setName:, class, dealloc, _isKVOA,
 
在NSKVONotifying_myPerson里面多了setName、class、dealloc, _isKVOA的方法

我们来写一个:

//NSKVONotifying_myPerson_Test.h
#import "myPerson.h"
@interface NSKVONotifying_myPerson_Test : myPerson
@end

#import "NSKVONotifying_myPerson_Test.h"
@implementation NSKVONotifying_myPerson_Test
-(void)setName:(NSString *)name{
    _NSSetObjectValueAndNotify();
}

void _NSSetObjectValueAndNotify(){
    //里面做了一些列的处理
}

//内部实现,隐藏了NSKVONotifying_myPerson类的存在
-(Class)class{
   return [myPerson class];
}

-(void)dealloc{
    //释放相应的资源
}

- (BOOL)_isKVOA
{
    return YES;
}
@end
[self printMethodNamesOfClass:[NSKVONotifying_myPerson_Test class]];
打印:NSKVONotifying_myPerson_Test _isKVOA, dealloc, setName:, class,

分析:

  1. dealloc、_isKVOA方法:return YES;(显然可知)
  2. (Class)class方法:return [myPerson class]; (屏幕内部实现,隐藏了NSKVONotifying_myPerson类的存在)

我们在来看看:_NSSetObjectValueAndNotify的里面具体的逻辑:

点击stepOver(上绿色按钮)会发现:

image image image

那么我们可以猜测: _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];
 }

综上所述:我们可以得出下关系图:


image
image

说明:

#import "ViewController3.h"
#import "myPerson.h"
#import <objc/runtime.h>

struct my_class{
    Class isa;
};

@interface ViewController3 ()
@property (nonatomic,strong)myPerson  *p;
@end

@implementation ViewController3

- (void)viewDidLoad {
    [super viewDidLoad];
   
    //0x0000000ffffffff8ULL  0x00007ffffffffff8ULL
    
    self.p  = [[myPerson alloc]init];
    self.p.name = @"jack";
    
    NSLog(@"添加之前“类(isa)“\n%p",object_getClass(self.p)); //self.p->isa
    NSLog(@"添加之前“元类”\n%p",object_getClass(object_getClass(self.p)));
    Class class = object_getClass(self.p);
    Class mclass = object_getClass(object_getClass(self.p));
    struct my_class  *myC = (__bridge struct my_class *)class;
    struct my_class *mMetaC = (__bridge struct my_class *)mclass;
    
   
    //添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.p addObserver:self forKeyPath:@"name" options:options context:@"name-context"];
    
    NSLog(@"添加之后“类(isa)“\n%p",object_getClass(self.p)); //self.p->isa
    NSLog(@"添加之后“元类”\n%p",object_getClass(object_getClass(self.p)));
    
    Class class2 = object_getClass(self.p);
    Class mclass2 = object_getClass(object_getClass(self.p));
    struct my_class  *myC2 = (__bridge struct my_class *)class2;
    struct my_class *mMetaC2 = (__bridge struct my_class *)mclass2;
  
    NSLog(@"End");
    
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.p.name = @"Jack2";
    
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

- (void)dealloc {
    [self.p removeObserver:self forKeyPath:@"name"];
    NSLog(@"%s-%d",__FUNCTION__,__LINE__);
}

@end

在 NSLog(@"End");打个断点:


image

从里面的内存值:很容易看出,素虽然返回了return [myPerson class],但是它的isa这里不是指向myPerson的元类对象。而是自己的元类的对象哟


我们试试看手动写一个KVO

NSObject+KVO.h
#import <Foundation/Foundation.h>

@interface NSObject (KVO)
//给NSObject类添加一个监听的方法 
-(void)my_ddObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end

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


@implementation NSObject (KVO)
-(void)my_ddObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    //动态添加一个类
    NSString *originClassName = NSStringFromClass([self class]);
    
    //苹果创建的是 NSKVONotifying_  为前缀的
    NSString *newClassName = [@"myKVONotifying_" stringByAppendingString:originClassName];
    
    
    const char *newName = [newClassName UTF8String];
    
    // 继承自当前类,创建一个子类
    Class kvoClass = objc_allocateClassPair([self class], newName, 0);
    
    // 添加setter方法
    class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "v@:@@");
    
    //注册新添加的这个类
    objc_registerClassPair(kvoClass);
    
    // 修改isa,本质就是改变当前对象的类名
    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指针指向父类myPerson,调用父类setter方法
    object_setClass(self, class_getSuperclass([self class]));
    
#warning -- 这边方法会报错:build Settings 里面搜索enable strict 修改为NO即可
    // 调用父类setter方法,重新复制
    objc_msgSend(self, @selector(setName:), name);
    
    // 取出myKVO_Person观察者
    id objc = objc_getAssociatedObject(self, (__bridge const void *)@"observer");

    
    // 通知观察者,执行通知方法
    //这里是直接重新写的setName方法,所以很容易知道是 “name”
    objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"kind":@"kind_value",@"new":@"new_value",@"old":@"old_value"}, @"这里是监听的上下文环境");
    
    // 重新修改为myKVO_Person类
    object_setClass(self, kvoClass);
    
}
@end


实现:
#import "ViewController4.h"
#import "NSObject+KVO.h"
#import "myPerson.h"

@interface ViewController4 ()
@property (nonatomic,strong)myPerson  *p;
@end

@implementation ViewController4

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title  = @"手动写一个KVO";
    self.p = [myPerson new];
    self.p.name = @"jack";
    
    [self.p my_ddObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:@"123"];
}

- (void)dealloc {
    [self.p removeObserver:self forKeyPath:@"name"];
    NSLog(@"%s-%d",__FUNCTION__,__LINE__);
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.p.name =  @"Lucy";
}
@end

友情链接:

上一篇下一篇

猜你喜欢

热点阅读