iOS开发 - NSHashTable与Runloop所造成的强引用问题

概述

首先,我来描述一下这个问题,NSHashTable 是OC中用于弱引用对象的NSMutableSet 类型,在项目使用中,我们发现调用其 allObjects 方法会造成强引用关系,导致对象不会释放。具体情况如下:

我们监听了主线程的 Runloop 并在 kCFRunLoopBeforeWaiting | kCFRunLoopExit 时触发,由于我们需要统计 UITableViewCell 的信息,所以我们将该 Observerorder 设置为了 INT_MAX ,以保证在系统注册的 CA Transaction 回调之后

		static CFRunLoopObserverRef observer;
        
        if (observer) {
            return;
        }
        
        CFRunLoopRef runLoop = CFRunLoopGetCurrent();
        CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
                                    kCFRunLoopExit);          // before exiting a runloop run
        observer = CFRunLoopObserverCreateWithHandler(NULL,        // allocator
                                                      activities,  // activities
                                                      YES,         // repeats
                                                      INT_MAX,     // order after CA transaction commits
                                                      ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
                                                                   [self impTrack];   
                                                      });
        
        CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
        
        CFRelease(observer);

impTrack 方法中我们会将别处收集的 UITableViewCell 进行处理

- (void)impTrack
{
    //逻辑判断 这里省略。。。
    
    [self.sourceTable.allObjects enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        // do your logic
    }];
}

这里调用了 allObjects ,可以从源码中看出这是一个 NSArray 类型

@property (readonly, copy) NSArray<ObjectType> *allObjects;   

一般来说 allObjects 会持有 UITableViewCell 强引用,但由 autoreleasepool 自动释放之后,就不存在强引用问题。

问题:是在UITableView不滑动时,UITableViewCell可以正常释放,在UITableView滑动之后,UITableViewCell无法释放。

demo 链接: https://pan.baidu.com/s/1QlL3v5lMu4XzXIhGAbI6Sg 密码: uo2o

Runloop Observer的问题

		static CFRunLoopObserverRef observer;
        
        if (observer) {
            return;
        }
        
        CFRunLoopRef runLoop = CFRunLoopGetCurrent();
        CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
                                    kCFRunLoopExit);          // before exiting a runloop run
        observer = CFRunLoopObserverCreateWithHandler(NULL,        // allocator
                                                      activities,  // activities
                                                      YES,         // repeats
                                                      INT_MAX,     // order after CA transaction commits
                                                      ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
                                                                   [self impTrack];   
                                                      });
        
        CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
        
        CFRelease(observer);

AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop()来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

界面更新

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

所以我们需要设定 order 值大于这个回调的 order 值,取了 INT_MAX


这里的问题在于,将 order 设置为了 INT_MAX,这与 autoreleasepool_wrapRunLoopWithAutoreleasePoolHandler 的优先级设置相同。

我们通过断点打印出各个 Observer 对象

(lldb) po [NSRunLoop currentRunLoop]

observers = (
    "<CFRunLoopObserver 0x6000031280a0 [0x7fff8062d610]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x600000e51200 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa220009038>\n)}}",
    "<CFRunLoopObserver 0x60000312ca00 [0x7fff8062d610]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff48810abe), context = <CFRunLoopObserver context 0x600002b28620>}",
    "<CFRunLoopObserver 0x6000031281e0 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff48cb5ad3), context = <CFRunLoopObserver context 0x7fa21ea04080>}",
    "<CFRunLoopObserver 0x600003130280 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x7fff2b454f32), context = <CFRunLoopObserver context 0x0>}",
    "<CFRunLoopObserver 0x600003128000 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff48cb5b3c), context = <CFRunLoopObserver context 0x7fa21ea04080>}",
    "<CFRunLoopObserver 0x600003128140 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x600000e51200 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa220009038>\n)}}",
    "<CFRunLoopObserver 0x600003134c80 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _runLoopObserverWithBlockContext (0x7fff23d9e440), context = <CFRunLoopObserver context 0x600000e49fb0>}"
)

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

