美文网首页
IOS基础:通知、KVO与KVC、Block、Delegate(

IOS基础:通知、KVO与KVC、Block、Delegate(

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-25 16:35 被阅读0次

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

续文见上篇 IOS基础:通知、KVO与KVC、Block、Delegate(上)

目录

  • 比较
  • 使用
    • 1、跨层传值
    • 2、Block的用法
    • 3、Delegate与Protocol的用法
  • 一、NSNotification
    • 1、关键类结构
    • 2、注册通知源码解析
    • 3、发送通知源码解析
    • 4、删除通知源码解析
    • 5、异步通知
    • 6、UserNotifications的使用
  • 二、KVC
    • 1、常见的API
    • 2、setValue:forKey: 的原理
    • 3、valueForKey: 的原理
    • 4、KVC处理异常
    • 5、KVC处理数值和结构体类型属性
    • 6、KVC键值验证(Key-Value Validation)
    • 7、KVC处理集合
    • 8、KVC使用场景
  • 三、KVO
    • 1、KVO 使用
    • 2、KVO 实现机制
    • 3、KVO的问题
    • 4、封装KVO的API
    • 5、KVO 防护Crash
  • 四、Block
    • 1、Block类型
    • 2、Block的数据结构 源码解析
    • 3、Block捕获变量 源码解析
    • 4、Block的循环引用
  • 五、Delegate与Protocol
  • Demo
  • 参考文献

三、KVO

KVOObjective-C 对观察者模式(Observer Pattern)的实现。也是Code Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。 简单来说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。KVO的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueObserving类别名,所以对于所有继承了NSObject的类型,都能使用KVO(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject)。

KVO的优点
  • 运用了设计模式:观察者模式
  • 支持多个观察者观察同一属性,或者一个观察者监听不同属性
  • 开发人员不需要实现属性值变化了发送通知的方案,系统已经封装好了,大大减少开发工作量
  • 能够对非我们创建的对象,即系统对象的状态改变作出响应,而且不需要系统对象的实现
  • 能够提供观察的属性的最新值以及旧值
  • key paths来观察属性,因此也可以观察嵌套对象
KVO的缺点
  • 观察的属性的键值编码是通过字符串方式实现的,编译器不会出现警告以及检查
  • 由于允许对一个对象进行不同属性观察,所以在唯一回调方法中,会出现地狱式 if-else if - else 分支处理情况

1、KVO 使用

a、注册与解除注册

以下两个方法的定义在 Foundation/NSKeyValueObserving.h 中,NSObjectNSArrayNSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或集合对象。

/** 注册
 * observer:观察者,也就是KVO通知的订阅者
 * keyPath:描述将要观察的属性,相对于被观察者
 * options:KVO的一些属性配置 
 * context:上下文,这个会传递到订阅者的函数中,用来区分消息,所以应当是不同的
 */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

// 注意在不用的时候,不要忘记解除注册,否则会导致内存泄露
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

// options所包括的内容
NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
b、处理变更通知

订阅者必须实现 observeValueForKeyPath:ofObject:change:context:方法,每当监听的keyPath发生变化了,就会在这个函数中回调。

// change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
c、基本使用Demo
KVOUseViewController.h
@interface KVOUseViewController : UIViewController

@end

@interface ScrollViewRecorder : NSObject

@property (nonatomic, strong) UIScrollView *scrollView;

- (void)beginRecordScrollViewOffset;

@end
KVOUseViewController.m
@implementation ScrollViewRecorder

// 添加观察者
- (void)beginRecordScrollViewOffset
{
    [self.scrollView addObserver:self forKeyPath:@"contentOffset" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
}

// 移除观察者
- (void)dealloc
{
    [self.scrollView removeObserver:self forKeyPath:@"contentOffset"];
}

// 观察到的数据
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"contentOffset"])
    {
        NSNumber *changeKind = change[NSKeyValueChangeKindKey];
        NSValue *oldValue = change[NSKeyValueChangeOldKey];
        NSValue *newValue = change[NSKeyValueChangeNewKey];
        CGPoint oldOffset = oldValue.CGPointValue;
        CGPoint newOffset = newValue.CGPointValue;
        NSLog(@"ChangeKind:%@,旧的Y坐标值:%f,新的Y坐标值:%f", changeKind,oldOffset.y,newOffset.y);
    }
}

@end

@interface KVOUseViewController ()<UIScrollViewDelegate>

@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) ScrollViewRecorder *scrollViewRecorder;

@end

@implementation KVOUseViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createSubViews];
}

- (void)createSubViews
{
    self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 100, 400, 400)];
    self.scrollView.backgroundColor = [UIColor redColor];
    self.scrollView.delegate = self;
    self.scrollView.contentSize = CGSizeMake(400, 800);
    [self.view addSubview:self.scrollView];
    
    self.scrollViewRecorder = [[ScrollViewRecorder alloc] init];
    self.scrollViewRecorder.scrollView = self.scrollView;
    // 开始观察记录
    [self.scrollViewRecorder beginRecordScrollViewOffset];
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSLog(@"y轴坐标值:%f", scrollView.contentOffset.y);
}

@end

输出结果为:

2020-09-25 09:33:52.634983+0800 KVO_KVC_Demo[4139:18093326] y轴坐标值:5.333333
2020-09-25 09:33:52.635162+0800 KVO_KVC_Demo[4139:18093326] ChangeKind:1,旧的Y坐标值:0.000000,新的Y坐标值:5.333333
2020-09-25 09:33:52.653726+0800 KVO_KVC_Demo[4139:18093326] y轴坐标值:12.000000
2020-09-25 09:33:52.653865+0800 KVO_KVC_Demo[4139:18093326] ChangeKind:1,旧的Y坐标值:5.333333,新的Y坐标值:12.000000
d、手动KVO(禁用KVO)

KVO的实现,是对注册的keyPath中自动实现了两个函数,在setter中自动调用。

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

可能有时候,我们要实现手动的KVO,或者我们实现的类库不希望被KVO,这时候需要关闭自动生成KVO通知,然后手动的调用。手动通知的好处就是,可以灵活加上自己想要的判断条件。下面看个例子:

@interface Target : NSObject
{
    int age;
}

// 手动 KVO - 监听age
- (int)age;
- (void)setAge:(int)theAge;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }
    
    return self;
}

// 手动 KVO - 监听age
- (int)age
{
    return age;
}

// 需要手动实现属性的 setter 方法
- (void)setAge:(int)theAge
{
    // 在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法
    // 这两个方法用于通知系统该 key 的属性值即将和已经变更了
    // 如果需要禁用该类KVO的话,实现属性的 setter 方法,不进行调用willChangeValueForKey: 和 didChangeValueForKey方法
    NSLog(@"age改变前为:%d",age);
    [self willChangeValueForKey:@"age"]; //KVO 在调用存取方法之前调用 
    age = theAge;
    [self didChangeValueForKey:@"age"];
    NSLog(@"age改变后为:%d",age);//KVO 在调用存取方法之后调用
}

// 要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"age"]) {// 禁用该属性的KVO
        return NO;
    }

    // 对其它非手动实现的 key,要转交给 super 来处理
    // 如果需要禁用该类KVO的话直接返回NO
    return [super automaticallyNotifiesObserversForKey:key];
}

