找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1222

积分

0

好友

154

主题
发表于 2025-12-24 02:20:49 | 查看: 62| 回复: 0

内存管理概览

内存布局

内存布局

  • stack(栈):方法调用、局部变量等  
  • heap(堆):通过 alloc/new/copy/mutableCopy 等分配的对象  
  • bss:未初始化的全局变量以及静态变量等  
  • data:已初始化的全局变量以及静态变量等  
  • text:程序代码区  

内存管理方案

问:iOS 系统怎样对内存进行管理?

根据不同场景,iOS Runtime 会采用不同的策略,常见可归纳为三类:

  • Tagged Pointer:例如 NSNumber 等小对象直接编码在指针里  
  • NONPOINTER_ISA:64 位下 isa 的某些 bit 存储额外信息,并非纯指针  
  • 散列表(SideTable):引用计数表、weak 弱引用表(工程中最常遇到的那套)

在学习 Runtime 与内存细节时,可以结合 iOS 相关资料体系化梳理,更容易把各块知识串起来。


NONPOINTER_ISA

NONPOINTER_ISA 结构

NONPOINTER_ISA 位域示意
NONPOINTER_ISA 位域示意 2


散列表方式(SideTable)

散列表结构
散列表结构 2

struct SideTable
{
    spinlock_t slock;// 保证原子操作的自旋锁
    RefcountMap refcnts;// 引用计数器存储地,是一个哈希 map
    weak_table_t weak_table;// 弱引用表,也是哈希 map 存储
}

问:SideTables 存储在哪?SideTable 存储在哪?

在源码中全局搜索 SideTables,能看到类似 “initialize SideTables” 的注释。

We cannot use a C++ static initializer to initialize SideTables because libc calls us before our C++ initializers run. We also don't want a global pointer to this struct because of the extra indirection.
Do it the hard way.

我们不能使用C++静态初始化器初始化SideTables,因为libc在我们的C++初始化器运行之前调用我们。
我们也不希望用一个全局的指针指向这个结构体,因为指针是间接访问。

核心信息就是:

  • 不能使用 C++ 静态初始化来初始化 SideTables
  • 也不希望用一个全局指针指向它(避免额外间接访问)

相关实现如下:

static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

这里的 StripedMap 是一个模板类型,头部注释一般会说明:

StripedMap< T > is a map of void* -> T

也就是:

  • StripedMap 是一个 void* 作为 key,T 作为 value 的 map
  • 因此 SideTables 本质是全局的 hash 表(按对象地址分流到不同桶/表)

参考链接(原文保留):

问:为什么不是一个 SideTable,而是多个?

多表与锁竞争

如果只有一个 SideTable,所有对象都挤在同一张表里:

  • 对任意对象做 retain/release/weak 操作都要对同一把锁加锁
  • 结果就是:锁竞争严重,吞吐下降

因此系统采用 分离锁(Striped/分段) 思路:

分离锁示意

把一张大表拆成多张表(示意为 8 张)。如果对象 A 落在表 A、对象 B 落在表 B,那么它们的操作可以并发进行,整体效率更好。

问:如何实现快速分流?

题意就是:通过对象指针,如何快速定位到它属于哪张 SideTable

快速分流

本质是利用对象地址(指针)做 hash / 掩码运算,映射到固定数量的 stripe(不同 SideTable)。


SideTable 的数据结构

SideTable 主要由三部分构成:

  1. spinlock_t slock:自旋锁,保证原子操作  
  2. RefcountMap refcnts:引用计数表(哈希 map)  
  3. weak_table_t weak_table:弱引用表(哈希结构)  

spinlock_t

自旋锁特点是“忙等”,适用于临界区非常短、加锁开销要极低的场景(当然也要注意长时间持锁会浪费 CPU)。

RefcountMap(引用计数表)

RefcountMap

  • refcnts 是一个哈希 map  
  • 插入与查找通常走同一套 hash 流程,整体效率高  

size_t 内容示意

关键点:

  • key:对象地址  
  • valuesize_t(包含引用计数及其他标志位)  
  • size_t 右移两位,得到引用计数值(低位用于存放标记位)  

weak_table_t(弱引用表)

weak_table_t

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

