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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章