@end

观察者的代码实现:

@interface RootViewController ()

@property(nonatomic, strong) Target *target;
 
@end

@implementation RootViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    Target *target = [[Target alloc] init];
    [target addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)([Target class])];
    target.age = 20;
    self.target = target;
}

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

// 处理变更通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (__bridge Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

@end

输出结果为:

2020-08-17 10:50:39.632171+0800 Demo[23556:2053770] age改变前为:10
2020-08-17 10:50:39.632296+0800 Demo[23556:2053770]  class: Target, Age changed
2020-08-17 10:50:39.632359+0800 Demo[23556:2053770]  old age is 10
2020-08-17 10:50:39.632437+0800 Demo[23556:2053770]  new age is 20
2020-08-17 10:50:39.632504+0800 Demo[23556:2053770] age改变后为:20

当我们注释掉调用 willChangeValueForKey:didChangeValueForKey:方法的语句后,输出结果为:

2020-08-17 10:50:39.632171+0800 Demo[23556:2053770] age改变前为:10
2020-08-17 10:50:39.632504+0800 Demo[23556:2053770] age改变后为:20

就不能观察到了。

e、键值观察依赖键

有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。因此,object引入了依赖键。

观察依赖键:observeValueForKeyPath:ofObject:change:context:中添加处理变更通知的代码。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (__bridge Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else if ([keyPath isEqualToString:@"information"])
    {
        Class classInfo = (__bridge Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Information changed", className);
        NSLog(@" old information is %@", [change objectForKey:@"old"]);
        NSLog(@" new information is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

实现依赖键:观察的是 TargetWrapper 类的information属性,该属性是依赖于 Target 类的 agegrade 属性。为此,在 Target 中添加了 grade 属性:

@interface Target : NSObject

@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        _age = 10;
        _grade = 5;
    }
    
    return self;
}

@end

@implementation TargetWrapper
 
- (id)init:(Target *)aTarget
{
    self = [super init];
    if (nil != self) {
        _target = aTarget;
    }
    return self;
}
 
- (NSString *)information
{
    // information 属性依赖于 target 的 age 和 grade 属性
    return [[NSString alloc] initWithFormat:@"%d#%d", _target.grade, _target.age];
}

- (void)setInformation:(NSString *)theInformation
{
    // target 的 age/grade 属性任一发生变化,information 的观察者都会得到通知
    NSArray * array = [theInformation componentsSeparatedByString:@"#"];
    [_target setGrade:[[array objectAtIndex:0] intValue]];
    [_target setAge:[[array objectAtIndex:1] intValue]];
}

// 告诉系统 information 属性依赖于哪些其他属性
+ (NSSet *)keyPathsForValuesAffectingInformation
{
    NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
    
    // 返回一个key-path 的集合
    return keyPaths;
}

// 实现 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法 
/*
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    // 要先获取 super 返回的结果 set
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    NSArray * moreKeyPaths = nil;

    // 然后判断 key 是不是目标 key
    if ([key isEqualToString:@"information"])
    {
        moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
    }

    if (moreKeyPaths)
    {
        // 如果是就将依赖属性的 key-path 结合追加到 super 返回的结果 set 中
        // 否则直接返回 super的结果
        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
    }

    return keyPaths;
}
*/

@end

观察者的实现代码为:

@interface RootViewController ()

@property(nonatomic, strong) TargetWrapper *wrapper;
 
@end

@implementation RootViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    Target *target = [[Target alloc] init];
    TargetWrapper * wrapper = [[TargetWrapper alloc] init:target];
    [wrapper addObserver:self
    forKeyPath:@"information"
       options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                 context:(__bridge void * _Nullable)([TargetWrapper class])];

    [target setAge:100];
    [target setGrade:55];

    self.wrapper = wrapper;
}

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"information"])
    {
        Class classInfo = (__bridge Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" class: %@, Information changed", className);
        NSLog(@" old information is %@", [change objectForKey:@"old"]);
        NSLog(@" new information is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

@end

输出结果为:

2020-08-17 11:02:36.540061+0800 Demo[23652:2061994]  class: TargetWrapper, Information changed
2020-08-17 11:02:36.540154+0800 Demo[23652:2061994]  old information is 5#10
2020-08-17 11:02:36.540224+0800 Demo[23652:2061994]  new information is 5#100
2020-08-17 11:02:36.540307+0800 Demo[23652:2061994]  class: TargetWrapper, Information changed
2020-08-17 11:02:36.540366+0800 Demo[23652:2061994]  old information is 5#100
2020-08-17 11:02:36.540417+0800 Demo[23652:2061994]  new information is 55#100
f、KVO和线程

KVO 是同步的,发生在与所观察的变化值同样的线程上。所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把KVO和多线程混起来,即如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO

KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO会保证下列两种情况的发生:

  • 如果我们调用一个支持 KVOsetter 方法,如self.exchangeRate = 2.345;KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。
  • 其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用之前, exchangeRateaccessor 方法都会返回同样的值。

2、KVO 实现机制

KVO 实现机制

当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter方法。自然,重写的 setter方法会负责在调用原 setter方法之前和之后,通知所有观察对象值的更改。最后把这个对象的isa 指针 ( isa指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。原来,这个中间类,继承自原本的那个类。不仅如此,Apple 还重写了 -class 方法,企图欺骗我们这个类没有变,就是原本那个类。

简单来说,一个对象在被调用addObserver方法时,会动态创建一个KVO前缀的原类的子类,用来重写所有的setter方法,并且该子类的- (Class) class- (Class) superclass方法会被重写,返回父类(原始类)的Class。最后会将当前对象的类改为这个KVO前缀的子类。

NSLog(@"self->isa:%@",self->isa);  
NSLog(@"self class:%@",[self class]);  

// 建立KVO监听前,打印结果为:
self->isa:Person 
self class:Person 

// 建立KVO监听之后,打印结果为:
self->isa:NSKVONotifying_Person 
self class:Person 

kvo是监听是以类对象为基础。self监听了p1,不管p1中是否存在name属性,都会创建NSKVONotifying_Person类,并重写它所有属性的setter方法。

Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
id cls1 = object_getClass(p1);
id cls2 = object_getClass(p2);
NSLog(@"添加 KVO 之前: cls1 = %@  cls2 = %@ ",cls1,cls2);
   
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
cls1 = object_getClass(p1);
cls2 = object_getClass(p2);
NSLog(@"添加 KVO 之后: cls1 = %@  cls2 = %@ ",cls1,cls2);
p1.name = @"jack";

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"触发了kvo");
    }
}

输出结果为:

2020-07-21 09:44:26.414572+0800 Demo[70782:20849472] 添加 KVO 之前: cls1 = Person  cls2 = Person 
2020-07-21 09:44:26.414835+0800 Demo[70782:20849472] 添加 KVO 之后: cls1 = NSKVONotifying_Person  cls2 = Person 
2020-07-21 09:44:26.414930+0800 Demo[70782:20849472] 触发了kvo

3、KVO的问题

a、通过kvc设置value能否生效?

Person:

// 扩展里的私有属性
@interface Person : NSObject

@property(nonatomic, strong, readonly) NSString *name;