0xa0 = 10100000 = kCFRunLoopBeforeWaiting + kCFRunLoopExit

order = 2147483647 的优先级有两个,一个是我们注册的 _runLoopObserverWithBlockContext ,另一个就是系统 autoreleasepool_wrapRunLoopWithAutoreleasePoolHandler

问题分析

根据 Runloop源码CFRunLoopAddObserverorder 的排序,最终我们的回调会在系统 autoreleasepool 的回调之后

void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) {
    CHECK_FOR_FORK();
    CFRunLoopModeRef rlm;
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rlo) || (NULL != rlo->_runLoop && rlo->_runLoop != rl)) return;
    __CFRunLoopLock(rl);
    if (modeName == kCFRunLoopCommonModes) {
	CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
	if (NULL == rl->_commonModeItems) {
	    rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
	}
	CFSetAddValue(rl->_commonModeItems, rlo);
	if (NULL != set) {
	    CFTypeRef context[2] = {rl, rlo};
	    /* add new item to all common-modes */
	    CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
	    CFRelease(set);
	}
    } else {
	rlm = __CFRunLoopFindMode(rl, modeName, true);
	if (NULL != rlm && NULL == rlm->_observers) {
	    rlm->_observers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
	}
	if (NULL != rlm && !CFArrayContainsValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo)) {
            Boolean inserted = false;
            for (CFIndex idx = CFArrayGetCount(rlm->_observers); idx--; ) {
                CFRunLoopObserverRef obs = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
                if (obs->_order <= rlo->_order) {
                //这里决定当order相等时,自定义回调会在系统回调之后
                    CFArrayInsertValueAtIndex(rlm->_observers, idx + 1, rlo);
                    inserted = true;
                    break;
                }
            }
            if (!inserted) {
	        CFArrayInsertValueAtIndex(rlm->_observers, 0, rlo);
            }
	    rlm->_observerMask |= rlo->_activities;
	    __CFRunLoopObserverSchedule(rlo, rl, rlm);
	}
        if (NULL != rlm) {
	    __CFRunLoopModeUnlock(rlm);
	}
    }
    __CFRunLoopUnlock(rl);
}

那么我们创建对象,在一个 Runloop 循环时,是无法释放掉,但是可以在下一次 Runloop 循环时,释放上次创建的对象。

在这里插入图片描述
这样不断循环,当某一时刻,allObjects 获得对象数量为0时,就不会再强引用,所以cell能够释放。

kCFRunloopExit 主线程Runloop在不切换mode情况下并不会调用

  • 那为什么滑动 UITableView 之后,cell 就无法释放了?
    滑动 UITableView 之后,runloop会切换mode,由kCFRunLoopDefaultMode切换为UITrackingRunLoopMode,根据源码,切换mode实际是调用CFRunLoopRunSpecific 这个函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
	Boolean did = false;
	if (currentMode) __CFRunLoopModeUnlock(currentMode);
	__CFRunLoopUnlock(rl);
	return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
	rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

使用 lldb 来断点这个函数,当滑动 UITableView

* thread #6, name = 'com.apple.uikit.eventfetch-thread', stop reason = breakpoint 4.1
  * frame #0: 0x00007fff23d9a7b0 CoreFoundation`CFRunLoopRunSpecific
    frame #1: 0x00007fff25939c71 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 211
    frame #2: 0x00007fff25939ee0 Foundation`-[NSRunLoop(NSRunLoop) runUntilDate:] + 72
    frame #3: 0x00007fff48d39bfb UIKitCore`-[UIEventFetcher threadMain] + 138
    frame #4: 0x00007fff2594f9eb Foundation`__NSThread__start__ + 1047
    frame #5: 0x00007fff51c0c109 libsystem_pthread.dylib`_pthread_start + 148
    frame #6: 0x00007fff51c07b8b libsystem_pthread.dylib`thread_start + 15
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
  * frame #0: 0x00007fff23d9a7b0 CoreFoundation`CFRunLoopRunSpecific
    frame #1: 0x00007fff38ba6c1a GraphicsServices`GSEventRunModal + 139
    frame #2: 0x00007fff48c8b9ec UIKitCore`UIApplicationMain + 1605
    frame #3: 0x000000010506f8fa TestIsland`main(argc=1, argv=0x00007ffeeab8fd00) at main.m:18:12
    frame #4: 0x00007fff51a231fd libdyld.dylib`start + 1

