1. 程式人生 > >NSNotificationCenter+RACSupport把我坑了

NSNotificationCenter+RACSupport把我坑了

熟悉RAC的,應該都知道它本身針對iOS系統類提供了許多類目用於增加方法,方便使用。但是,今天在使用NSNotificationCenter+RACSupport的時候遇到了坑,接下來便分享出來。

下面用到的完成測試用例在這裡

首頁,建立兩個頁面A、B,然後A訂閱通知,B傳送通知,觀察通知的傳遞。
當點選A中的按鈕跳轉的B的頁面時,B傳送通知,這時候A收到通知。日誌如下

2018-09-06 18:14:20.902227+0800 TestRAC+NSNotification[35033:8811463] A收到B的通知了

這時是沒有問題的。

那如果這兩個頁面的通知順序反過來呢?

新建C頁面,並且在C頁面訂閱通知。然後先點選A頁面按鈕跳轉到B,日誌如下:

2018-09-06 18:20:43.901481+0800 TestRAC+NSNotification[35325:8830635] A收到B的通知了

跟上一步一樣,沒有什麼問題。接著繼續點選按鈕,跳轉到C頁面,然後返回到B頁面,繼續點選通知按鈕,日誌如下:

2018-09-06 18:20:57.481049+0800 TestRAC+NSNotification[35325:8830635] A收到B的通知了
2018-09-06 18:20:57.481345+0800 TestRAC+NSNotification[35325:8830635] C收到B的通知了

What?這是什麼情況,為毛C也能收到通知。難道C沒有被釋放嗎?

在C中新增如下程式碼:

- (void)dealloc
{
    NSLog(@"c掛了");
}

重新執行,看看C有沒有掛。當從C頁面返回時,日誌如下:

2018-09-06 18:22:53.286350+0800 TestRAC+NSNotification[35424:8837258] c掛了

C頁面確實掛了,但是仍舊能夠收到通知資訊。
接著點選通知按鈕,整個過程的日誌如下:

2018-09-06 18:22:48.908253+0800 TestRAC+NSNotification[35424:8837258] A收到B的通知了
2018-09-06 18:22:53.286350+0800 TestRAC+NSNotification[35424:8837258] c掛了
2018-09-06 18:24:33.474609+0800 TestRAC+NSNotification[35424:8837258] A收到B的通知了
2018-09-06 18:24:33.475009+0800 TestRAC+NSNotification[35424:8837258] C收到B的通知了

看到了吧,這就是我遇到的坑。那為什麼會這個樣子呢。其實是因為rac_addObserverForName:方法的實現:

- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object {
    @unsafeify(object);
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        @strongify(object);
        id observer = [self addObserverForName:notificationName object:object queue:nil usingBlock:^(NSNotification *note) {
            [subscriber sendNext:note];
        }];

        return [RACDisposable disposableWithBlock:^{
            [self removeObserver:observer];
        }];
    }] setNameWithFormat:@"-rac_addObserverForName: %@ object: <%@: %p>", notificationName, [object class], object];
}

這個方法返回一個訊號,建立訊號時通過self呼叫addObserverForName:方法訂閱通知。接著返回一個清理物件,清理物件的工作是removeObserver

addObserverForName:方法不熟悉的可以看下方法的註釋:

- (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.

返回一個被系統持有的物件,並且這個物件應當被呼叫者拿到,稍後用於呼叫removeObserver:方法將其移除來停止觀察。

所以,既然上面的C中通知能夠繼續回撥,證明removeObserver:沒有被呼叫。為什麼呢?

原因有兩點。
1. 訊號的建立中只調用了sendNext:方法,沒有呼叫sendError: sendCompleted方法,所以清理物件的清理方法不會呼叫。
2. 這裡的self[NSNotificationCenter defaultCenter]物件,這個物件是單例物件,所以不會釋放,這樣清理物件也不會呼叫清理方法。

既然存在這種問題,那我們應該怎麼解決呢?

其實我們可以直接使用addObserverForName: API,這樣子我們既可以使用回撥的方式處理通知,也可以取消通知的訂閱。

新建D頁面新增如下程式碼:

- (void)viewDidLoad {
    [super viewDidLoad];

    __block id observer;
    observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"B" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"D收到B的通知了");
        [[NSNotificationCenter defaultCenter] removeObserver:observer];
    }];
}

- (void)dealloc
{
    NSLog(@"c掛了");
}

同樣的操作過程,列印日誌如下:

2018-09-06 18:44:22.613633+0800 TestRAC+NSNotification[36323:8903067] A收到B的通知了
2018-09-06 18:44:26.360049+0800 TestRAC+NSNotification[36323:8903067] c掛了
2018-09-06 18:44:27.021684+0800 TestRAC+NSNotification[36323:8903067] A收到B的通知了
2018-09-06 18:44:28.830822+0800 TestRAC+NSNotification[36323:8903067] A收到B的通知了
2018-09-06 18:44:29.302511+0800 TestRAC+NSNotification[36323:8903067] A收到B的通知了

可以看到,不管點選多少次按鈕,D都不會接收到通知的。