@end

@interface Person()

@property(nonatomic, strong, readwrite) NSString *name;

@end

@implementation Person

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.name = @"齐天大圣";
    }
    return self;
}

@end

RootViewController.h:

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

@interface RootViewController ()

@end

@implementation RootViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    id cls1 = object_getClass(p1);
    id cls2 = object_getClass(p2);
    NSLog(@"添加 KVO 之前: cls1 = %@  cls2 = %@ ",cls1,cls2);
    NSString *name = [p1 valueForKey:@"name"];
    NSLog(@"name:%@",name);
       
    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    cls1 = object_getClass(p1);
    cls2 = object_getClass(p2);
    NSLog(@"添加 KVO 之后: cls1 = %@  cls2 = %@ ",cls1,cls2);
    
    [p1 setValue:@"孙悟空" forKey:@"name"];
    name = [p1 valueForKey:@"name"];
    NSLog(@"name:%@",name);
}

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"触发了kvo");
    }
}

@end

输出结果为:

2020-07-21 09:56:35.877177+0800 Demo[70977:20865927] 添加 KVO 之前: cls1 = Person  cls2 = Person 
2020-07-21 09:56:35.877300+0800 Demo[70977:20865927] name:齐天大圣
2020-07-21 09:56:35.877510+0800 Demo[70977:20865927] 添加 KVO 之后: cls1 = NSKVONotifying_Person  cls2 = Person 
2020-07-21 09:56:35.877600+0800 Demo[70977:20865927] 触发了kvo
2020-07-21 09:56:35.877670+0800 Demo[70977:20865927] name:孙悟空

能, kvc会调用子类NSKVONotifying_ Person重写setter添加的方法。例如当我们调用personsetName方法时,实际是调用的一个子类实例的setName方法,但由于重写了class,在外部看不出来其中的差别。在setter方法中,我们在实际值被改变的前后回调用- (void)willChangeValueForKey:(NSString *)key;- (void)didChangeValueForKey:(NSString *)key;方法,通知观察者值的变化。

并且见到即使是私有属性,通过KVCKVO的方式也可以对其进行更改,XLForm框架便是基于此实现的。

b、通过成员变量直接赋值value能否生效?

Person:

@interface Person : NSObject

@property(nonatomic, strong) NSString *name;

- (void)changName;

@end

#import "Person.h"

@implementation Person

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.name = @"齐天大圣";
    }
    return self;
}

- (void)changName
{
    _name = @"筋斗云";
}

@end

RootViewController:

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

@interface RootViewController ()

@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;

@end

@implementation RootViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.person1 = [[Person alloc] init];
    self.person2 = [[Person alloc] init];
    id cls1 = object_getClass(self.person1);
    id cls2 = object_getClass(self.person2);
    NSLog(@"添加 KVO 之前: cls1 = %@  cls2 = %@ ",cls1,cls2);
    NSString *name = [self.person1 valueForKey:@"name"];
    NSLog(@"name:%@",name);
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"name" options:options context:NULL];
    cls1 = object_getClass(self.person1);
    cls2 = object_getClass(self.person2);
    NSLog(@"添加 KVO 之后: cls1 = %@  cls2 = %@ ",cls1,cls2);
    
    [self.person1 changName];
    name = [self.person1 valueForKey:@"name"];
    NSLog(@"name:%@",name);
}

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"监听到了%@的%@属性发生了改变%@",object,keyPath,change);
    }
}

/// 使用结束后记得移除
- (void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"name"];
}

@end

输出结果为:

2020-07-21 10:19:44.549165+0800 Demo[71136:20884349] 添加 KVO 之前: cls1 = Person  cls2 = Person 
2020-07-21 10:19:44.549285+0800 Demo[71136:20884349] name:齐天大圣
2020-07-21 10:19:44.549485+0800 Demo[71136:20884349] 添加 KVO 之后: cls1 = NSKVONotifying_Person  cls2 = Person 
2020-07-21 10:19:44.549553+0800 Demo[71136:20884349] name:筋斗云

不能,通过成员变量_name无法触发系统重写的kvo,虽然改变了值,但是却无法被监听到,需要手动添加对应方法:

- (void)changName
{
    [self willChangeValueForKey:@"name"];
    _name = @"筋斗云";
    [self didChangeValueForKey:@"name"];
}

输出结果为:

2020-07-21 10:22:11.703925+0800 Demo[71154:20886564] 添加 KVO 之前: cls1 = Person  cls2 = Person 
2020-07-21 10:22:11.704057+0800 Demo[71154:20886564] name:齐天大圣
2020-07-21 10:22:11.704333+0800 Demo[71154:20886564] 添加 KVO 之后: cls1 = NSKVONotifying_Person  cls2 = Person 
2020-07-21 10:22:11.704512+0800 Demo[71154:20886564] 监听到了<Person: 0x600002c0c240>的name属性发生了改变{
    kind = 1;
    new = "\U7b4b\U6597\U4e91";
    old = "\U9f50\U5929\U5927\U5723";
}
2020-07-21 10:22:11.704602+0800 Demo[71154:20886564] name:筋斗云

这样就可以了。

c、子类继承父类的一个属性,当这个属性被改变时,KVO能否观察到?

Person和Student:

@interface Person : NSObject

@property (nonatomic, strong) NSString *lastName;

@end

@interface Student : Person

@end

RootViewController:

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"lastName"]) {
        NSDictionary *dict = change;
        NSString *old = dict[@"old"];
        NSString *new = dict[@"new"];
        NSLog(@"监听到了%@的%@属性发生了改变%@,旧值:%@,新值:%@",object,keyPath,change,old,new);
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    Person *p = [[Person alloc] init];
    Student *stu = [[Student alloc] init];
    
    p.lastName = @"佳培";
    stu.lastName = @"平凡";
    [p addObserver:self forKeyPath:@"lastName" options:options context:NULL];
    p.lastName = @"NPC";
    stu.lastName = @"世界";
}

输出为:

2020-07-21 11:09:39.822197+0800 Demo[71523:20926615] 监听到了<Person: 0x600001ebcc30>的lastName属性发生了改变{
    kind = 1;
    new = NPC;
    old = "\U4f73\U57f9";
},旧值:佳培,新值:NPC

可见继承属性在子类中并没有监听到自身变化。改为子类监听:

    p.lastName = @"佳培";
    stu.lastName = @"平凡";
    [stu addObserver:self forKeyPath:@"lastName" options:options context:NULL];
    p.lastName = @"NPC";
    stu.lastName = @"世界";

输出结果为:

2020-07-21 11:11:56.658199+0800 Demo[71539:20928780] 监听到了<Student: 0x6000024f6ee0>的lastName属性发生了改变{
    kind = 1;
    new = "\U4e16\U754c";
    old = "\U5e73\U51e1";
},旧值:平凡,新值:世界

可见父类的属性也无法响应子类对其父类属性的监听,看来二者是隔绝了的。再试一下将二者均加上:

    p.lastName = @"佳培";
    stu.lastName = @"平凡";
    [p addObserver:self forKeyPath:@"lastName" options:options context:NULL];
    [stu addObserver:self forKeyPath:@"lastName" options:options context:NULL];
    p.lastName = @"NPC";
    stu.lastName = @"世界";

