iOS常见的音讯告诉机制有如下三种,delegate不多赘述,本篇主要探求KVO
与NSNotification
的底层原理完成及比照。
源码地址:gnu/lib-base
方式 | delegate | KVO | NSNotificationCenter |
---|---|---|---|
告诉方式 | 1对1 | 一对多 | 一对多 |
适用言语 | 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
Notification
structure, which bridges to theNSNotification
class.(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
,保证线程安全 -
创立调查者目标
Observation
o
,存储传入的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
不为空,从nameless
map中找到对应链表并存储(流程同上) -
若参数
name
和object
都为空,即代表接纳一切音讯,使NSNotificationCenter
的wildcard
指向该Observation
目标o
同理可推得,在发布告诉的时分,也是依据上述的数据结构和流程取得对应Observation
目标,并履行selector
对应的办法,如下所示。
[o->observer performSelector: o->selector
withObject: notification];
比照
KVO
- 长处
– 能够获取到被调查目标的新旧值改变
– 能够调查嵌套目标
- 缺陷
– 只能调查特点的改变
– 调查的特点只能用NSString来定义,无法进行编译查看
NotificationCenter
- 长处
– 无需获取到被调查目标就能增加调查者
– 能够调查自定义事情,而纷歧定是特点
– 关于一个宣布的告诉,多个目标能够做出反应,即1对多的方式完成简单
- 缺陷
– 需求被调查目标主动抛出事情
– 需求及时移除调查者否则或许导致溃散