确实会调用该方法,进行切换 modeRunloop切换时,会将现场的mode下的Runloop退出,然后开启一个新的modeRunloop循环在这里插入图片描述
问题就在于切换时,由于我们方法在 Autoreleasepool 执行 pop push 之后,会有一次没有加入自动释放池,导致变量无法释放。

这也就是为什么手动添加 @autoreleasepool{} 可以防止这个问题,因为 @autoreleasepool{} 原理是在大括号开始添加 push 以及结尾添加 pop。

最终修改将 order 从 INT_MAX 修改为 INT_MAX-1,以保证我们方法包括在自动释放池 push pop 操作中。

扩展

我们查看 GNU 源码中 NSConcreteHashTableallObjects 作为参考:

- (NSArray*) allObjects
{
  NSHashEnumerator	enumerator;
  NSUInteger		index;
  NSArray		*a;
  GS_BEGINITEMBUF(objects, nodeCount, id);

  enumerator = NSEnumerateHashTable(self);
  index = 0;
  while (index < nodeCount
    && (objects[index] = NSNextHashEnumeratorItem(&enumerator)) != nil)
    {
      index++;
    }
  NSEndHashTableEnumeration(&enumerator);
  a = [[[NSArray alloc] initWithObjects: objects count: index] autorelease];
  GS_ENDITEMBUF();
  return a;
}

或许你也有这样的疑问,按道理说,autorelease 方法执行时没有page时,会创建page,插入哨兵对象 POOL_BOUNDARY,那么下次push就不会创建一个新的page,那么对象不就可以正常释放的?

由于@autoreleasepool{} 最终执行 objc_autoreleasePoolPush 以及 objc_autoreleasePoolPop,这里分析其具体做了啥。

来看下 objc_autoreleasePoolPush 最终调用 autoreleaseNoPage(POOL_BOUNDARY) 方法

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // "No page" could mean no pool has been pushed
    // or an empty placeholder pool has been pushed and has no contents yet
    ASSERT(!hotPage());

    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) {
        // We are pushing a second pool over the empty placeholder pool
        // or pushing the first object into the empty placeholder pool.
        // Before doing that, push a pool boundary on behalf of the pool 
        // that is currently represented by the empty placeholder.
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     objc_thread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // Install and return the empty pool placeholder.
        return setEmptyPoolPlaceholder();
    }

    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    return page->add(obj);
}

那么就会走 setEmptyPoolPlaceholder ,下一次遇到对象创建,或者方法时,pushExtraBoundary 就会为YES,添加 POOL_BOUNDARY 然后添加对应的 obj

即push操作并不会插入一个 POOL_BOUNDARY ,而是标记了一个 Placeholder,当下次有对象加入自动释放池时,再进行插入 POOL_BOUNDARY ,然后再插入对象 obj

意味着每次runloop执行kRunloopEntry时,进行push操作,会将标红部分创建的obj独立到 POOL_BOUNDARY 区间之外,无法释放。

在方法中打印自动释放池信息

OBJC_EXTERN void
_objc_autoreleasePoolPrint(void);

然后在需要打印处调用 _objc_autoreleasePoolPrint();

日志输出如下(下面是hotpage):