输出结果为:

2020-07-21 11:13:50.437916+0800 Demo[71554:20930618] 监听到了<Person: 0x6000026302d0>的lastName属性发生了改变{
    kind = 1;
    new = NPC;
    old = "\U4f73\U57f9";
},旧值:佳培,新值:NPC
2020-07-21 11:13:50.438083+0800 Demo[71554:20930618] 监听到了<Student: 0x600002630120>的lastName属性发生了改变{
    kind = 1;
    new = "\U4e16\U754c";
    old = "\U5e73\U51e1";
},旧值:平凡,新值:世界

看来结论是子类想要监听父类某个属性的话,还是需要以自己为对象,重写一遍监听方法对,二者是隔绝了的。

d、子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能否观察到?

Student:

- (void)setLastName:(NSString *)lastName
{
    NSLog(@"重写的setLastName方法");
}

RootViewController:

    p.lastName = @"佳培";
    stu.lastName = @"平凡";
    [p addObserver:self forKeyPath:@"lastName" options:options context:NULL];
    [stu addObserver:self forKeyPath:@"lastName" options:options context:NULL];
    p.lastName = @"NPC";
    stu.lastName = @"世界";

输出结果为:

2020-07-21 11:20:41.085424+0800 Demo[71620:20938718] 重写的setFirstName方法
2020-07-21 11:20:41.085783+0800 Demo[71620:20938718] 监听到了<Person: 0x6000008eef10>的lastName属性发生了改变{
    kind = 1;
    new = NPC;
    old = "\U4f73\U57f9";
},旧值:佳培,新值:NPC
2020-07-21 11:20:41.085878+0800 Demo[71620:20938718] 重写的setFirstName方法
2020-07-21 11:20:41.085991+0800 Demo[71620:20938718] 监听到了<Student: 0x6000008eefa0>的lastName属性发生了改变{
    kind = 1;
    new = "<null>";
    old = "<null>";
},旧值:<null>,新值:<null>

首先根据上个问题的结论,如果子类不重写监听方法的话肯定是监听不到的,其次,因为重写了Setter方法,而且没添加任何实现,结果输出旧值和新值全为空。

e、KVO可以实现跨线程的监听吗?

我们知道使用Notification时,跨线程发送通知是无法被接受到的,那么现在看看KVO在多线程中的表现。

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"lastName"]) {
        NSDictionary *dict = change;
        NSString *old = dict[@"old"];
        NSString *new = dict[@"new"];
        NSLog(@"监听到了%@的%@属性发生了改变%@,旧值:%@,新值:%@",object,keyPath,change,old,new);
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
    // 在两个线程分别设置目标和观察者
    dispatch_queue_t   concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    
    __block Student *stu1 = nil;
    dispatch_async(concurrentQueue, ^{
        stu1 = [[Student alloc] init];
        NSLog(@"Student: %@",[NSDate new]);
        stu1.lastName = @"佳培";
    });
    
    dispatch_async(concurrentQueue, ^{
        sleep(2);
        
        [stu1 addObserver:self forKeyPath:@"lastName" options:options context:NULL];
        NSLog(@" StudentKvoObserver: %@",[NSDate new]);
    });
    
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"dispatch_barrier_async: %@",[NSDate new]);
        NSLog(@"开始:%@",[NSDate new]);
        stu1.lastName = @"NPC";
        NSLog(@"结束:%@",[NSDate new]);
    });
}

输出结果为:

2020-07-21 11:35:43.098330+0800 Demo[71682:20948565] Student: Tue Jul 21 11:35:43 2020
2020-07-21 11:35:45.104017+0800 Demo[71682:20948563]  StudentKvoObserver: 2020-07-21 03:35:45 +0000
2020-07-21 11:35:45.104243+0800 Demo[71682:20948563] dispatch_barrier_async: 2020-07-21 03:35:45 +0000
2020-07-21 11:35:45.104377+0800 Demo[71682:20948563] 开始:2020-07-21 03:35:45 +0000
2020-07-21 11:35:45.104615+0800 Demo[71682:20948563] 监听到了<Student: 0x6000013983c0>的lastName属性发生了改变{
    kind = 1;
    new = NPC;
    old = "\U4f73\U57f9";
},旧值:佳培,新值:NPC
2020-07-21 11:35:45.104742+0800 Demo[71682:20948563] 结束:2020-07-21 03:35:45 +0000

可以看到在两个不同的线程里创建的ObserverTarget,观察变化也是能够生效的。
这里有一个关于GCD的问题,这里我使用了dispatch_barrier_async,分发到自定义的并发队列上,这时barrier是正常工作的,保证了第三个task在前两个执行完之后执行。但是当我直接使用系统全局的并发队列时,barrier不起作用,不能保证他们的执行顺序。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

输出结果为:

2020-07-21 11:40:30.791567+0800 Demo[71699:20952036] Student: Tue Jul 21 11:40:30 2020
2020-07-21 11:40:30.792517+0800 Demo[71699:20952034] dispatch_barrier_async: 2020-07-21 03:40:30 +0000
2020-07-21 11:40:30.792614+0800 Demo[71699:20952034] 开始:2020-07-21 03:40:30 +0000
2020-07-21 11:40:30.792691+0800 Demo[71699:20952034] 结束:2020-07-21 03:40:30 +0000
2020-07-21 11:40:32.792599+0800 Demo[71699:20952033]  StudentKvoObserver: 2020-07-21 03:40:32 +0000
f、KVO键之间的依赖

fullName属性依赖于 firstNamelastName,当firstName或者lastName改变时,这个fullName属性需要被通知到。

#import "RootViewController.h"
#import <objc/runtime.h>

@interface RootViewController ()
 
@property (nonatomic,copy)NSString *fullName;
@property (nonatomic,copy)NSString *firstName;
@property (nonatomic,copy)NSString *lastName;

@end

@implementation RootViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.firstName = @"诸葛";
    self.lastName = @"四郎";
    
    [self addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

    self.lastName = @"流云";
}

- (NSString *)fullName
{
    return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}

// 键之间的依赖关系
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ([key isEqualToString:@"fullName"])
    {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"fullName"]) {
        NSDictionary *dict = change;
        NSString *old = dict[@"old"];
        NSString *new = dict[@"new"];
        NSLog(@"监听到了%@的%@属性发生了改变%@,旧值:%@,新值:%@",object,keyPath,change,old,new);
    }
}

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

@end

输出结果为:

2020-07-21 15:05:49.063183+0800 Demo表[72355:21059324] 监听到了<RootViewController: 0x7ff980205d80>的fullName属性发生了改变{
    kind = 1;
    new = "\U8bf8\U845b \U6d41\U4e91";
    old = "\U8bf8\U845b \U56db\U90ce";
},旧值:诸葛 四郎,新值:诸葛 流云

可以见到顺利实现了效果。但是如果直接对fullName进行赋值呢?

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.firstName = @"诸葛";
    self.lastName = @"四郎";
    
    [self addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

    self.fullName = @"流云";
}

输出结果为:

