之前在看重复的NSTimer在加到runloop之后,发现由于runloop会强持有observer,导致在dealloc中去invalidate不会起作用的问题;联想到通知也是addObserver,我们一般都是在dealloc中去removeObserver,而observer的dealloc可以正常调用,就想了解一下这其中的不同
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中
具体的过程可参照下图理解
由于通知内部存储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赋值给实例
GSObservation
和Observation
结构体的定义如下:
/*
* 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
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);
}
大概的流程就是如下图所示:
我们调用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);
}
发送通知的大致流程:
- 收集需要发送通知的observations,添加到数组中
- 收集没有设置name和object的observation
- 收集observation的object跟通知的object匹配,但是没有设置name的observation
- 收集observation的name跟通知的name匹配的,包含没有设置object或者object匹配的observation【先添加object匹配的,再添加object为nil的observation】
- 遍历发送通知【直接调用imp去执行函数调用】
- (*o->method)(o->observer, o->selector, notification);
- 释放notification对象
至此已经解答了前面的疑问~
那么问题来了:为什么通知中心内部是弱引用,而runloop中添加observer却是强持有了?你知道吗