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

291

积分

0

好友

33

主题
发表于 14 小时前 | 查看: 2| 回复: 0

本文将对iOS的内存管理机制进行一次深入的底层探索,涵盖从基础的内存布局到复杂的SideTables、自动释放池实现原理,并详细分析循环引用的成因与解决方案。

iOS内存管理概述图

内存布局

程序的内存通常被划分为以下几个区域:

内存布局示意图

  • 栈(Stack):由编译器自动分配释放,存放函数的参数值、局部变量值等。方法调用就在此区域进行。
  • 堆(Heap):由程序员分配和释放,通过 allocnew 等操作创建的对象都存放在这里。
  • BSS段:存放未初始化的全局变量和静态变量。
  • 数据段(Data):存放已初始化的全局变量和静态变量。
  • 代码段(Text):存放程序的可执行代码。

理解程序的基础网络/系统内存布局是分析高级内存管理机制的前提。

内存管理方案

iOS系统针对不同场景,提供了多样化的内存管理方案,主要有以下三种:

  1. Tagged Pointer:用于存储如NSNumber等小对象,将数据直接存放在指针地址中。
  2. NONPOINTER_ISA:在64位架构下,isa指针的部分比特位被用于存储引用计数等额外信息,而不仅仅是指向类对象的地址。
  3. 散列表(SideTables):这是我们通常讨论的引用计数和弱引用管理机制的核心,包含引用计数表和弱引用表。

NONPOINTER_ISA

NONPOINTER_ISA内存布局图
内存布局详细图
内存字段说明图

NONPOINTER_ISA 利用64位指针的富裕空间,在 isa 中内嵌了对象的引用计数(extra_rc)、是否拥有弱引用等信息,这是对内存和性能的一种优化。

散列表(SideTables)方式

NONPOINTER_ISA 的存储空间不足以存放引用计数时,或者需要管理弱引用时,系统就会使用散列表。

散列表结构图
SideTables()结构图

其核心数据结构是 SideTable

struct SideTable 
{
    spinlock_t slock;        // 保证原子操作的自旋锁
    RefcountMap refcnts;     // 引用计数哈希表
    weak_table_t weak_table; // 弱引用哈希表
}
SideTables的存储与本质

在Runtime源码中,SideTables 是通过一个静态函数返回的。

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++静态初始化器,也没有使用全局指针。

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

StripedMap 是一个以 void* 为键(Key),模板类型 T(这里是 SideTable)为值(Value)的映射表。因此,SideTables() 本质上是一个全局的哈希表

为什么需要多个SideTable?(分离锁技术)

如果全局只有一个 SideTable,那么任何线程对任何对象进行引用计数操作时,都需要对这张大表加锁,这会引发严重的效率问题。

单一SideTable效率问题图示

为了解决这个问题,系统采用了 分离锁 技术。将一张大表拆分成多个(例如64个)SideTable。这样,对不同 SideTable 中对象的操作就可以并行进行,大大提升了并发性能。

分离锁结构图

如何快速定位对象所属的SideTable?

系统通过哈希函数,将对象的内存地址映射到固定的 SideTable 索引上,实现快速分流。

SideTables哈希表本质图

SideTable的数据结构详解

回顾 SideTable 的三个成员:

  1. spinlock_t slock:自旋锁,适用于轻量、快速的访问场景。
  2. RefcountMap refcnts:引用计数表。
  3. weak_table_t weak_table:弱引用表。

RefcountMap 引用计数表

这是一个哈希表,以对象的 DisguisedPtr(伪装的对象地址)为键,以 size_t 为值。这个 size_t 的值在位移后(通常是右移2位)才代表对象实际的引用计数。

引用计数表结构图
size_t内存布局图

weak_table_t 弱引用表

同样是一个哈希表结构,它以对象地址为键,以 weak_entry_t 结构体为值。一个对象可能被多个 __weak 变量指向,这种一对多的关系就存储在 weak_entry_t 中。

weak_table_t结构图

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

MRC 与 ARC

MRC(手动引用计数)