2020-07-21 15:08:32.372174+0800 Demo[72371:21061947] 监听到了<RootViewController: 0x7fa370c08310>的fullName属性发生了改变{
    kind = 1;
    new = "\U8bf8\U845b \U56db\U90ce";
    old = "\U8bf8\U845b \U56db\U90ce";
},旧值:诸葛 四郎,新值:诸葛 四郎

虽然KeyPathfullName, 但是你对fullName进行赋值,也不会调用监控的方法,因为已经监控对象指向了依赖对象。

keyPathsForValuesAffectingValueForKey添加依赖的方法,此方法同样遵循KVC的命名查找规则。你可以通过实现 keyPathsForValuesAffecting<Key> 方法来达到前面同样的效果,这里的<Key>就是属性名,不过第一个字母要大写,用前面的例子来说就是这样:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName
{
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

输出结果为:

2020-07-21 15:12:24.865388+0800 合并两个有序链表[72410:21065976] x = 4, y = 18
2020-07-21 15:12:24.940469+0800 合并两个有序链表[72410:21065976] 监听到了<RootViewController: 0x7f8f80c06190>的fullName属性发生了改变{
    kind = 1;
    new = "\U8bf8\U845b \U6d41\U4e91";
    old = "\U8bf8\U845b \U56db\U90ce";
},旧值:诸葛 四郎,新值:诸葛 流云
g、如何手动关闭kvo
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.lastName = @"四郎";
    
    [self addObserver:self forKeyPath:@"lastName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

    self.lastName = @"流云";
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"lastName"])
    {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

///监听name的变化回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"lastName"]) {
        NSDictionary *dict = change;
        NSString *old = dict[@"old"];
        NSString *new = dict[@"new"];
        NSLog(@"监听到了%@的%@属性发生了改变%@,旧值:%@,新值:%@",object,keyPath,change,old,new);
    }
}

无任何输出!即关闭了监听。

4、KVO 防护Crash

Crash原因
  • KVO 添加次数和移除次数不匹配:
  • 移除了未注册的观察者,导致崩溃。
  • 重复移除多次,移除次数多于添加次数,导致崩溃。
  • 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
  • 被观察者提前被释放,被观察者在 dealloc 时仍然注册着KVO,导致崩溃。
  • 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。
  • 添加或者移除时 keypath == nil,导致崩溃。
防护方案

a、第三方库
为了避免上面提到的使用 KVO造成崩溃的问题,于是出现了很多关于KVO的第三方库,比如最出名的就是 FaceBook 开源的第三方库 facebook / KVOController

FBKVOControllerKVO机制进行了额外的一层封装,框架不但可以自动帮我们移除观察者,还提供了block或者selector的方式供我们进行观察处理。不可否认的是,FBKVOController为我们的开发提供了很大的便利性。但是相对而言,这种方式对项目代码的侵入性比较大,必须考编码规范来强制约束团队人员使用这种方式。

那么有没有一种对项目代码侵入性小,同时还能有效防护KVO 崩溃的防护机制呢?

b、自己实现防护

思路:

  • 添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。
  • 移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。
  • 观察键值改变时:同样通过关系哈希表判断,将改变操作分发到原有的观察者上。
  • 另外,为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着KVO 导致崩溃。还利用Method Swizzling实现了自定义的 dealloc,在系统 dealloc调用之前,将多余的观察者移除掉。

具体实现的Demobujige / YSC-Avoid-Crash

5、封装KVO的API

官方实现的KVO提供的 API实在不怎么样。比如,你只能通过重写 -observeValueForKeyPath:ofObject:change:context:方法来获得通知。想要提供自定义的selector ,不行;想要传一个 block,门都没有。而且你还要处理父类的情况 - 父类同样监听同一个对象的同一个属性。但有时候,你不知道父类是不是对这个消息有兴趣。虽然 context这个参数就是干这个的,也可以解决这个问题, 在-addObserver:forKeyPath:options:context:传进去一个父类不知道的context。但总觉得框在这个 API 的设计下,代码写的很别扭。至少至少,也应该支持block吧。

这个知识点比较麻烦,可以略过不看,这样做的目的是因为官方实现的KVO提供的 API实在不怎么样,所以重新封装一套。

a、首先,我们创建NSObjectCategory,并在头文件中添加两个 API

typedef void(^PGObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block;

- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end

b、接下来,实现 PG_addObserver:forKey:withBlock:方法。

  • 检查对象的类有没有相应的 setter 方法。如果没有抛出异常;
  • 检查对象isa 指向的类是不是一个KVO 类。如果不是,新建一个继承原来类的子类,并把isa 指向这个新建的子类;
  • 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
// 添加这个观察者
- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block
{
    // Step 1: Throw exception if its class or superclasses doesn't implement the setter
    // 第一步里,先通过 setterForGetter() 方法获得相应的 setter 的名字(SEL)。
    // 也就是把 key 的首字母大写,然后前面加上 set 后面加上 :,这样 key 就变成了 setKey:。
    // 然后再用 class_getInstanceMethod 去获得 setKey: 的实现(Method)。如果没有,自然要抛出异常。
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        // throw invalid argument exception
    }
    
    // Step 2: Make KVO class if this is first time adding observer and  its class is not an KVO class yet
    // 第二步,我们先看类名有没有我们定义的前缀。
    // 如果没有,我们就去创建新的子类,并通过 object_setClass() 修改 isa 指针。
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }
    
    // Step 3: Add our kvo setter method if its class (not superclasses) hasn't implemented the setter
    // 第三步,重写 setter 方法。新的 setter 在调用原 setter 方法后,通知每个观察者(调用之前传入的 block ):
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
    
    // Step 4: Add this observation info to saved observation objects
    // 最后一步,把这个观察的相关信息存在 associatedObject 里。观察的相关信息(观察者,被观察的 key, 和传入的 block )封装在 PGObservationInfo 类里。
    PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

c、动态创建新的类需要用 objc/runtime.h中定义的objc_allocateClassPair()函数。传一个父类(原始类)类名,然后额外的空间(通常为 0),它返回给你一个类。然后就给这个类添加方法,也可以添加变量。这里,我们只重写了class方法。哈哈,跟 Apple 一样,这时候我们也企图隐藏这个子类的存在。最后 objc_registerClassPair()告诉Runtime这个类的存在。

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
    NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    // grab class method's signature so we can borrow it
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}

d、细心的同学会发现下面代码中我们有对 objc_msgSendSuper进行了类型转换。

static void kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        // throw invalid argument exception
    }
    
    id oldValue = [self valueForKey:getterName];
    
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    for (PGObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

最后:

@interface PGObservationInfo : NSObject

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) PGObservingBlock block;

@end

四、Block

基本介绍

定义:Block是将函数及其执行上下文封装起来的对象。
查看源码:使用clang main.m -rewrite-objc -o dest.cpp查看编译之 后的文件内容。
截获变量:

  • 对于基本数据类型的局部变量截获其值
  • 对于对象类型的局部变量连同所有权修饰符一起截获
  • 不截获全局变量、静态全局变量。
  • 以指针形式截获局部静态变量。

