iOS开发-NSNotification源码原理学习

问题

苹果并没有开源相关代码,但是可以读下 GNUStep 的源码

或者这里下载
链接:https://pan.baidu.com/s/1F25GgeLxqKjeo10Zgfr2OQ
密码:qpka

从下列问题触发,探索下NSNotification的实现原理

  1. 实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等)
  2. 通知的发送时同步的,还是异步的
  3. NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何异步发送消息
  4. NSNotificationQueue是异步还是同步发送?在哪个线程响应
  5. NSNotificationQueuerunloop的关系
  6. 如何保证通知接收的线程在主线程
  7. 页面销毁时不移除通知会崩溃吗
  8. 多次添加同一个通知会是什么结果?多次移除通知呢
  9. 下面的方式能接收到通知吗?为什么
// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

1. 实现原理

对于addObserver:selector:name:object:会创建一个observation

typedef	struct	Obs {
  id		observer;	/* Object to receive message.	*/
  SEL		selector;	/* Method selector.		*/
  struct Obs	*next;		/* Next item in linked list.	*/
  int		retained;	/* Retain count for structure.	*/
  struct NCTbl	*link;		/* Pointer back to chunk table	*/
} Observation;

对于Observation持有observer

  • iOS SDK 8之前:不是一个类似OC中的weak类型,持有的相当与一个__unsafe_unretain指针对象,当对象释放时,会访问已经释放的对象,造成BAD_ACCESS
  • iOS SDK 8之后:持有的是weak类型指针,对nil对象performSelector不再会崩溃

nameObservation是映射关系,observersel包含在Observation结构体中。


此外,NSNotification维护了GSIMapTable表的结构,用于存储Observation,分别是namelessnamecachenameless存储没有传入名字的通知,named存储传入了名字的通知,cache用于快速缓存.

#define	CHUNKSIZE	128
#define	CACHESIZE	16
typedef struct NCTbl {
  Observation		*wildcard;	/* Get ALL messages.		*/
  GSIMapTable		nameless;	/* Get messages for any name.	*/
  GSIMapTable		named;		/* Getting named messages only.	*/
  unsigned		lockCount;	/* Count recursive operations.	*/
  NSRecursiveLock	*_lock;		/* Lock out other threads.	*/
  Observation		*freeList;
  Observation		**chunks;
  unsigned		numChunks;
  GSIMapTable		cache[CACHESIZE];
  unsigned short	chunkIndex;
  unsigned short	cacheIndex;
} NCTable;

这里值得注意namelessnamed的结构,虽然同为hash


在nameless表中:
GSIMapTable的结构如下
object : Observation
object : Observation
object : Observation

----------------------------
在named表中:
GSIMapTable结构如下:
name : maptable
name : maptable
name : maptable

maptable的结构如下
object : Observation
object : Observation
object : Observation



关于GSIMap结构

对于addObserver方法,为什么需要object参数?

- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

就是addObserver其实不用传入name也可以,传入object,当postNotification方法同样发出这个object时,就会触发通知方法。

例如这样的写法:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    MyObject *obj = [MyObject new];
    [center addObserver:self selector:@selector(doAction:) name:nil object:obj];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [center postNotificationName:@"TEST" object:obj];
    });
}

- (void)doAction:(NSNotification*)sender {
    NSLog(@"%s %@ %@",__FUNCTION__,sender,[NSThread currentThread]);
}

参考 https://www.jianshu.com/p/83770200d476

都传入null对象会怎么样

你可能也注意到了,addObserver方法nameobject都可以为空,这表示将会把observer赋值为 wildcard,他将会监听所有的通知。

addObserver源码逻辑

addObserver的逻辑如下

1. 根据传入的selector和observer创建Observation,并存入maptable中,如果已存在,则是从cache中取。
2. 如果name存在,则向named的maptable表中插入元素,key为name,value为GSIMapTable,GSIMapTable中存key为object,value为Observation,转入5。如果不存在则进入3
3. 如果object存在,则向nameless的maptable表中插入元素,key为object,value为Observation。如果不存在,则进入4,否则转入5
4. name和object都为空,则Observation->next=wildcard,将老的wildcard赋值为next指针,然后Observation对象置为wildcard,wildcard = Observation
5. 结束

2.通知的发送时同步的,还是异步的