从注释可以读出:

  • key:对象 id(可理解为对象地址)  
  • valueweak_entry_t  

补充一个常见理解点:

  • weak_entry_t *weak_entries; 表示一个“结构体数组”(数组名 weak_entries,元素类型 weak_entry_t

一个对象可能被多个 __weak 指针指向,是一对多关系:

__weak NSObject *object = [[NSObject alloc] init];
__weak NSObject *object1 = object;
__weak NSObject *object2 = object;
  • key 是 [[NSObject alloc] init] 的地址(也就是 object 里存的地址)  
  • value 是 weak_entry_t,内部会记录 object/object1/object2 这些弱引用指针的位置(本质上是地址集合)

题外话:我们口头说的 “person 对象” 到底是什么?

YZPerson *person = [[YZPerson alloc] init];

更严谨地说:

  • person指针变量(栈上的变量),里面存的是地址值  
  • 真正的“对象实例”在堆上,是 [[YZPerson alloc] init] 返回的那块内存  
  • 所以日常表述里的“person 对象”,多半指的是等号右侧创建出来的实例,而不是指针变量本身  

MRC 和 ARC

MRC

手动引用计数相关方法:

  • alloc
  • retain
  • release
  • retainCount
  • autorelease
  • dealloc

注意:在 ARC 下,retain/release/retainCount/autorelease禁止显式调用,写了编译器会直接报错。

dealloc 的特殊性

  • MRC:需要显式 [super dealloc]  
  • ARC:允许重写 dealloc 来释放非 OC 资源(移除通知、释放 C 指针等),但禁止在内部调用 [super dealloc](编译器会自动处理)

autorelease 的替代方案

ARC 虽然禁止 [obj autorelease],但 autorelease 机制仍存在,主要通过:

  • @autoreleasepool 代码块  
  • __autoreleasing 修饰符  
  • 编译器对方法命名规则(Naming Conventions)的所有权推导  

MRC 中 autorelease 很常用,这点要特别留意。

ARC

ARC 是 LLVM(编译期)与 Runtime(运行期)协作的结果:

  • LLVM(编译期):静态分析代码,在合适位置插入 objc_retain/objc_release  
  • Runtime(运行期):处理 weak(置 nil)与 autorelease 的优化细节  

ARC 下的规则要点:

  • 禁止调用 retain/release/retainCount/dealloc  
  • 允许重写 dealloc,但禁止 [super dealloc]  
  • 新增 weak/strong 等属性关键字  

引用计数管理

下面按 alloc/retain/release/retainCount/dealloc 的路径梳理一次实现逻辑。

alloc 的实现

alloc 最终会走到 C 的 calloc,分配并清零内存;关键点是:并不会在此处把引用计数“显式设置为 1”

+ (id)alloc {
    return _objc_rootAlloc(self);
}

进入_objc_rootAlloc(self)
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

进入callAlloc
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());//C语言的calloc函数
if (!obj) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;

obj->initInstanceIsa(cls, dtor);里面有
initIsa(cls, true, hasCxxDtor);

进入initIsa
inline void
objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
    assert(!isTaggedPointer());

    if (!indexed) {
        isa.cls = cls;
    } else {
        assert(!DisableIndexedIsa);
        isa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.indexed is part of ISA_MAGIC_VALUE
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
    }
}

完成
在里面没有发现关于sidatable表的操作

这点容易误解:很多人长期以为 alloc 会“+1”,但从这条路径看,并没有直接操作 SideTable

retain 的实现

retain 流程

- (id)retain {
    return _objc_rootRetain(self);
}

进入
NEVER_INLINE id
_objc_rootRetain(id obj)
{
    ASSERT(obj);

    return obj->rootRetain();
}

进入
inline id
objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

进入
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
        //#define SIDE_TABLE_RC_ONE            (1UL<<2)左移两位,是4(实际反应出来是1)
    }
    table.unlock();

    return (id)this;
}

三句核心操作:

  • SideTable& table = SideTables()[this];:定位对象所属 SideTable  
  • size_t& refcntStorage = table.refcnts[this];:获取该对象的 size_t 存储单元  
  • refcntStorage += SIDE_TABLE_RC_ONE;:引用计数 +1(以位移形式存放)  