1、Block类型

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
         int num = 1;// 局部变量
         static int staticNum = 2;// 静态局部变量
         char str[] = "hello";// 字面值
         
         // NSMallocBlock 堆
         void(^aBlock)(void) = ^(void) {
             printf("num:%d\n", num);
         };
        
         //  NSGlobalBlock 全局
         void(^bBlock)(void) = ^(void) {
            printf("staticNum:%d\n", staticNum);
         };

         // NSGlobalBlock 全局
         void(^cBlock)(char *) = ^(char *word) {
             printf("word:%s\n", word);
         };
         
         aBlock();
         bBlock();
         cBlock(str);
         NSLog(@"a:%@----b:%@----c:%@----d:%@", aBlock, bBlock, cBlock, ^{NSLog(@"%d", num);});// NSStackBlock
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

<__NSMallocBlock__: 0x100607950>----<__NSGlobalBlock__: 0x100002118>--
--<__NSGlobalBlock__: 0x100002158>----<__NSStackBlock__: 0x7fff5fbff5c0>

打印结果如下:

num:1
staticNum:2
word:hello
2020-07-21 13:19:18.774637+0800 Demo[71887:20987955] a:<__NSMallocBlock__: 0x60000010c360>----b:<__NSGlobalBlock__: 0x103b9c068>----c:<__NSGlobalBlock__: 0x103b9c0a8>----d:<__NSStackBlock__: 0x7ffeec069c38>

因为bBlockcBlock只使用了静态变量和入参,不需要捕获外部变量,所以为全局Block __NSGlobalBlock__,存在于全局区,内存在程序结束后由系统回收。

最后一个使用了外部变量num,为栈Block__NSStackBlock__,内存由编译器控制,过了作用域就会被回收。

aBlock虽然也只使用了外部变量,但由于在ARC下会自动调用一次copy方法,将Block从栈区copy到堆区,所以aBlock为堆Block__NSMallocBlock__,内存由ARC控制,没有强指针指向时释放。而在MRC中,赋值不会执行copy操作,所以aBlock依然会存在于栈中,所以在MRC中一般都需要执行copy,否则很容易造成crash

ARC中,当Block作为属性被strongcopy修饰或被強指针应用或作为函数返回值或被赋值给Block类型成员变量时,都会默认执行copy方法。而MRC中,只有被copy修饰时,Block才会执行copy。所以MRCBlock都需要用copy来修饰,而在ARC中用copy修饰只是沿用了MRC的习惯,此时用copystrong效果是相同的。

  • NSGlobalBlock使用copy还是NSGlobalBlock什么都不做
  • NSStackBlock使用copy,变为NSMallocBlock
  • NSMallocBlock使用copy还是NSMallocBlock,只是引用计数+1
Block的copy操作

NSGlobalBlock

block内部没有调用auto修饰符(auto关键字是系统自动添加)变量的block都是NSGlobalBlock类型。
这里不使用clang转换代码,因为block都是运行时确定的,所以通过打断点确定block类型。

int main(int argc, char * argv[]) {
    
    void(^helloBlock)(void) = ^{
        NSLog(@"Hello World");
    };

    helloBlock();

    return 0;
}

lldb指令打印

(lldb) po [helloBlock class]
__NSGlobalBlock__

又或者使用static修饰符

int main(int argc, char * argv[]) {
    static int age = 10;
    void(^helloBlock)(void) = ^{
        NSLog(@"小明今天%d岁", age);
    };

    helloBlock();

    return 0;
}

lldb指令打印

(lldb) po [helloBlock class]
__NSGlobalBlock__

NSStackBlock

XcodeARC改为MRC

将Xcode里ARC改为MRC

block内部使用了auto修饰符的都是NSStackBlock类型,栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放。

int main(int argc, char * argv[]) {
    int age = 10;
    void(^helloBlock)(void) = ^{
        NSLog(@"小明今天%d岁", age);
    };
    helloBlock();
}

lldb指令打印

(lldb) po [helloBlock class]
__NSStackBlock__

NSMallocBlock

NSStackBlock调用copy得到的就是NSMallocBlock类型,存放在堆中需要我们自己进行内存管理。

int main(int argc, char * argv[]) {
    int age = 10;
    void(^helloBlock)(void) = [^{
        NSLog(@"小明今天%d岁", age);
    } copy];
    helloBlock();
    return 0;
}

lldb指令打印

(lldb) po [helloBlock class]
__NSMallocBlock__

2、Block的数据结构 源码解析

转化为C++:比如我需要转换main.m中的代码就可以写clang -rewrite-objc main.m

///__block_impl是编译器给我们生成的结构体,每一个block都会用到这个结构体
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

结构体内发现了isa,我们知道只要OC对象都会有一个isa指针,所以我们可以知道block本质上也是一个OC对象。 block是封装了函数调用以及函数调用环境的OC对象,本质上继承自NSObject

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;///__block_impl 变量impl
  struct __main_block_desc_0* Desc;
    // 构造函数,初始化这个结构体
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;///说明block是栈
    impl.Flags = flags;
    impl.FuncPtr = fp; // fp就是前面传进来的__main_block_func_0
    Desc = desc; // desc就是前面传进来的__main_block_desc_0_DATA
  }
};

// block里面的函数,被解析成一个独立的函数,会被赋值给impl.FuncPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_a5d38c_mi_0);
    }

// block的描述信息,主要是记录下__main_block_impl_0结构体大小
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// main函数
int main(int argc, char * argv[]) {

    // 结构体初始化并赋值
    ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    return 0;
}

block函数被转换成了单独了一个函数__main_block_func_0并赋值给block结构体内FuncPtr成员,这样做我们想必也知道以后要是调用这个函数,直接调用结构体的FuncPtr成员即可。

int main(int argc, char * argv[]) {

    void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    // 使用结构体的FuncPtr成员来调用函数
    ((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

    return 0;
}
补充:函数指针回调和block

联系:函数指针和Block都可以实现回调的操作,声明上也很相似,实现上都可以看成是一个代码片段。
区别:

  • 函数里面只能访问全局变量,而Block代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性
    (当然通过__block访问指示符修饰的局部变量还可以在block代码块里面进行修改)。
  • 从内存的角度看,函数指针只不过是指向代码区的一段可执行代码,而block实际上是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。
  • 函数指针只能指向预先定义好的函数代码块,函数地址是在编译链接时就已经确定好的。Block本质是Objective-C对象,是NSObject的子类,可以接收消息。

3、Block捕获变量 源码解析

局部基本数据类型变量

OC

int main(int argc, char * argv[]) {
    
    int age = 10;
    void(^helloBlock)(void) = ^{
        NSLog(@"小明今天%d岁", age);
    };
    
    helloBlock();

    return 0;
}

C++

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age; // 结构体多了一个age成员
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age)/* 成员变量的age在这里被赋值,初始化的时候外部会传值 */ {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // 使用的是__main_block_impl_0成员age

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_942932_mi_0, age);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};


int main(int argc, char * argv[]) {

    int age = 10;
    // 初始化,并把10传进结构体
    void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

    ((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

    return 0;

}

在转换代码里,结构体多了个名为age的成员变量,并且main函数里对block结构体初始化的时候就把10这个值传递进去了,取来用的时候直接调取结构体age这个成员变量。初始化完成后,结构体内的age成员和main函数里定义的age变量其实已经没啥关系了,所以这也是为什么,之后修改变量age这个值,block内读取age的值是不会变的。

对象型的局部变量

OC

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        void (^block)(void) = ^{
            NSLog(@"age--- %ld",person.age);
         };
        block();

    }
    return 0;
}