objc[36361]: AUTORELEASE POOLS for thread 0x10f55cdc0
objc[36361]: 61 releases pending.
objc[36361]: [0x7fbcf880c000]  ................  PAGE  (hot) (cold)
objc[36361]: [0x7fbcf880c038]    0x600001cde9d0  __NSArrayM
objc[36361]: [0x7fbcf880c040]    0x7fbcf752dd90  TestCell
objc[36361]: [0x7fbcf880c048]    0x7fbcf7432d70  TestCell
objc[36361]: [0x7fbcf880c050]    0x7fbcf7538da0  TestCell
objc[36361]: [0x7fbcf880c058]    0x7fbcf753ee10  TestCell
objc[36361]: [0x7fbcf880c060]    0x7fbcf742bed0  TestCell
objc[36361]: [0x7fbcf880c068]    0x7fbcf753d840  TestCell
objc[36361]: [0x7fbcf880c070]    0x7fbcf75417a0  TestCell
objc[36361]: [0x7fbcf880c078]    0x7fbcf752ff70  TestCell
objc[36361]: [0x7fbcf880c080]    0x7fbcf74378f0  TestCell
objc[36361]: [0x7fbcf880c088]    0x7fbcf7435120  TestCell
objc[36361]: [0x7fbcf880c090]    0x7fbcf75324b0  TestCell
objc[36361]: [0x7fbcf880c098]    0x7fbcf753b320  TestCell
objc[36361]: [0x7fbcf880c0a0]    0x7fbcf7529f30  TestCell
objc[36361]: [0x7fbcf880c0a8]    0x7fbcf7536020  TestCell
objc[36361]: [0x7fbcf880c0b0]    0x7fbcf7533ab0  TestCell
objc[36361]: [0x7fbcf880c0b8]    0x7fbcf7439ed0  TestCell
objc[36361]: [0x7fbcf880c0c0]    0x7fbcf7619220  TestCell
objc[36361]: [0x7fbcf880c0c8]    0x7fbcf742e690  TestCell
objc[36361]: [0x7fbcf880c0d0]    0x7fbcf76145b0  TestCell
objc[36361]: [0x7fbcf880c0d8]    0x600001cdea30  __NSArrayM
objc[36361]: [0x7fbcf880c0e0]    0x7fbcf752dd90  TestCell
objc[36361]: [0x7fbcf880c0e8]    0x7fbcf7432d70  TestCell
objc[36361]: [0x7fbcf880c0f0]    0x7fbcf7538da0  TestCell
objc[36361]: [0x7fbcf880c0f8]    0x7fbcf753ee10  TestCell
objc[36361]: [0x7fbcf880c100]    0x7fbcf742bed0  TestCell
objc[36361]: [0x7fbcf880c108]    0x7fbcf753d840  TestCell
objc[36361]: [0x7fbcf880c110]    0x7fbcf75417a0  TestCell
objc[36361]: [0x7fbcf880c118]    0x7fbcf752ff70  TestCell
objc[36361]: [0x7fbcf880c120]    0x7fbcf74378f0  TestCell
objc[36361]: [0x7fbcf880c128]    0x7fbcf7435120  TestCell
objc[36361]: [0x7fbcf880c130]    0x7fbcf75324b0  TestCell
objc[36361]: [0x7fbcf880c138]    0x7fbcf753b320  TestCell
objc[36361]: [0x7fbcf880c140]    0x7fbcf7529f30  TestCell
objc[36361]: [0x7fbcf880c148]    0x7fbcf7536020  TestCell
objc[36361]: [0x7fbcf880c150]    0x7fbcf7533ab0  TestCell
objc[36361]: [0x7fbcf880c158]    0x7fbcf7439ed0  TestCell
objc[36361]: [0x7fbcf880c160]    0x7fbcf7619220  TestCell
objc[36361]: [0x7fbcf880c168]    0x7fbcf742e690  TestCell
objc[36361]: [0x7fbcf880c170]    0x7fbcf76145b0  TestCell
objc[36361]: [0x7fbcf880c178]  ################  POOL 0x7fbcf880c178
objc[36361]: [0x7fbcf880c180]    0x600001cf0fc0  __NSArrayM
objc[36361]: [0x7fbcf880c188]    0x7fbcf752dd90  TestCell
objc[36361]: [0x7fbcf880c190]    0x7fbcf7432d70  TestCell
objc[36361]: [0x7fbcf880c198]    0x7fbcf7538da0  TestCell
objc[36361]: [0x7fbcf880c1a0]    0x7fbcf753ee10  TestCell
objc[36361]: [0x7fbcf880c1a8]    0x7fbcf742bed0  TestCell
objc[36361]: [0x7fbcf880c1b0]    0x7fbcf753d840  TestCell
objc[36361]: [0x7fbcf880c1b8]    0x7fbcf75417a0  TestCell
objc[36361]: [0x7fbcf880c1c0]    0x7fbcf752ff70  TestCell
objc[36361]: [0x7fbcf880c1c8]    0x7fbcf74378f0  TestCell
objc[36361]: [0x7fbcf880c1d0]    0x7fbcf7435120  TestCell
objc[36361]: [0x7fbcf880c1d8]    0x7fbcf75324b0  TestCell
objc[36361]: [0x7fbcf880c1e0]    0x7fbcf753b320  TestCell
objc[36361]: [0x7fbcf880c1e8]    0x7fbcf7529f30  TestCell
objc[36361]: [0x7fbcf880c1f0]    0x7fbcf7536020  TestCell
objc[36361]: [0x7fbcf880c1f8]    0x7fbcf7533ab0  TestCell
objc[36361]: [0x7fbcf880c200]    0x7fbcf7439ed0  TestCell
objc[36361]: [0x7fbcf880c208]    0x7fbcf7619220  TestCell
objc[36361]: [0x7fbcf880c210]    0x7fbcf742e690  TestCell
objc[36361]: [0x7fbcf880c218]    0x7fbcf76145b0  TestCell
objc[36361]: ##############