问:retain 时系统如何查找引用计数?

可以理解为“两次 hash 查找”:

  1. SideTables 通过对象指针定位到对应 SideTable  
  2. SideTable.refcnts 中再通过对象指针定位到 size_t 存储位  

最终表现就是引用计数加一。

release 的实现

release 流程

- (oneway void)release {
    ((id)self)->rootRelease();
}

进入
ALWAYS_INLINE bool
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

进入
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    ......
    return sidetable_release(performDealloc);
}

进入sidetable_release
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    if (table.trylock()) {
        RefcountMap::iterator it = table.refcnts.find(this);
        if (it == table.refcnts.end()) {
            do_dealloc = true;
            table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
        } else if (it->second < SIDE_TABLE_DEALLOCATING) {
            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
            do_dealloc = true;
            it->second |= SIDE_TABLE_DEALLOCATING;
        } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
            it->second -= SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        if (do_dealloc  &&  performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return do_dealloc;
    }

    return sidetable_release_slow(table, performDealloc);
}

对应三句关键点:

  • SideTable& table = SideTables()[this];:找到 SideTable  
  • RefcountMap::iterator it = table.refcnts.find(this);:找到引用计数条目  
  • it->second -= SIDE_TABLE_RC_ONE;:引用计数 -1(未 pinned 时)  

retainCount 的实现

retainCount 流程

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

进入rootRetainCount()
inline uintptr_t
objc_object::rootRetainCount()
{
    assert(!UseGC);
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    if (bits.indexed) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

进入sidetable_retainCount()
uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}

关键步骤:

  • SideTable& table = SideTables()[this];:定位 SideTable  
  • size_t refcnt_result = 1;:先把返回值初始化为 1  
  • it = table.refcnts.find(this);:查找引用计数记录  
  • refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;:把 size_t 右移得到的计数累加回来  
  • return refcnt_result;:返回结果  

问:新创建对象,alloc 后引用计数是多少?

按这里的口径:

  • 新建对象“额外计数”为 0  
  • 调用 retainCount 时,内部以 refcnt_result = 1 起步,最终返回 1  

也就是说:alloc 后 retainCount 的观测值通常为 1,但其底层计数存储并不等同于“alloc 时写入 1”。


dealloc 的实现与副作用

dealloc

object_dispose() 实现示意:
object_dispose

objc_destructInstance()clearDeallocating() 相关:
objc_destructInstance
clearDeallocating

从这些流程可以看到:

  • dealloc 期间会触发 弱引用置 nil  
  • dealloc 期间会对 引用计数表记录进行清理/擦除  

问:关联对象是否需要在 dealloc 手动移除?

通过关联对象给类“加实例变量”时,一般不需要在 dealloc 里手动移除,因为在对象销毁路径中 Runtime 已包含关联对象的清理操作。


弱引用管理(weak)

weak 赋值与函数调用关系

理解要点:

  • __weak 并不是“特殊指针类型”,而是一种语义:编译器会把相关操作落到 objc_initWeak 等函数上  
  • 类似地,strong 的语义会对应到 retain/release 路径(可理解为 objc_initStrong 一类处理)  

objc_initWeak

id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
}

进入storeWeak
static id
storeWeak(id *location, objc_object *newObj)
{
    if (HaveNew) {
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
                                                      (id)newObj, location,
                                                      CrashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
}

weak 登记过程

  • 如果已存在对应 weak_entry_t,就把当前弱引用位置插入进去  
  • 如果不存在,则新建一个 weak_entry_t 并插入  

weak_entry_t 示意

问:weak 变量自动变 nil 是如何实现的?

对象销毁时会走清理逻辑:

void
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

weak_clear

结论

当对象 dealloc 后,内部会调用弱引用清理相关函数。该函数会根据对象指针定位到弱引用表,把对应的弱引用位置集合取出(数组/集合),再逐个遍历,将这些 weak 指针 全部置为 nil


自动释放池(AutoreleasePool)

自动释放池示意

问:array 在什么时候释放?(自动释放池与 dealloc 的关系)

在一次 RunLoop 即将结束时,会执行 AutoreleasePool::pop(),对池内对象统一 release

  • 自动释放池 pop
  • 对象引用计数到 0
  • 触发 dealloc
  • 对象真正释放

注意:先 release 使引用计数归零,才会进入 dealloc。不是先 dealloc 再把计数设置为 0。

⚠️:以上对工厂方法(例如 [NSMutableArray array])更典型;对 [[NSArray alloc] init] 这种情况不完全相同。


不是所有 array 都会等到 RunLoop 结束

在 ARC 下常见分两类:

情况 A:alloc/init 创建的局部变量(大多数场景)

- (void)test {
    NSArray *array = [[NSArray alloc] init];
    // ... 使用 array
} // 函数结束
  • 关键点:这种 array 多数情况下不会进入自动释放池  
  • 释放时机:编译器能分析出变量作用域,会在 } 前插入 objc_release(array)  
  • 结果:作用域结束时立即 dealloc,无需等待 RunLoop  