开发者需要手动调用 retain, release, autorelease, retainCount, dealloc 等方法管理内存。在MRC中,必须在 dealloc 方法中显式调用 [super dealloc]

ARC(自动引用计数)

由编译器(LLVM)和运行时(Runtime)协作完成。编译器在编译期插入合适的 retainrelease 调用;运行时则负责管理弱引用和自动释放池等。ARC下禁止显式调用 retain, release, retainCount, dealloc。可以重写 dealloc 来释放非Objective-C资源(如CF对象、移除通知),但禁止调用 [super dealloc](编译器自动处理)。ARC引入了 __weak__strong 等所有权修饰符。

引用计数管理实现剖析

alloc 实现

alloc 方法的核心是调用 C 函数 calloc 分配内存,并初始化 isa 指针。值得注意的是,在 alloc 阶段,对象的引用计数并未被设置为1,这与许多人的直觉相悖。

retain 实现

retain 操作最终会走到 sidetable_retain 函数。

retain实现代码流程图

id objc_object::sidetable_retain() {
    SideTable& table = SideTables()[this]; // 1. 通过哈希找到SideTable
    table.lock();
    size_t& refcntStorage = table.refcnts[this]; // 2. 找到引用计数存储位置
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE; // 3. 引用计数+1 (实际加4)
    }
    table.unlock();
    return (id)this;
}

流程:通过对象地址两次哈希查找(先找 SideTable,再在 RefcountMap 中找具体条目),然后增加引用计数。

release 实现

release 操作的核心是减少引用计数,并在计数为0时触发 dealloc

release实现代码流程图

uintptr_t objc_object::sidetable_release(bool performDealloc) {
    SideTable& table = SideTables()[this]; // 1. 找到SideTable
    bool do_dealloc = false;
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this); // 2. 找到条目
    if (it == table.refcnts.end()) { // 未找到,说明计数为0
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) { // 计数小于阈值,将变为0
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE; // 3. 引用计数-1
    }
    table.unlock();
    if (do_dealloc && performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return do_dealloc;
}

retainCount 实现

retainCount 的返回值是 1 + extra_rc + sidetable中的计数。对于新 alloc 的对象,其散列表中的计数为0,但 retainCount 方法会返回1。

retainCount实现代码流程图

dealloc 实现

dealloc 的内部调用链非常清晰,负责对象的最终销毁工作。

对象释放总流程图
调用链:dealloc -> _objc_rootDealloc -> rootDealloc

object_dispose实现流程图
关键函数 object_dispose 负责处理销毁实例。

objc_destructInstance销毁流程
objc_destructInstance 依次处理C++析构函数和关联对象。

总结 dealloc 的关键操作

  1. 调用 C++ 析构函数(如果对象有 hasCxxDtor)。
  2. 移除关联对象(_object_remove_associations)。
  3. 清理弱引用表和引用计数表。

clearDeallocating清理弱引用和引用计数表
最后一步:sidetable_clearDeallocating 清除弱引用并擦除引用计数条目。

关联对象是否需要手动移除?
不需要。系统在 dealloc 时已自动调用 _object_remove_associations() 进行清理。

弱引用(__weak)管理机制

弱引用的建立

__weak 修饰符在编译时会被转换为对 objc_initWeak 函数的调用。

弱引用初始化代码转换图

其核心流程是调用 storeWeak,并最终通过 weak_register_no_lock 将弱引用指针注册到对应对象的 weak_entry_t 中。

添加weak变量流程图
weak_register_no_lock注册弱引用

弱引用的清除(自动置nil)

当对象被释放时,在 dealloc 的清理阶段,会调用 weak_clear_no_lock 函数。

weak_clear_no_lock工作示意图

该函数从弱引用表中取出该对象对应的所有弱引用指针数组,然后遍历这个数组,将其中每一个弱引用指针的值都置为 nil。这正是弱引用“自动置nil”功能的实现原理。

自动释放池(AutoreleasePool)

自动释放池的作用与时机

