原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 比较
- 使用
- 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
KVO
是 Objective-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
中,NSObject
、NSArray
、NSSet
均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或集合对象。
/** 注册
* 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
类的 age
和 grade
属性。为此,在 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
会保证下列两种情况的发生:
- 如果我们调用一个支持
KVO
的setter
方法,如self.exchangeRate = 2.345;
,KVO
能保证所有exchangeRate
的观察者在setter
方法返回前被通知到。 - 其次,如果某个键被观察的时候附上了
NSKeyValueObservingOptionPrior
选项,直到-observe...
被调用之前,exchangeRate
的accessor
方法都会返回同样的值。
2、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
添加的方法。例如当我们调用person
的setName
方法时,实际是调用的一个子类实例的setName
方法,但由于重写了class
,在外部看不出来其中的差别。在setter
方法中,我们在实际值被改变的前后回调用- (void)willChangeValueForKey:(NSString *)key;
和- (void)didChangeValueForKey:(NSString *)key;
方法,通知观察者值的变化。
并且见到即使是私有属性,通过KVC
和KVO
的方式也可以对其进行更改,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
可以看到在两个不同的线程里创建的Observer
和Target
,观察变化也是能够生效的。
这里有一个关于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
属性依赖于 firstName
和lastName
,当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";
},旧值:诸葛 四郎,新值:诸葛 四郎
虽然KeyPath
是fullName
, 但是你对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。
FBKVOController
对KVO
机制进行了额外的一层封装,框架不但可以自动帮我们移除观察者,还提供了block
或者selector
的方式供我们进行观察处理。不可否认的是,FBKVOController
为我们的开发提供了很大的便利性。但是相对而言,这种方式对项目代码的侵入性比较大,必须考编码规范来强制约束团队人员使用这种方式。
那么有没有一种对项目代码侵入性小,同时还能有效防护KVO
崩溃的防护机制呢?
b、自己实现防护
思路:
- 添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。
- 移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。
- 观察键值改变时:同样通过关系哈希表判断,将改变操作分发到原有的观察者上。
- 另外,为了避免被观察者提前被释放,被观察者在
dealloc
时仍然注册着KVO
导致崩溃。还利用Method Swizzling
实现了自定义的dealloc
,在系统dealloc
调用之前,将多余的观察者移除掉。
具体实现的Demo:bujige / 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、首先,我们创建NSObject
的 Category
,并在头文件中添加两个 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>
因为bBlock
和cBlock
只使用了静态变量和入参,不需要捕获外部变量,所以为全局Block __NSGlobalBlock__
,存在于全局区,内存在程序结束后由系统回收。
最后一个使用了外部变量num
,为栈Block__NSStackBlock__
,内存由编译器控制,过了作用域就会被回收。
而aBlock
虽然也只使用了外部变量,但由于在ARC
下会自动调用一次copy
方法,将Block
从栈区copy
到堆区,所以aBlock
为堆Block__NSMallocBlock__
,内存由ARC
控制,没有强指针指向时释放。而在MRC
中,赋值不会执行copy
操作,所以aBlock
依然会存在于栈中,所以在MRC
中一般都需要执行copy
,否则很容易造成crash
。
在ARC
中,当Block
作为属性被strong
、copy
修饰或被強指针应用或作为函数返回值或被赋值给Block
类型成员变量时,都会默认执行copy
方法。而MRC
中,只有被copy
修饰时,Block
才会执行copy
。所以MRC
中Block
都需要用copy
来修饰,而在ARC
中用copy
修饰只是沿用了MRC
的习惯,此时用copy
和strong
效果是相同的。
-
NSGlobalBlock
使用copy
还是NSGlobalBlock
什么都不做 -
NSStackBlock
使用copy
,变为NSMallocBlock
-
NSMallocBlock
使用copy
还是NSMallocBlock
,只是引用计数+1

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
将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;
};
可见此时block
中weakPerson
的关键字变成了__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
被释放,强引用strongSelf
在block
作用域结束之后,自动释放。若在 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与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
的代理。这就会导致LoginVC
的delegate
属性强引用代理对象,导致循环引用的问题,最终两个对象都无法正常释放。设置为弱引用属性。这样在代理对象生命周期存在时,可以正常为我们工作,如果代理对象被释放,委托方和代理对象都不会因为内存释放导致的Crash
。
实际运用场景举个例子?
控制器瘦身:UITableView
的数据处理、展示逻辑和简单的逻辑交互都由代理对象去处理,传入一些参数,和控制器相关的逻辑处理传递出来,交由控制器来处理,这样控制器的工作少了很多,而且耦合度也大大降低了。
Demo
Demo在我的Github上,欢迎下载。
Notice-Block-Delegate-KVO-KVC
参考文献
轻松过面:一文全解iOS通知机制(经典收藏)
如何自己动手实现 KVO
iOS 开发:『Crash 防护系统』(二)KVO 防护
KVO进阶——KVO实现探究
iOS底层原理总结篇-- 深入理解 KVC\KVO 实现机制
iOS KVC和KVO详解
网友评论