postNotificationName的底层实现是

- (void) _postAndRelease: (NSNotification*)notification

由于内部会读取TABLE

lockNCTable(TABLE);
... //找到对应的observer对象
unlockNCTable(TABLE);

...//执行performSelector方法

lockNCTable(TABLE);
GSIArrayEmpty(a); //释放临时创建的数组对象 - 用于存储observer的
unlockNCTable(TABLE);

同步异步这个问题,由于TABLE资源的问题,同一个线程会按顺序执行,自然是同步的。

3. NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何异步发送消息

由于是使用的performSelector方法,没有进行转线程,默认是postNotification方法的线程。


[o->observer performSelector: o->selector
                                withObject: notification];

对于异步发送消息,可以使用NSNotificationQueue,queue顾明意思,我们是需要将NSNotification放入queue中执行的。

有三种状态

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1,      // 当runloop处于空闲状态时post
    NSPostASAP = 2,    // 当当前runloop完成之后立即post
    NSPostNow = 3    // 立即post
};
NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP];

参考文章 https://www.jianshu.com/p/356f7af4f2ee

4.NSNotificationQueue和runloop的关系

NSNotificationQueue的执行是依赖于runloop的,它的三种模式各自的执行时机不一样。

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1,      // 当runloop处于空闲状态时post
    NSPostASAP = 2,    // 当当前runloop完成之后立即post
    NSPostNow = 3    // 立即post
};

例如

void asyncQueueNotiInRunloop() {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1");
        NSLog(@"%@", [NSThread currentThread]);
        
        //NSPostWhenIdle
        //NSPostASAP
        //NSPostNow
        NSNotification *notification = [NSNotification notificationWithName:@"TEST" object:nil];
        [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
        // run runloop
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSRunLoopCommonModes];
        CFRunLoopRun();
        NSLog(@"3");
    });
}

如果去掉run runloop部分的代码,则无法触发通知

5.如何保证通知接收的线程在主线程

由于通知的发出使用performSeletor实现,如果需要保证接收的线程在主线程,可以:

  1. 保证主线程发出
  2. 接收到通知后跳转到主线程,苹果建议使用NSMachPort进行消息转发到主线程。

https://blog.csdn.net/shengpeng3344/article/details/90206265

  1. 使用block接口addObserverForName:object:queue:usingBlock:
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
    // The return value is retained by the system, and should be held onto by the caller in
    // order to remove the observer with removeObserver: later, to stop observation.
  1. 页面销毁时不移除通知会崩溃吗?

对于Observation持有observer

  • iOS SDK 8之前:不是一个类似OC中的weak类型,持有的相当与一个__unsafe_unretain指针对象,当对象释放时,会访问已经释放的对象,造成BAD_ACCESS
  • iOS SDK 8之后:持有的是weak类型指针,对nil对象performSelector不再会崩溃

所以说不一定会崩溃,但是根据代码严谨是需要remove

6.多次添加同一个通知会是什么结果?多次移除通知呢?

由于源码中并不会进行重复过滤,所以添加同一个通知,等于就是添加了2次,回调也会触发两次。

为什么会触发两次呢,因为- (void) postNotificationName: (NSString*)name object: (id)object的逻辑是这样的:

1. 查找所有是wildcard类型的Observation,加入数组array,即既不监听name也不监听object
2. 查找指定object但未指定name的Observation,加入数组array
3. 查找指定了相同name和object的Observation,加入数组array,当name一致但object不一致时,也不会加入到数组array,但如果传入的object!=nil,则会将name对应的maptable中,所有key为nil的Observation也加入数组。
4. 遍历array,执行performSelector
5. 清空array

第三步的意思是:如果发出一个通知,方法中传入了对象object,那么那些只监听通知name,object设置为nil的当然也可以收到,object匹配了的也可以收到,object不匹配的则收不到。

关于多次移除,并没有问题,因为会去map中查找,找到才会删除。当nameobject都为nil时,会移除所有关于该observerWILDCARD

7.下面的方式能接收到通知吗?为什么


[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];

[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

根据postNotification的实现,会找到keyTestNotification的maptable,再从中选择keynilobservation,所以是找不到以@1为key的observation

内容太多,有错误还望指出下,我也才每隔一段时间重新审视下博文


是不是对一个人越热情,那个人对你就越冷

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值