美文网首页
NSNotificationCenter源码学习

NSNotificationCenter源码学习

作者: jayhe | 来源:发表于2019-09-27 16:37 被阅读0次
    • 之前在看重复的NSTimer在加到runloop之后,发现由于runloop会强持有observer,导致在dealloc中去invalidate不会起作用的问题;联想到通知也是addObserver,我们一般都是在dealloc中去removeObserver,而observer的dealloc可以正常调用,就想了解一下这其中的不同

    *iOS9之前addObserver,不remove也不会造成observer的泄漏,但是会有野指针的问题,为什么?

    • addObserver多次一个observer,通知会回调多次,为什么?

    一般我们都会要在dealloc中去removeObserver,iOS9之后则不需要,系统帮我们处理了
    带着这样的疑问,通过GNU的源码来看一下内部的实现

    addObserver

    - (void) addObserver: (id)observer
                selector: (SEL)selector
                    name: (NSString*)name
                  object: (id)object
    {
        IMP     method;
        Observation *list;
        Observation *o;
        GSIMapTable m;
        GSIMapNode  n;
        
        if (observer == nil)
            [NSException raise: NSInvalidArgumentException
                        format: @"Nil observer passed to addObserver ..."];
        
        if (selector == 0)
            [NSException raise: NSInvalidArgumentException
                        format: @"Null selector passed to addObserver ..."];
        
    #if defined(DEBUG)
        if ([observer respondsToSelector: selector] == NO)
            NSLog(@"Observer '%@' does not respond to selector '%@'", observer,
                  NSStringFromSelector(selector));
    #endif
        
        method = [observer methodForSelector: selector];
        if (method == 0)
            [NSException raise: NSInvalidArgumentException
                        format: @"Observer can not handle specified selector"];
        
        lockNCTable(TABLE);
        
        o = obsNew(TABLE, selector, method, observer);
        
        if (object != nil)
        {
            object = CHEATGC(object);
        }
        
        /*
         * Record the Observation in one of the linked lists.
         *
         * NB. It is possible to register an observer for a notification more than
         * once - in which case, the observer will receive multiple messages when
         * the notification is posted... odd, but the MacOS-X docs specify this.
         */
        
        if (name)
        {
            /*
             * Locate the map table for this name - create it if not present.
             */
            n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
            if (n == 0)
            {
                m = mapNew(TABLE);
                /*
                 * As this is the first observation for the given name, we take a
                 * copy of the name so it cannot be mutated while in the map.
                 */
                name = [name copyWithZone: NSDefaultMallocZone()];
                GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
                GS_CONSUMED(name)
            }
            else
            {
                m = (GSIMapTable)n->value.ptr;
            }
            
            /*
             * Add the observation to the list for the correct object.
             */
            n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
            if (n == 0)
            {
                o->next = ENDOBS;
                GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
            }
            else
            {
                list = (Observation*)n->value.ptr;
                o->next = list->next;
                list->next = o;
            }
        }
        else if (object)
        {
            n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
            if (n == 0)
            {
                o->next = ENDOBS;
                GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
            }
            else
            {
                list = (Observation*)n->value.ptr;
                o->next = list->next;
                list->next = o;
            }
        }
        else
        {
            o->next = WILDCARD;
            WILDCARD = o;
        }
        
        unlockNCTable(TABLE);
    }
    

    简化一下流程:

    • 异常判断处理
    • 创建observer实例o = obsNew(TABLE, selector, method, observer)
    • 添加observer到list中

    具体的过程可参照下图理解


    图片.png

    由于通知内部存储observation实例用的是map,observation实例存放在链表中,没有去重的处理,所以添加一个观察者,多次那么久会在mapTable中存在多个,发送通知的时候也会触发多次

    obsNew

    看看obsNew内部是如何创建observation实例的

    static Observation *
    obsNew(NCTable *t, SEL s, IMP m, id o)
    {
        Observation *obs;
        
    #if __OBJC_GC__
        
        /* With clang GC, observations are garbage collected and we don't
         * use a cache.  However, because the reference to the observer must be
         * weak, the observation has to be an instance of a class ...
         */
        static Class observationClass;
        
        if (0 == observationClass)
        {
            observationClass = [GSObservation class];
        }
        obs = NSAllocateObject(observationClass, 0, _zone);
        
    #else
        
        /* Generally, observations are cached and we create a 'new' observation
         * by retrieving from the cache or by allocating a block of observations
         * in one go.  This works nicely to both hide observations from the
         * garbage collector (when using gcc for GC) and to provide high
         * performance for situations where apps add/remove lots of observers
         * very frequently (poor design, but something which happens in the
         * real world unfortunately).
         */
        if (t->freeList == 0)
        {
            Observation *block;
            
            if (t->chunkIndex == CHUNKSIZE)
            {
                unsigned    size;
                
                t->numChunks++;
                
                size = t->numChunks * sizeof(Observation*);
                t->chunks = (Observation**)NSReallocateCollectable(
                                                                   t->chunks, size, NSScannedOption);
                
                size = CHUNKSIZE * sizeof(Observation);
                t->chunks[t->numChunks - 1]
                = (Observation*)NSAllocateCollectable(size, 0);
                t->chunkIndex = 0;
            }
            block = t->chunks[t->numChunks - 1];
            t->freeList = &block[t->chunkIndex];
            t->chunkIndex++;
            t->freeList->link = 0;
        }
        obs = t->freeList;
        t->freeList = (Observation*)obs->link;
        obs->link = (void*)t;
        obs->retained = 0;
        obs->next = 0;
    #endif
        
        obs->selector = s;
        obs->method = m;
    #if GS_WITH_GC
        GSAssignZeroingWeakPointer((void**)&obs->observer, (void*)o);
    #else
        obs->observer = o;
    #endif
        
        return obs;
    }
    

    简化一下:

    • 创建Observation实例;会判断是否支持GC,来创建GSObservation对象或者Observation结构体
    • 将name、selector、observer、imp赋值给实例

    GSObservationObservation结构体的定义如下:

    /*
     * Observation structure - One of these objects is created for
     * each -addObserver... request.  It holds the requested selector,
     * name and object.  Each struct is placed in one LinkedList,
     * as keyed by the NAME/OBJECT parameters.
     * If 'next' is 0 then the observation is unused (ie it has been
     * removed from, or not yet added to  any list).  The end of a
     * list is marked by 'next' being set to 'ENDOBS'.
     *
     * This is normally a structure which handles memory management using a fast
     * reference count mechanism, but when built with clang for GC, a structure
     * can't hold a zeroing weak pointer to an observer so it's implemented as a
     * trivial class instead ... and gets managed by the garbage collector.
     */
    #ifdef __OBJC_GC__
    
    @interface  GSObservation : NSObject
    {
      @public
      __weak id observer;   /* Object to receive message.   */
      SEL       selector;   /* Method selector.     */
      IMP       method;     /* Method implementation.   */
      struct Obs    *next;      /* Next item in linked list.    */
      struct NCTbl  *link;      /* Pointer back to chunk table  */
    }
    @end
    @implementation GSObservation
    @end
    #define Observation GSObservation
    
    #else
    
    typedef struct  Obs {
      id        observer;   /* Object to receive message.   */
      SEL       selector;   /* Method selector.     */
      IMP       method;     /* Method implementation.   */
      struct Obs    *next;      /* Next item in linked list.    */
      int       retained;   /* Retain count for structure.  */
      struct NCTbl  *link;      /* Pointer back to chunk table  */
    } Observation;
    
    #endif
    

    可以看到GC的情况下,内部有一个weak修饰的observer属性,弱引用观察者对象

    GSAssignZeroingWeakPointer((void**)&obs->observer, (void*)o);
    
    BOOL
    GSAssignZeroingWeakPointer(void **destination, void *source)
    {
      objc_assign_weak(source, (id*)destination);
      return YES;
    }
    
    id objc_assign_weak(id value, id *location) 
        { return (*location = value); }
    

    在非GC的情况下,直接将结构体obs的observer赋值为观察者,关键代码如下

    obs->observer = o;
    

    这种情况下,结构体引用观察者,但是不会造成retainCount+1;
    由于不知道结构体的内存管理方式,写了测试代码验证在手动管理内存的情况下结构体对对象的引用,引用计数的变化,测试代码如下:

    #import "TestCode.h"
    
    typedef struct  Obs {
        id        observer;   /* Object to receive message.   */
        int       retained;
    } Observation;
    
    @implementation TestCode
    
    - (instancetype)init {
        self = [super init];
        if (self) {
            [self test];
        }
        
        return self;
    }
    
    - (void)test {
        NSLog(@"%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(self)));
        Observation *obs = malloc(sizeof(Observation));
    //    obs->retained = 0;
        obs->observer = self;
        NSLog(@"%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(self)));
        /*
         2019-09-19 09:57:41.229922+0800 RuntimeLearning[99635:3579888] 1
         2019-09-19 09:57:44.509753+0800 RuntimeLearning[99635:3579888] 1
         */
    }
    
    @end
    

    我们发现在GC或者非GC的情况下,addObserver的时候,内部对观察者的引用,都不造成观察者引用计数的增加;所以这也是我们没有removeObserver的时候,观察者的dealloc也会执行

    那么此时还有一个问题,就是虽然观察者释放了,但是如果不removeObserver的话那么还是会有野指针的情况发生,iOS9之后我们不需要手动去移除了,系统是怎么做到了。我们通过打一个符号断点


    图片.png

    当观察者(以VC为例)执行dealloc的时候发现,最终UIViewController的dealloc内部调用了removeObserver:


    图片.png

    这里我们已经知道了,系统帮我们做了处理,所以iOS9之后即使不写removeObserver也不会发生野指针的崩溃问题了

    removeObserver

    remove主要是按照observer、name、object入参,删除mapTable中对应的observer

    - (void) removeObserver: (id)observer
                       name: (NSString*)name
                     object: (id)object
    {
        if (name == nil && object == nil && observer == nil)
            return;
        
        /*
         *  NB. The removal algorithm depends on an implementation characteristic
         *  of our map tables - while enumerating a table, it is safe to remove
         *  the entry returned by the enumerator.
         */
        
        lockNCTable(TABLE);
        
        if (object != nil)
        {
            object = CHEATGC(object);
        }
        
        if (name == nil && object == nil)
        {
            WILDCARD = listPurge(WILDCARD, observer);
        }
        
        if (name == nil)
        {
            GSIMapEnumerator_t  e0;
            GSIMapNode      n0;
            
            /*
             * First try removing all named items set for this object.
             */
            e0 = GSIMapEnumeratorForMap(NAMED);
            n0 = GSIMapEnumeratorNextNode(&e0);
            while (n0 != 0)
            {
                GSIMapTable     m = (GSIMapTable)n0->value.ptr;
                NSString        *thisName = (NSString*)n0->key.obj;
                
                n0 = GSIMapEnumeratorNextNode(&e0);
                if (object == nil)
                {
                    GSIMapEnumerator_t  e1 = GSIMapEnumeratorForMap(m);
                    GSIMapNode      n1 = GSIMapEnumeratorNextNode(&e1);
                    
                    /*
                     * Nil object and nil name, so we step through all the maps
                     * keyed under the current name and remove all the objects
                     * that match the observer.
                     */
                    while (n1 != 0)
                    {
                        GSIMapNode  next = GSIMapEnumeratorNextNode(&e1);
                        
                        purgeMapNode(m, n1, observer);
                        n1 = next;
                    }
                }
                else
                {
                    GSIMapNode  n1;
                    
                    /*
                     * Nil name, but non-nil object - we locate the map for the
                     * specified object, and remove all the items that match
                     * the observer.
                     */
                    n1 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
                    if (n1 != 0)
                    {
                        purgeMapNode(m, n1, observer);
                    }
                }
                /*
                 * If we removed all the observations keyed under this name, we
                 * must remove the map table too.
                 */
                if (m->nodeCount == 0)
                {
                    mapFree(TABLE, m);
                    GSIMapRemoveKey(NAMED, (GSIMapKey)(id)thisName);
                }
            }
            
            /*
             * Now remove unnamed items
             */
            if (object == nil)
            {
                e0 = GSIMapEnumeratorForMap(NAMELESS);
                n0 = GSIMapEnumeratorNextNode(&e0);
                while (n0 != 0)
                {
                    GSIMapNode  next = GSIMapEnumeratorNextNode(&e0);
                    
                    purgeMapNode(NAMELESS, n0, observer);
                    n0 = next;
                }
            }
            else
            {
                n0 = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
                if (n0 != 0)
                {
                    purgeMapNode(NAMELESS, n0, observer);
                }
            }
        }
        else
        {
            GSIMapTable     m;
            GSIMapEnumerator_t  e0;
            GSIMapNode      n0;
            
            /*
             * Locate the map table for this name.
             */
            n0 = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
            if (n0 == 0)
            {
                unlockNCTable(TABLE);
                return;     /* Nothing to do.   */
            }
            m = (GSIMapTable)n0->value.ptr;
            
            if (object == nil)
            {
                e0 = GSIMapEnumeratorForMap(m);
                n0 = GSIMapEnumeratorNextNode(&e0);
                
                while (n0 != 0)
                {
                    GSIMapNode  next = GSIMapEnumeratorNextNode(&e0);
                    
                    purgeMapNode(m, n0, observer);
                    n0 = next;
                }
            }
            else
            {
                n0 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
                if (n0 != 0)
                {
                    purgeMapNode(m, n0, observer);
                }
            }
            if (m->nodeCount == 0)
            {
                mapFree(TABLE, m);
                GSIMapRemoveKey(NAMED, (GSIMapKey)((id)name));
            }
        }
        unlockNCTable(TABLE);
    }
    

    大概的流程就是如下图所示:


    图片.png

    我们调用removeObserver才会将观察者从通知管理中心的mapTable中移除,不移除的话,则Observation还在,当有符合的通知出发,就会发送给Observation,如果observer已经释放了,那么就访问了未知的内容,导致异常;当然iOS9之后已经不需要担心这个问题,系统已经帮我们处理了

    postNotification

    - (void) postNotificationName: (NSString*)name
                           object: (id)object
                         userInfo: (NSDictionary*)info
    {
        GSNotification  *notification;
        
        notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
        notification->_name = [name copyWithZone: [self zone]];
        notification->_object = [object retain];
        notification->_info = [info retain];
        [self _postAndRelease: notification];
    }
    
    /**
     * Private method to perform the actual posting of a notification.
     * Release the notification before returning, or before we raise
     * any exception ... to avoid leaks.
     */
    - (void) _postAndRelease: (NSNotification*)notification
    {
        Observation *o;
        unsigned    count;
        NSString    *name = [notification name];
        id      object;
        GSIMapNode  n;
        GSIMapTable m;
        GSIArrayItem    i[64];
        GSIArray_t  b;
        GSIArray    a = &b;
    #if GS_WITH_GC
        NSGarbageCollector  *collector = [NSGarbageCollector defaultCollector];
    #endif
        
        if (name == nil)
        {
            RELEASE(notification);
            [NSException raise: NSInvalidArgumentException
                        format: @"Tried to post a notification with no name."];
        }
        object = [notification object];
        if (object != nil)
        {
            object = CHEATGC(object);
        }
        
        /*
         * Lock the table of observations while we traverse it.
         *
         * The table of observations contains weak pointers which are zeroed when
         * the observers get garbage collected.  So to avoid consistency problems
         * we disable gc while we copy all the observations we are interested in.
         * We use scanned memory in the array in the case where there are more
         * than the 64 observers we allowed room for on the stack.
         */
    #if GS_WITH_GC
        GSIArrayInitWithZoneAndStaticCapacity(a, (NSZone*)1, 64, i);
        [collector disable];
    #else
        GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);
    #endif
        lockNCTable(TABLE);
        
        /*
         * Find all the observers that specified neither NAME nor OBJECT.
         */
        for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next)
        {
            GSIArrayAddItem(a, (GSIArrayItem)o);
        }
        
        /*
         * Find the observers that specified OBJECT, but didn't specify NAME.
         */
        if (object)
        {
            n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
            if (n != 0)
            {
                o = purgeCollectedFromMapNode(NAMELESS, n);
                while (o != ENDOBS)
                {
                    GSIArrayAddItem(a, (GSIArrayItem)o);
                    o = o->next;
                }
            }
        }
        
        /*
         * Find the observers of NAME, except those observers with a non-nil OBJECT
         * that doesn't match the notification's OBJECT).
         */
        if (name)
        {
            n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
            if (n)
            {
                m = (GSIMapTable)n->value.ptr;
            }
            else
            {
                m = 0;
            }
            if (m != 0)
            {
                /*
                 * First, observers with a matching object.
                 */
                n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
                if (n != 0)
                {
                    o = purgeCollectedFromMapNode(m, n);
                    while (o != ENDOBS)
                    {
                        GSIArrayAddItem(a, (GSIArrayItem)o);
                        o = o->next;
                    }
                }
                
                if (object != nil)
                {
                    /*
                     * Now observers with a nil object.
                     */
                    n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
                    if (n != 0)
                    {
                        o = purgeCollectedFromMapNode(m, n);
                        while (o != ENDOBS)
                        {
                            GSIArrayAddItem(a, (GSIArrayItem)o);
                            o = o->next;
                        }
                    }
                }
            }
        }
        
        /*
         * Finished with the table ... we can unlock it and re-enable garbage
         * collection, safe in the knowledge that the observers we will be
         * notifying won't get collected prematurely.
         */
        unlockNCTable(TABLE);
    #if GS_WITH_GC
        [collector enable];
    #endif
        
        /*
         * Now send all the notifications.
         */
        count = GSIArrayCount(a);
        while (count-- > 0)
        {
            o = GSIArrayItemAtIndex(a, count).ext;
            if (o->next != 0)
            {
                NS_DURING
                {
                    (*o->method)(o->observer, o->selector, notification);
                }
                NS_HANDLER
                {
                    NSLog(@"Problem posting notification: %@", localException);
                }
                NS_ENDHANDLER
            }
        }
        lockNCTable(TABLE);
        GSIArrayEmpty(a);
        unlockNCTable(TABLE);
        
        RELEASE(notification);
    }
    
    

    发送通知的大致流程:

    1. 收集需要发送通知的observations,添加到数组中
    • 收集没有设置name和object的observation
    • 收集observation的object跟通知的object匹配,但是没有设置name的observation
    • 收集observation的name跟通知的name匹配的,包含没有设置object或者object匹配的observation【先添加object匹配的,再添加object为nil的observation】
    1. 遍历发送通知【直接调用imp去执行函数调用】
    • (*o->method)(o->observer, o->selector, notification);
    1. 释放notification对象

    相关文章

      网友评论

          本文标题:NSNotificationCenter源码学习

          本文链接:https://www.haomeiwen.com/subject/zaeeuctx.html