什么鬼?不仅仅是__NSArrayM ,连cell都加入了自动释放池。和预想的不一样,我们模拟一下代码,输出日志,以防其他地方影响:

OBJC_EXTERN void
_objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSHashTable *hashtb = [NSHashTable weakObjectsHashTable];
        NSObject *objc = [[NSObject alloc] init];
        NSObject *objc2 = [[NSObject alloc] init];
        [hashtb addObject:objc];
        [hashtb addObject:objc2];
        @autoreleasepool {
            hashtb.allObjects;
            _objc_autoreleasePoolPrint();
        }
        
    }
    return 0;
}

输出日志为:

objc[36295]: ##############
objc[36295]: AUTORELEASE POOLS for thread 0x1000d2dc0
objc[36295]: 6 releases pending.
objc[36295]: [0x102017000]  ................  PAGE  (hot) (cold)
objc[36295]: [0x102017038]  ################  POOL 0x102017038
objc[36295]: [0x102017040]       0x100717440  NSConcreteHashTable
objc[36295]: [0x102017048]  ################  POOL 0x102017048
objc[36295]: [0x102017050]       0x10071b820  __NSArrayM
objc[36295]: [0x102017058]       0x10070dd30  NSObject
objc[36295]: [0x102017060]       0x10070df80  NSObject
objc[36295]: ##############
Program ended with exit code: 0

说明其allObject方法是会造成里面元素加入自动释放池。而且这份自动释放池信息和之前的对比,可以明显发现,少了一个 POOL_BOUNDARY ,导致里面的元素无法正常释放。

demo 链接: https://pan.baidu.com/s/1QlL3v5lMu4XzXIhGAbI6Sg 密码: uo2o