这是 ARC 的常见性能优化:减少 autorelease 的开销、降低内存峰值。

情况 B:工厂方法创建(且未被优化)

- (void)test {
    NSArray *array = [NSArray array]; // 传统的 autorelease 对象
}
  • 这种通常会进入 AutoreleasePool  
  • 一般在 RunLoop 即将休眠(BeforeWaiting)或退出(Exit)时,pool pop 才释放  

总结成一句:alloc/init 的局部变量通常直接 release;工厂方法对象通常延迟到 pool pop。


官方文档的“方法家族”(Method Families)

ARC 编译器会根据方法名推导所有权:

  1. Own-returning methodsalloc/new/copy/mutableCopy 开头  

    • 调用者默认拥有所有权(+1)  
    • 编译器会在作用域结束插入 release(-1),不依赖自动释放池  
  2. Non-own-returning methods:例如 [NSArray array] 等  

    • 调用者不拥有所有权  
    • 为保证返回对象不过早销毁,通常通过 AutoreleasePool 或 TLS 优化延长寿命  

这个差异对 内存峰值优化很关键:例如循环内大量临时对象,如果是工厂方法创建更容易“堆到 RunLoop 才释放”,就需要考虑 @autoreleasepool


特殊情况补充:weak 可能延长对象寿命

严格来说,“局部变量 alloc/init 不进池子”虽然很常见,但存在更复杂的特例,例如引入 __weak 指针时:

- (void)test {
    NSArray *arr = [[NSArray alloc] init];
    __weak NSArray *weakArr = arr; // 引入了 weak
}

此时编译器可能出于语义正确性与时序安全考虑,采用不同的延长寿命策略(包括可能走 autorelease 路径),避免在 weak 读写的中间窗口出现不可控状态。


题外话:数组变量销毁 vs 对象释放

当你问“array 什么时候释放”时,需要区分:

  • array 这个指针变量本身:在栈上,作用域结束时变量消失  
  • array 指向的对象:在堆上,真正释放取决于引用计数与释放路径  

指针变量与对象

因此更准确的问法是:array 指向的对象什么时候释放


问:AutoreleasePool 为何可以嵌套使用?

多层嵌套的本质,是在池结构中多次插入“哨兵对象”(pool boundary),以便 pop 时分段回收。

问:AutoreleasePool 的实现原理是怎样的?

AutoreleasePool 原理
AutoreleasePool 原理 2
AutoreleasePool 原理 3

“批量操作”指每次 pop 会把对应边界内的对象统一 release

自动释放池的结构特点

  • 以“栈”式入栈/出栈为核心语义  
  • 内部通过双向链表组织多页(page)  
  • 与线程是一一对应关系  

池结构


AutoreleasePoolPage

AutoreleasePoolPage

class AutoreleasePoolPage
{
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
}

page 组织关系
page 组织关系 2


[obj autorelease] 的实现

autorelease 流程

核心调用链(按原文保留):

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

rootAutorelease2() 最终会走到:

  • AutoreleasePoolPage::autorelease((id)this)  
  • autoreleaseFast(obj)  
static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {// page 有值且没满
        return page->add(obj);
    } else if (page) {// page 有值但满了
        return autoreleaseFullPage(obj, page);
    } else {// page 为空
        return autoreleaseNoPage(obj);
    }
}