C++

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
};

可以看到和基本数据类型不同的是,person对象被block捕获后,在结构体中多了一个修饰关键字__strong
即局部基本数据类型变量,block简单的捕获其值,但是局部对象类型变量,block会连带其修饰符一起捕获。

静态局部变量

OC

int main(int argc, char * argv[]) {
    
    static int age = 10;
    void(^helloBlock)(void) = ^{
        NSLog(@"小明今天%d岁", age);
    };
    
    helloBlock();

    return 0;
}

C++

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *age; // 同样是新增了age成员,不同的是这次是个指针
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *age = __cself->age; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_5c5d34_mi_0, (*age));
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};


int main(int argc, char * argv[]) {

    static int age = 10;
    // 初始化的时候,也是直接将地址传递进去
    void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));

    ((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

    return 0;

}

同样新增了一个age的成员变量,但是跟之前不同的是这次是一个指针成员,初始化的时候直接将变量的地址传递了进去,我们知道如果使用指针传递,我们可以随时随地的修改这个变量内的值,并且读取的时候也是被改变后的值,因为block内部直接访问的是变量的地址。所以,这里就跟前面不一样了,后面修改了值,调用block后,block内部读取变量的值是修改后的值。

全局变量

OC

int age = 10;

int main(int argc, char * argv[]) {
    
    void(^helloBlock)(void) = ^{
        NSLog(@"小明今天%d岁", age);
    };
    
    helloBlock();

    return 0;
}

C++

int age = 10; // 变量同时也放在了全局

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_main_4e4a2d_mi_0, age);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {

    void(*helloBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);

    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

转换后的代码也是全局变量,那么这个变量也是想怎么改就怎么改,block内部读取的值也是最新被赋值的值。

property属性

OC

@interface ViewController ()

@property (nonatomic, assign) int age;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    void(^helloBlock)(void) = ^{
        NSLog(@"小明今天%d岁", self.age);
    };
    
    helloBlock();
}
@end

C++

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  ViewController *self; // 多了一个控制器自己的成员
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
  ViewController *self = __cself->self; // bound by copy
        // 通过runtime读取成员变量的值,所以也可以保证被修改的值,后面block内部读取的时候也是读取最新的
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_ViewController_bc83d3_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
    }
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
  void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0), __ViewController__viewDidLoad_block_copy_0, __ViewController__viewDidLoad_block_dispose_0};

// OC方法默认会传两个参数:self和SEL
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

    // 初始化Block的时候传进去了self
    void(*helloBlock)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);
}

使用属性的时候,block结构体多了一个self的成员变量,初始化的时候将控制器实例传递了进去。读取值的时候通过消息发送机制获取最新的成员变量的值。

__block修饰符

通过static修饰的局部变量和全局变量被block捕获后,外部修改变量的值后,block内部读取的时候也是最新的值,但是实际需求中,我们只是临时使用下这个变量,函数执行完毕后,这个变量销毁就可以了,不希望变量被放到全局区,所以这时候需要通过__block来修饰。
OC

int main(int argc, char * argv[]) {
    __block int age = 10;
    void(^helloBlock)(void) = nil;
    {
        helloBlock = ^{
            NSLog(@"小明今天%d岁", age);
        };
    }
    age = 20;
    helloBlock();
    return 0;
}

C++

// 变量被转换成这种结构体了
 struct __Block_byref_age_0 {
  void *__isa; // 有isa,可以理解为被转换后,在内部,该变量变成了一个OC对象
__Block_byref_age_0 *__forwarding; // 指向该实例自身的指针
 int __flags;
 int __size;
 int age; // 原来的变量
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // block修饰的变量,变成了一个结构体
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int main(int argc, char * argv[]) {
        // 初始化__Block_byref_age_0,并把age地址传给forwarding,说明forwarding是指向自己的
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        
        // 初始化block函数
        void(*helloBlock)(void) = __null;
        {
            helloBlock = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        }
        
        // 重新赋值,已经变成给结构体赋值了
        (age.__forwarding->age) = 20;
        // 调用block函数
        ((void (*)(__block_impl *))((__block_impl *)helloBlock)->FuncPtr)((__block_impl *)helloBlock);
        return 0;
}

__block修饰的变量会被改成一个结构体,而且该结构含有isa成员,那么这个变量很明显被转换成一个OC对象了,原本的变量也被包含在这个对象内。

__block修饰的变量的修改,也就是对这个对象的age成员的修改((age.__forwarding->age) = 20;)。同时这个对象还有一个forwarding指针,这个的作用就是指向自己,如果在栈区就指向在栈区的实例地址,如果在堆区就指向在堆区的实例地址

我们__block修饰的变量不仅被转换成一个对象结构体,并且第一次使用的时候还会被拷贝到堆上。这里还需要注意一个地方,就是如果是同一个变量被多个block捕获,那么这个变量在堆上只存在一份地址,block对其只是引用计数加1,而不是说多个block捕获这个变量,这个变量就会被多次拷贝到堆上。

最后,因为这个变量被拷贝到堆区了,所以需要将变量转换后的结构体里的forwarding指向这个堆区地址,由于只是拷贝操作,所以栈上存在一份,堆上也存在一份。因此,如果__block修饰的对象或变量在栈区,则forwarding指向栈区地址,如果被复制到的堆区,则栈上的forwarding指向堆区地址,被拷贝到堆上的对象的forwarding则指向自己在堆上的地址。

int main(int argc, char * argv[]) {
    __block Animal *a = [Animal new];
    a.age = 10;
    void(^helloBlock)(void) = nil;
    {
        helloBlock = ^{
            NSLog(@"小明今天%d岁", a.age);
        };
    }

    a.age = 20;

    helloBlock();

    return 0;
}

如果__block修饰的是对象的话,也被转换成一个结构体,这个结构体不同之处就是多了两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose,用来管理结构体的成员Animal a的内存,之前因为是变量所以不需要管理,只需要管理其所在的结构体内存就可以了。

实战演示:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        NSMutableArray *mArray = @[@1,@2,@3].mutableCopy;
        __block NSMutableArray *bmArray = @[@1,@2,@3].mutableCopy;
        __block NSArray *array = @[@1,@2,@3];
         // NSMallocBlock 堆
         void(^aBlock)(void) = ^(void) {
             [mArray addObject:@120];
             NSLog(@"NSMutableArray:%@", mArray);
             
             bmArray = @[@4].mutableCopy;
             array = @[@4];
             NSLog(@"__block NSMutableArray:%@", bmArray);
             NSLog(@"__block NSArray:%@", array);
         };
         
         aBlock();
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

输出结果为:

2020-07-21 13:51:31.326204+0800 Demo[72077:21012461] NSMutableArray:(1,2,3,120)
2020-07-21 13:51:31.326698+0800 Demo[72077:21012461] __block NSMutableArray:(4)
2020-07-21 13:51:31.326782+0800 Demo[72077:21012461] __block NSArray:(4)

可见,针对block在修改NSMutableArray,需不需要添加__block?答案是不需要的,修改内容也是对数组的使用。只有对对象赋值的时候才需要__block

实战演示:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        __block int x = 6;
        // NSMallocBlock 堆
        int(^aBlock)(int) = ^(int num) {
            // x = 9;
            return x * num;
        };
        // x = 4;
        int y = aBlock(2);
        NSLog(@"x = %d, y = %d", x, y);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

输出结果为:

2020-07-21 13:58:45.696074+0800 Demo[72137:21019394] x = 6, y = 12

打开x = 4的注释,输出结果为:

2020-07-21 14:00:22.619347+0800 Demo[72152:21021342] x = 4, y = 8

再打开x = 9的注释,输出结果为:

2020-07-21 14:01:10.275583+0800 合并两个有序链表[72157:21022626] x = 9, y = 18

最后,将int y = aBlock(2);x = 4;互换位置:

        __block int x = 6;
        // NSMallocBlock 堆
        int(^aBlock)(int) = ^(int num) {
            x = 9;
            return x * num;
        };
        int y = aBlock(2);
        x = 4;  
        NSLog(@"x = %d, y = %d", x, y);

输出结果为:

2020-07-21 14:02:32.166966+0800 Demo[72183:21025125] x = 4, y = 18

可见,存在先后顺序。

4、Block的循环引用

首先,创建一份会循环引用的代码:
OC

typedef  void(^HelloBlock)(void);
@interface ViewController ()

@property (nonatomic, assign) int age;
@property (nonatomic, copy) HelloBlock block;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.age = 10;
    self.block = ^{
        NSLog(@"小明今天%d岁", self.age);
    };
    self.age = 20;
    self.block();
}

