iOS常见的消息通知机制有如下三种,delegate不多赘述,本篇主要探究KVO与NSNotification的底层原理实现及对比。
源码地址:gnu/lib-base
| 方式 | delegate | KVO | NSNotificationCenter |
|---|---|---|---|
| 通知方式 | 一对一 | 一对多 | 一对多 |
| 适用语言 | OC+Swift | OC+继承自NSObject的Swift类对象 | OC+Swift |
| 描述 | 可以有返回值互相的引用关系协同工作 处理业务逻辑 | 只能作用在value以及变化上需要相互依赖单方向无协同 | 能携带更多的信息常用于系统级消息无依赖单方向无协同 |
KVO
基础
-
KVO键值观察是一种机制,允许将其他对象的指定属性更改通知给观察者对象
-
基于KVC
You can only use key-value observing with classes that inherit from NSObject.Mark properties that you want to observe through key-value observing with both the @objc attribute and the @dynamic modifier.(你只能对继承自NSObject的类使用KVO。用@objc属性和@dynamic修饰符标记您想要通过键值观察来观察的属性)
KVO基本用法如下
// 添加观察者
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
// 实现KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
// 移除观察者
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
数据结构
GNU内部相关数据结构如下所示,下面探究底层实现时会用到
// 静态全局变量
static NSRecursiveLock *kvoLock = nil;
static NSMapTable *classTable = 0;
static NSMapTable *infoTable = 0;
// 涉及类型
@interface GSKVOReplacement : NSObject {
Class original; // 原始类
Class replacement; // 替代类
NSMutableSet *keys; // 观察者setter的keys
}
@end
@interface GSKVOInfo : NSObject {
NSObject *instance; // 观察者(不影响引用计数).
NSRecursiveLock *iLock; // 递归锁
NSMapTable *paths; // 观察的keyPaths map[调用添加方法时传的keyPath: ?]
}
// 用于记录一个keyPath的观察结果和发送通知过程的递归状态。
@interface GSKVOPathInfo : NSObject {
@public
unsigned recursion;
unsigned allOptions;
NSMutableArray *observations;
NSMutableDictionary *change;
}
@end
// 记录一次观察的所有信息。
@interface GSKVOObservation : NSObject {
@public
NSObject *observer; // Not retained (zeroing weak pointer)
void *context;
int options;
}
@end
底层探究
从添加观察者入手addObserver:forKeyPath:options:context:方法入手【该过程通过kvoLock 加锁】
-
若
classTable中没有存储被观察对象的类对应的替代类(即未创建过),创建一个GSKVOReplacement对象【该过程通过kvoLock加锁】,存储到classTable中【该过程通过kvoLock加锁】
/**
* 替代类GSKVOReplacement的创建
* 创建原始类的子类,并使用抽象基类的实现重写一些方法。
*/
superName = NSStringFromClass(original);
// 1. 子类名称为GSKVO拼接原始类名
name = [@"GSKVO" stringByAppendingString: superName];
// 2. 这里用到了runtime的动态性去动态地创建了一个类:objc_registerClassPair
template = GSObjCMakeClass(name, superName, nil);
GSObjCAddClasses([NSArray arrayWithObject: template]);
replacement = NSClassFromString(name);
// 3. 在该方法中,用runtime接口class_copyMethodList 递归 获取原始类的实例方法和类方法,添加到替代类中。
GSObjCAddClassBehavior(replacement, baseClass);
- 若
infoTable中没有存储本次观察的信息
1. 创建一个GSKVOInfo对象【该过程通过kvoLock 加锁】
2. 存储到infoTable中【该过程通过kvoLock 加锁】,
3. 将被观察对象设置成第1步中创建的GSKVOReplacement 替代类类型
// a
// 存储被观察对象
instance = i;
// 创建一个容量为8的map,
paths = NSCreateMapTable(NSObjectMapKeyCallBacks,
NSObjectMapValueCallBacks,
8);
iLock = [NSRecursiveLock new];
// b
// 可见infoTable的key为被观察对象的指针地址,value为上面创建的GSKVOInfo对象
NSMapInsert(infoTable, (void*)self, observationInfo);
// c
object_setClass(self, [r replacement]);
前3步可以说都是在做准备,下面才开始添加观察者。
-
递归获取传入的
keyPath中字符”.”的位置调用addObserver:forKeyPath:options:context:方法,直到keyPath中没有”.”,执行下一步 -
替换替代类中对应
keyPath属性的setter方法 -
调用第3步创建的
GSKVOInfo的addObserver:forKeyPath:options:context:方法
1. 判断观察者是否实现observeValueForKeyPath:ofObject:change:context:,没实现就退出
2. 从paths中获取传入的keyPath对应的GSKVOPathInfo(没有则创建)
3. 遍历GSKVOPathInfo中的所有观察者(GSKVOObservation列表),若当前keyPath存在当前观察者,将新的context和options覆盖掉原来的
4. 若没有GSKVOObservation,则创建,注意:在创建时,使observer实例变量弱引用观察者
5. 若options中存在NSKeyValueObservingOptionInitial,向观察者发送现有值的通知(key为NSKeyValueObservingOptionNew)
// c
while (count-- > 0) {
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver) {
o->context = aContext;
o->options = options;
observation = o;
}
pathInfo->allOptions |= o->options;
}
// d
observation = [GSKVOObservation new];
// 弱引用,不会影响观察者的引用计数
GSAssignZeroingWeakPointer((void**)&observation->observer,
(void*)anObserver);
// e
[pathInfo->change setObject: [NSNumber numberWithInt: 1]
forKey: NSKeyValueChangeKindKey];
// 调用观察者实现的observeValueForKeyPath:ofObject:change:context: 观察方法
[anObserver observeValueForKeyPath: aPath
ofObject: instance
change: pathInfo->change
context: aContext];
NSNotificationCenter
基础
-
中心化的注册和广播逻辑
-
主要关注系统性事件,而不是特有对象的事件
-
常用于:前后台切换 内存警告
The Swift overlay to the Foundation framework provides the
Notificationstructure, which bridges to theNSNotificationclass.(Swift覆盖到Foundation框架提供了Notification结构,它桥接到OC的NSNotification类。)
NSNotificationCenter基本用法如下
/** 向NSNotificationCenter注册观察者 */
// 注:该返回值将被系统保留。调用者应该持有该值,以便使用removeObserver删除观察者:稍后,停止观察。
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
object:(nullable id)obj
queue:(nullable NSOperationQuzeue *)queue
usingBlock:(void (NS_SWIFT_SENDABLE ^)(NSNotification *note))block;
- (void)addObserver:(id)observer
selector:(SEL)aSelector
name:(nullable NSNotificationName)aName
object:(nullable id)anObject;
/** 移除观察者 */
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer
name:(nullable NSNotificationName)aName
object:(nullable id)anObject;
/** 发送通知 */
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName
object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName
object:(nullable id)anObject
userInfo:(nullable NSDictionary *)aUserInfo;
对象向NSNotificationCenter注册以接收通知(NSNotification)。当对象将自己添加为观察者时,它将指定应该接收哪些通知。因此,一个对象可以多次调用此方法,以便将自己注册为多个不同通知的观察者。
每个正在运行的应用程序都有一个defaultCenter,也可以创建新的NSNotificationCenter来组织特定上下文中的通信。
NSNotificationCenter不保留观察者或对象。因此,在释放这些对象之前,你应该总是发送removeObserver:或removeObserver:name:object:到通知中心。
通知中心只能在单个程序中发送通知。如果想发布一个通知到其他进程或从其他进程接收通知,使用
NSDistributedNotificationCenter。
数据结构
NSNotificationCenter数据结构如下,在NSNotificationCenter内部存有一个成员变量_table,本质是NSTable 结构体,其中存储了一个Observation结构体,根据其中的struct Obs *next可以看出观察者是一个****链表结构。** **
@interface NSNotificationCenter : NSObject
{
@private
void *_table;
}
typedef struct NCTbl {
Observation *wildcard; // 若name和object都为空,观察者存储在这里
GSIMapTable nameless; // 若name为空,object不为空,观察者存储在这里
GSIMapTable named; // 若name不为空,观察者存储在这里
NSRecursiveLock *_lock; // 用NSRecursiveLock来保证线程安全
···
} NCTable;
// Observation观察着对象(GNU内部结构)
typedef struct Obs {
id observer; // 接收消息的对象
SEL selector; // 接收消息的方法selector
struct Obs *next; // 指向下一个观察者
···
} Observation;
// named对应的GSIMapTable结构
struct _GSIMapTable {
GSIMapBucket buckets; /* Array of buckets. */
GSIMapNode freeNodes; /* List of unused nodes. */
GSIMapNode *nodeChunks; /* Chunks of allocated memory. */
···
};
NSNotification数据结构如下
@interface NSNotification : NSObject <NSCopying, NSCoding>
@property (readonly, copy) NSNotificationName name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;
@end
底层探究
我们以添加观察者的addObserver:selector:name:object:方法为例探究
-
加锁
NSRecursiveLock,保证线程安全 -
创建观察者对象
Observationo,存储传入的selector和observer的指针地址 -
若参数
name不为空,执行如下处理
// 1. 从center的named中以name(hash)为key,获取对应的GSIMapTable【以下简称nameTable】
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0) {
// 1.1 若不存在,则创建,并存储到链表中
m = mapNew(TABLE);
// 注意:由于这是对name第一次添加观察,所以获取name的copy,确保不会出现外部修改name值导致的问题。
name = [name copyWithZone: NSDefaultMallocZone()];
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
···
} else {
// 1.2 若存在,则拿到nameTable中的GSIMapTable的指针地址
m = (GSIMapTable)n->value.ptr;
}
// 2. 以传入的object为key,找到对应结点GSIMapNode
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0) {
// 2.1 若不存在,则创建,并存储到nameTable中
o->next = ENDOBS;
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
} else {
// 2.2 若存在,将observation插入到对应链表头部
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
-
若参数
name为空,object不为空,从namelessmap中找到对应链表并存储(流程同上) -
若参数
name和object都为空,即代表接收所有消息,使NSNotificationCenter的wildcard指向该Observation对象o
同理可推得,在发布通知的时候,也是根据上述的数据结构和流程取得对应Observation对象,并执行selector对应的方法,如下所示。
[o->observer performSelector: o->selector
withObject: notification];
对比
KVO
- 优点
- 可以获取到被观察对象的新旧值变化
- 可以观察嵌套对象
- 缺点
- 只能观察属性的变化
- 观察的属性只能用NSString来定义,无法进行编译检查
NotificationCenter
- 优点
- 无需获取到被观察对象就能添加观察者
- 可以观察自定义事件,而不一定是属性
- 对于一个发出的通知,多个对象能够做出反应,即1对多的方式实现简单
- 缺点
- 需要被观察对象主动抛出事件
- 需要及时移除观察者不然可能导致崩溃