首先,我来描述一下这个问题,NSHashTable
是OC中用于弱引用对象的NSMutableSet
类型,在项目使用中,我们发现调用其 allObjects
方法会造成强引用关系,导致对象不会释放。具体情况如下:
我们监听了主线程的 Runloop
并在 kCFRunLoopBeforeWaiting | kCFRunLoopExit
时触发,由于我们需要统计 UITableViewCell
的信息,所以我们将该 Observer
的 order
设置为了 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
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);
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 = (
"{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7fa220009038>\n)}}" ,
"{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff48810abe), context = }" ,
"{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff48cb5ad3), context = }" ,
"{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x7fff2b454f32), context = }" ,
"{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff48cb5b3c), context = }" ,
"{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7fa220009038>\n)}}" ,
"{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _runLoopObserverWithBlockContext (0x7fff23d9e440), context = }"
)
/* 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源码 中CFRunLoopAddObserver
对 order
的排序,最终我们的回调会在系统 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
确实会调用该方法,进行切换 mode
,Runloop
切换时,会将现场的mode
下的Runloop
退出,然后开启一个新的mode
的Runloop
循环
问题就在于切换时,由于我们方法在 Autoreleasepool
执行 pop push
之后,会有一次没有加入自动释放池,导致变量无法释放。
这也就是为什么手动添加 @autoreleasepool{} 可以防止这个问题,因为 @autoreleasepool{} 原理是在大括号开始添加 push 以及结尾添加 pop。
最终修改将 order 从 INT_MAX 修改为 INT_MAX-1,以保证我们方法包括在自动释放池 push pop 操作中。
我们查看 GNU 源码中 NSConcreteHashTable
的 allObjects
作为参考:
- (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