autoreleaseFullPage(obj, page)

static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full.
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    assert(page == hotPage());
    assert(page->full()  ||  DebugPoolAllocation);

    do {
        if (page->child)
            page = page->child;
        else
            page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

含义概括:

  • hot page 满了:优先找 child  
  • 没有 child:新建 AutoreleasePoolPage  
  • 找到非满页后 add(obj)  

autoreleaseNoPage(obj)

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // No pool in place.
    assert(!hotPage());

    if (obj != POOL_SENTINEL  &&  DebugMissingPools) {
        _objc_inform("MISSING POOLS: Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug",
                     (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }

    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    // Push an autorelease pool boundary if it wasn't already requested.
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    // Push the requested object.
    return page->add(obj);
}

含义概括:

  • 当前线程没有 page:先新建一页  
  • 需要的话先插入 POOL_SENTINEL  
  • 再把对象 add 进去  

最终小结(按原文逻辑整理):

  • page 存在且没满:直接插入 obj  
  • page 存在但满了:找子 page,没有就新建,再插入 obj  
  • page 不存在:新建 page(并处理哨兵),再插入 obj  

page 追加示意
page 追加示意 2


循环引用

循环引用常见分三种:

  • 自循环引用  
  • 相互循环引用  
  • 多循环引用  

自循环引用

自循环引用示意

示例场景:

  • YZPerson 内有成员变量 YZStudent *student  
  • 例如 YZStudent *student = [[YZPerson alloc] init]; 这类引用链若形成闭环就会泄漏  

自循环引用示意 2
自循环引用示意 3

工程中更常踩坑的点包括:

  • 代理(delegate)  
  • block  
  • NSTimer  
  • “大环引用”(多对象交错持有)  

下面重点看 NSTimer

问:如何破除循环引用?

两个方向:

  • 从源头避免闭环(设计上不形成强引用环)  
  • 在合适时机主动断环(手动置空/销毁/解除绑定)  

问:具体方案有哪些?

  • __weak  
  • __block  
  • __unsafe_unretained  

循环引用方案

__block 破解循环引用

  • MRC__block 修饰对象不会增加引用计数,能在一定程度上避免循环引用  
  • ARC__block 修饰对象默认会被强引用,通常无法避免循环引用,需要手动解环  

__block

__unsafe_unretained 破解循环引用

  • 修饰对象不会增加引用计数,能避免某些循环引用  
  • 但对象释放后可能产生悬垂指针(野指针)风险更高,因此一般不建议优先使用 __unsafe_unretained  

NSTimer 的循环引用与 RunLoop 的强引用

NSTimer 循环引用

典型链路:

  • VC 强引用某对象  
  • 对象强引用 NSTimer  
  • NSTimer 强引用 target(对象)  
  • 形成环,导致无法释放  

能否把“对象持有 NSTimer”改成弱引用解决?

弱引用 NSTimer

这里还要注意另一条强引用链:

  • NSTimer 加入当前线程的 RunLoop  
  • RunLoop 会对 NSTimer 强引用  
  • 如果是主线程,主 RunLoop 强持有 timer;timer 又引用对象
    即使 VC -> 对象 的强引用断开,对象仍可能因为 timer/RunLoop 链路存活,造成泄漏  

另外:

  • 非重复定时器:通常在回调里销毁并置 nil,可以解除循环引用并释放  
  • 重复定时器:不能简单在回调里销毁(逻辑上往往不成立),需要更稳妥的解法  

原文提到的“中间变量解除循环引用”的方式不如某些成熟实现(如尝试让 NSTimer 对对象弱引用)更优,这里保持原意不展开。


延伸阅读与知识体系建议

从 SideTable、weak、AutoreleasePool 到循环引用,这些内容都属于 Runtime + 内存管理核心链路。进一步想把“锁竞争、哈希分流、RunLoop 生命周期、对象销毁路径”串起来时,可以参考 网络/系统 中的并发与基础系统知识来补足底层视角。




上一篇:现代C#语法糖实战手册:.NET Core开发中提升效率的高频技巧
下一篇:Dubbo线程模型详解:配置调优与高频面试题解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 13:36 , Processed in 0.313664 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表