#include #include #include using namespace std; #define NULL 0 unsigned int key; //用来输入/输出文件流类 unsigned int key2; //key和key2分别是用做了电话号码和姓名的关键字 int *p; struct node //新建节点(用户姓名、地址、电话号码、指向下一个结点的指针 ) { char name[8],address[20]; char num[11]; node * next; }; typedef node* pnode; typedef node* mingzi; //声明了名字和电话两个指针 node **phone; node **nam; node *a; void hash(char num[11]) //以电话号码为关键字建立哈希函数 { int i = 3; key=(int)num[2]; while(num[i]!=NULL) { key+=(int)num[i]; i++; } key=key%20; } void hash2(char name[8]) //姓名为关键字建立哈希函数 { int i = 1; key2=(int)name[0]; while(name[i]!=NULL) { key2+=(int)name[i]; i++; } key2=key2%20; } //强制类型转换,将用户名的每一个字母的ASCLL码值相加并且除以20后的余数 node* input() //输入节点信息 ,建立结点,并将结点的next指针指空 { node *temp; temp = new node; temp->next=NULL; cout<<"输入姓名:"<>temp->name; cout<<"输入地址:"<>temp->address; cout<<"输入电话:"<>temp->num; return temp; } //对于指针类型返回的是地址 int apend() //添加节点 { node *newphone; node *newname; newphone=input(); newname=newphone; newphone->next=NULL; newname->next=NULL; hash(newphone->num); //利用哈希函数计算出对应关键字的存储地址 hash2(newname->name); newphone->next = phone[key]->next; //利用电话号码为关键字插入 phone[key]->next=newphone; //是采用链地址法,拉链法处理冲突的散列表结构 newname->next = nam[key2]->next; //利用用户名为关键字插入 nam[key2]->next=newname; return 0; } void create() //新建节点 { int i; phone=new pnode[20]; //动态创建对象数组,C++课本P188页 for(i=0;inext=NULL; } } void create2() //新建节点 { int i; nam=new mingzi[20]; for(i=0;inext=NULL; } } void list() //显示列表 { int i; node *p; for(i=0;inext; while(p) { cout<name<<'_'<address<<'_'<num<next; } } } void list2() //显示列表 { int i; node *p; for(i=0;inext; while(p) { cout<name<<'_'<address<<'_'<num<next; } } } void find(char num[11]) //在以电话号码为关键字的哈希表中查找用户信息 { hash(num); node *q=phone[key]->next; while(q!= NULL) { if(strcmp(num,q->num)==0) break; q=q->next; } if(q) cout<name<<"_" <address<<"_"<num<<endl; else cout<<"无此记录"<next; while(q!= NULL) { if(strcmp(name,q->name)==0) break; q=q->next; } if(q) cout<name<<"_" <address<<"_"<num<<endl; else cout<<"无此记录"<<endl; } void save() //保存用户信息 { int i; node *p; for(i=0;inext; while(p) { fstream iiout("out.txt", ios::out); //创建一个文件流对象:iiout iiout<name<<"_"<address<<"_"<num<next; } } } void menu() //菜单 { cout<<" 哈希表通讯录"<<endl; cout<<" 0.添加记录"<<endl; cout<<" 2.姓名散列"<<endl; cout<<" 3.查找记录"<<endl; cout<<" 4.号码散列"<<endl; cout<<" 5.清空记录"<<endl; cout<<" 6.保存记录"<<endl; cout<<" 7.退出系统"<>sel; if(sel==3) { cout<<"8姓名查询" <<endl;cout<<"9号码查询"<>b; if(b==9) {cout<<"请输入电话号码:"<>num; cout<<"输出查找的信息:"<<endl; find(num); } else {cout<<"请输入姓名:"<>name; cout<<"输出查找的信息:"<<endl; find2(name);}} if(sel==2) {cout<<"姓名散列结果:"<<endl; list2();} if(sel==0) {cout<<"请输入要添加的内容:"<<endl; apend();} if(sel==4) {cout<<"号码散列结果:"<<endl; list(); } if(sel==5) {cout<<"列表已清空:"<<endl; create();create2();} if(sel==6) { cout<<"通信录已保存:"<<endl; save();} if(sel==7) return 0; } return 0; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值