C++

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
   // block强引用了self
  ViewController *const __strong self;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们看到转换代码中,block结构体内是强引用了self。而self又本来就强引用block的,所以这就造成了循环引用。一般解决方法是加上__weak

OC

// OC代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        __weak Person *weakPerson = person;
        void (^block)(void) = ^{
            NSLog(@"age--- %ld",weakPerson.age);
         };
        block();

    }
    return 0;
}

C++

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
};

可见此时blockweakPerson的关键字变成了__weak

__weak __typeof (self)weakSelf = self;

_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"Change" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf reload];
    }
}

block之前定义对self的弱引用weakSelf,因为是弱引用,所以self被释放时weakSelf会变成nil,在block中引用该弱引用,考虑到多线程情况,通过强引用strongSelf来引用该弱引用,如果self不为nil,就会retain self,以防在block内部使用过程中self被释放,强引用strongSelfblock作用域结束之后,自动释放。若在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf;若在Block 内需要多次访问self,则需要使用 strongSelf

block中修饰被捕获的对象类型变量的关键字除了__strong__weak外还有一个__unsafe_unretained。那这结果关键字起什么作用呢?当block被拷贝到堆上时是调用的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数就会根据这3个关键字来进行操作。

  • 如果关键字是__strong,那block内部就会对这个对象进行一次retain操作,引用计数+1,也就是block会强引用这个对象。也正是这个原因,导致在使用block时很容易造成循环引用。
  • 如果关键字是__weak__unsafe_unretained,那block对这个对象是弱引用,不会造成循环引用。所以我们通常在block外面定义一个__weak__unsafe_unretained修饰的弱指针指向对象,然后在block内部使用这个弱指针来解决循环引用的问题。

block从堆上移除时,则会调用block内部的dispose函数,dispose函数内部调用_Block_object_dispose函数会自动释放强引用的变量。


五、Delegate与Protocol

Delegate

什么是Delegate与Protocol呢?
举个简单的例子,外卖app就是我的代理,我就是委托方,我买了一瓶红茶并付给外卖app钱,这就是购买协议。我只需要从外卖app上购买就可以,具体的操作都由外卖app去处理,我只需要最后接收这瓶红茶就可以。我付的钱就是参数,最后送过来的红茶就是处理结果。

有哪些特性呢?
如果只是某个类使用,我们常做的就是写在某个类中。如果是多个类都是用同一个协议,建议创建一个Protocol文件,在这个文件中定义协议。遵循的协议可以被继承,例如我们常用的UITableView,由于继承自UIScrollView的缘故,所以也将UIScrollViewDelegate继承了过来,我们可以通过代理方法获取UITableView偏移量等状态参数。

协议只能定义公用的一套接口,类似于一个约束代理双方的作用。但不能提供具体的实现方法,实现方法需要代理对象去实现。协议可以继承其他协议,并且可以继承多个协议,在iOS中对象是不支持多继承的,而协议可以多继承。

默认是@required状态的,无论是@optional还是@required,在委托方调用代理方法时都需要做一个判断,判断代理是否实现当前方法,否则会导致崩溃。

写个Dem可以吗?
委托类

// 登录协议
#import "LoginProtocol.h"

@interface LoginViewController : UIViewController
// 通过属性来设置代理对象
@property (nonatomic, weak) id delegate;
@end
 
@implementation LoginViewController
- (void)loginButtonClick:(UIButton *)button {
  // 判断代理对象是否实现这个方法,没有实现会导致崩溃
  if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {
      // 调用代理对象的登录方法,代理对象去实现登录方法
      [self.delegate userLoginWithUsername:self.username.text password:self.password.text];
  }
}

代理类

// 遵守登录协议
@interface ViewController ()  <LoginProtocol>
@end
 
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
 
    LoginViewController *loginVC = [[LoginViewController alloc] init];
    loginVC.delegate = self;// 设置实现协议的代理对象
    [self.navigationController pushViewController:loginVC animated:YES];
}
 
// 代理方实现具体登录细节
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password {
    NSLog(@"username : %@, password : %@", username, password);
}

原理是什么呢?
其实委托方的代理属性本质上就是代理对象自身,设置委托代理就是代理属性指针指向代理对象,相当于代理对象只是在委托方中调用自己的方法,如果方法没有实现就会导致崩溃。

为什么我们设置代理属性都使用weak呢?
由于代理对象使用强引用指针,引用创建的委托方LoginVC对象,并且成为LoginVC的代理。这就会导致LoginVCdelegate属性强引用代理对象,导致循环引用的问题,最终两个对象都无法正常释放。设置为弱引用属性。这样在代理对象生命周期存在时,可以正常为我们工作,如果代理对象被释放,委托方和代理对象都不会因为内存释放导致的Crash

实际运用场景举个例子?
控制器瘦身:UITableView的数据处理、展示逻辑和简单的逻辑交互都由代理对象去处理,传入一些参数,和控制器相关的逻辑处理传递出来,交由控制器来处理,这样控制器的工作少了很多,而且耦合度也大大降低了。


Demo

Demo在我的Github上,欢迎下载。
Notice-Block-Delegate-KVO-KVC

参考文献

轻松过面:一文全解iOS通知机制(经典收藏)
如何自己动手实现 KVO
iOS 开发:『Crash 防护系统』(二)KVO 防护
KVO进阶——KVO实现探究
iOS底层原理总结篇-- 深入理解 KVC\KVO 实现机制
iOS KVC和KVO详解

相关文章

网友评论

      本文标题:IOS基础:通知、KVO与KVC、Block、Delegate(

      本文链接:https://www.haomeiwen.com/subject/jvdnmktx.html