自动释放池的主要作用是延迟对象的 release 时机。对于工厂方法(如 [NSArray array])返回的对象,系统会将其加入自动释放池,通常在当前 RunLoop 迭代结束时进行释放。

自动释放池与RunLoop关系图

重要区分

  • alloc/init 创建的局部变量:编译器(遵循方法家族命名约定)通常会在作用域结束时直接插入 release不会加入自动释放池。这是ARC的性能优化。
  • 工厂方法创建的对象:通常会被加入自动释放池,在RunLoop休眠或池子 pop 时释放。

自动释放池的实现原理

@autoreleasepool {} 在编译时会被改写为 pushpop 调用。

@autoreleasepool编译转换

void *ctx = objc_autoreleasePoolPush();
// {} 中的代码
objc_autoreleasePoolPop(ctx);

objc_autoreleasePoolPush调用关系
objc_autoreleasePoolPop调用关系

自动释放池底层是由一系列 AutoreleasePoolPage双向链表形式连接而成的栈结构,且与线程一一对应。

双向链表结构图

AutoreleasePoolPage 结构

class AutoreleasePoolPage {
    id *next;                         // 指向下一个可存放autorelease对象地址的位置
    pthread_t const thread;           // 所属线程
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    // ...
};

AutoreleasePoolPage内存结构图

每个 Page 就像一页栈。push 操作会插入一个哨兵对象(POOL_SENTINEL)pop 操作时会释放从上次 push 之后直到这个哨兵对象之间所有加入池中的对象。

push操作插入哨兵对象

autorelease 方法实现

[obj autorelease] 方法的本质,就是调用 autoreleaseFast 函数,将对象加入当前线程的 hotPage(当前活跃的Page)。

autorelease方法实现流程图

流程简述

  1. 如果当前 hotPage 存在且未满,直接加入。
  2. 如果 hotPage 已满,则沿着 child 指针查找或创建新的Page,并加入。
  3. 如果没有 hotPage,则创建第一个Page,先加入哨兵对象,再加入目标对象。

pop 操作

pop 操作根据传入的哨兵对象上下文,找到对应位置,然后向该位置之前的所有对象发送 release 消息,最后回滚 next 指针。

AutoreleasePoolPage::pop操作说明

嵌套的 @autoreleasepool 就是多次插入哨兵对象,pop 时则一层层释放。在如 for 循环内创建大量临时对象的场景,手动添加 @autoreleasepool 可以及时降低内存峰值。

循环引用及解决方案

循环引用主要发生在对象间相互强引用,导致引用计数无法降为零的情况。常见于Delegate、Block、NSTimer等场景。

循环引用的类型

  • 自循环引用:对象强持有自身。
  • 相互循环引用:两个对象相互强引用。
  • 多循环引用:多个对象形成环状强引用链。

自循环引用示意图

解决方案

核心思路:避免产生循环引用在合适时机手动断环
常用修饰符:

  1. __weak:不会增加引用计数,被修饰对象释放后指针自动置nil。最安全常用
  2. __unsafe_unretained:不会增加引用计数,但对象释放后会产生悬垂指针,不安全。
  3. __block(在MRC下):可以用于打破Block的循环引用(因为MRC下__block不会retain对象)。在ARC下,__block变量会被强引用,通常需要配合在Block内手动置nil来断环。

NSTimer的循环引用问题

NSTimer会强引用它的 target,如果 target(例如ViewController)又强引用这个Timer,就会形成循环引用。此外,RunLoop也会强引用已加入的Timer,这加剧了问题的复杂性。

NSTimer循环引用问题图

解决方案通常采用“中间对象”模式,让一个弱引用 target 的中间类来充当Timer的 target,并在中间类的方法中回调真正的目标。或者使用iOS 10+的 block 形式API。深入理解iOS内存管理的这些底层机制,不仅能帮助我们编写更健壮、高效的代码,也是应对复杂内存问题、进行性能调优的坚实基础。对于希望深入Android/iOS底层开发的工程师来说,这是必修课。




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

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

GMT+8, 2025-12-24 17:19 , Processed in 0.215507 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

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