ios内存管理

内存管理方案

nonpointer-isa

末位indexed用来表示当前指针是否是纯的isa指针还是包含一些内存管理信息

1位has_assoc用来表示是否存在关联对象

2位has_cxx_dtor当前对象是否使用到C++的一些代码

3 - 35 共33位shiftcls表示当前对象类对象的指针地址

36 - 41共6位magic

第42位weakly_referenced表示当前对象是否存在弱引用指针

第43位deallocating是否正在进行内存释放操作

第44位has_sidetable_rc表示如果当前isa指针对应引用计数超过表示范围,需要使用外挂sidetable

45-63位extra_rc用来存储引用计数

散列表方式

sideTables()作为一个哈希表,通过对象指针可以找到其对应的引用计数表

sideTable的结构包含下面三个内容:自旋锁,引用计数表,弱引用表

自旋锁

忙等的锁:获取不到会阻塞当前线程,等待锁释放的信号进行唤醒

引用计数表:Map,通过指针查找引用计数

弱引用表:哈希表,通过对象指针找到对应的结构体数组,结构体数组里面存储的每个对象实际上就是弱引用指针

引用计数管理

retain实现

release实现

retainCount实现

dealloc实现 对象是否可以释放会进行判断:共五个条件

  1. 是否是非指针型的isa

  2. 是否存在弱引用指针

  3. 是否存在关联对象

  4. 是否涉及到C++相关内容

  5. 是否通过引用计数表来维护

object_dispose()实现:这里会先调用objc_destructinstance这个函数,然后再free

clearDeallocating实现

弱引用管理

弱引用初始化

当声明__weak id obj = object;时,编译器会生成以下调用链:

复制代码
objc_initWeak(&obj, object) → storeWeak<DontHaveOld, DoHaveNew>(&obj, object)

在storeWeak中会调用weak_register_no_lock这个函数来添加弱引用

弱引用添加

复制代码
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                         id *referrer_id, WeakRegisterDeallocatingOptions options)
  1. 查找 referent 对应的 weak_entry_t(通过weak_entry_for_referent:函数内部通过哈希算法计算对应index,从而在弱引用表中得到这个对象对应的弱引用的数组)

  2. 若找到 entry:调用append_referrer将新的弱引用指针地址添加到 entry 中

  3. 若未找到 entry:创建新的 weak_entry_t,初始化 referent 和第一个 referrer,检查 weak_table 是否需要扩容(weak_grow_maybe),将新 entry 插入 weak_table(weak_entry_insert)

  4. 标记对象为 "被弱引用"(在 refcnts 中设置SIDE_TABLE_WEAKLY_REFERENCED位)

这里描述一下weak_entry_t的结构

复制代码
struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被弱引用的对象(指针伪装处理)
    
    union {
        struct {
            weak_referrer_t *referrers; // 动态数组,存储弱引用指针地址
            uintptr_t        num_refs;  // 弱引用计数
            uintptr_t        mask;      // 动态数组掩码
            uintptr_t        max_hash_displacement;
        };
        struct {
            // 内联数组,当弱引用数≤4时使用,避免动态内存分配
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT]; // WEAK_INLINE_COUNT=4
        };
    };
};

清楚weak变量,将其设置为nil

复制代码
dealloc → clearDeallocating → sidetable_clearDeallocating → weak_clear_no_lock

关键函数weak_clear_no_lock逻辑:

  1. 以对象为 key,从 weak_table 中查找对应的 weak_entry_t

  2. 遍历 entry 中的所有 referrers(弱引用指针地址)

  3. 对每个 referrer 执行*referrer = nil,将弱引用指针自动置空

  4. 将该 entry 从 weak_table 中删除,清理所有相关记录

  5. 清除对象的 "被弱引用" 标记

自动释放池

自动释放池结构

  1. 以栈为结点通过双向链表的形式组合

  2. 是与线程一一对应的

    #define PAGE_SIZE 4096 // 单页总大小:4KB

    struct AutoreleasePoolPage {
    // 校验字段:检测内存越界、结构损坏,调试用
    uint64_t magic;
    // 当前 Page 所属线程(线程隔离核心)
    pthread_t thread;
    // 双向链表指针:前驱页、后继页
    AutoreleasePoolPage *parent;
    AutoreleasePoolPage *child;
    // 指向 Page 中下一个空闲位置(核心指针)
    id *next;

    复制代码
     // 预留空间 + 动态存储区:存放 autorelease 的对象指针
     id objects[];
    
     // 哨兵对象(特殊标记,不是真实 OC 对象)
     static const uintptr_t POOL_BOUNDARY;

    };

完整工作流程

池创建 (Push) → 对象入池 (autorelease) → 池销毁 (Pop/Drain)

push逻辑
  1. 获取当前线程对应的 Page 链表(线程隔离)。

  2. 找到链表最后一个可用 Page。

  3. 将 POOL_BOUNDARY 哨兵 存入 Page 的 next 位置,next 后移。

  4. 返回哨兵的内存地址(称为 token),用于后续销毁池时定位边界。

autorelease逻辑
  1. 校验:nil、TaggedPointer 对象直接跳过,不加入池。

  2. 获取当前线程的 Page 链表。

  3. 检查当前 Page 是否还有空闲位置:

  4. 有空闲:把对象指针存入 *next,next++。无空闲:新建 child Page,再存入对象。

pop逻辑
  1. 根据传入的 token(哨兵地址),定位到当前池的边界。

  2. 从当前 Page 的 next 位置向前遍历:每取出一个对象指针,就调用 obj release(引用计数 -1)。

  3. 持续遍历,直到遇到 POOL_BOUNDARY 哨兵为止。

  4. 遍历完成后,将 next 指针重置到哨兵位置,当前池销毁。 关键:只会释放「当前哨兵之后」的对象,不会影响外层池。

note:在每次runloop快要结束的时候调用pop来进行对象释放

循环引用

自循环引用

对象强持有obj变量,同时变量指向该对象

相互循环引用

A中的obj指向B,B中的obj指向A

多循环引用

如何破除循环引用

  1. 避免产生循环引用

  2. 在合适的位置手动断环

具体的解决方案

  1. __weak

  2. __block

  3. __unsafe_unretained 修饰对象不会增加引用计数,避免循环引用,如果在某一时机被释放,会产生悬垂指针,一般不建议使用

循环引用常见场景

场景 1:Delegate / DataSource 代理(UI 组件高频)

问题成因 视图 / 控件(UIView、UITableView、UICollectionView 等)的 delegate / dataSource 通常指向控制器 ViewController:

  1. VC 强持有 View(self.view)

  2. 若 View 的 delegate 用 strong 修饰 → View 又强持有 VC

  3. 形成闭环:VC(strong) → View(strong) → VC

解决方法:所有 UI 组件的代理、数据源,统一使用 weak 修饰

场景2: Block 循环引用

暂时省略,会后续在Block文章中补充

  1. self(VC/View)强持有 Timer;

  2. Timer 创建时,会底层强持有 target(也就是 self);

  3. 闭环:self(strong) → Timer(strong) → self。

即使对象对NSTimer弱引用持有,由于RunLoop强持有对象,所有当VC销毁,对象也不会释放

解决方案:

对于非重复定时器

  1. 在定时器回调方法当中手动调用invalidate方法

如果是对于重复多次回调的计时器

  1. 添加中间对象

    如果中间对象持有的指向对象的弱引用指针被置为nil,在对应的回调方法中将NStimer给无效,并置为nil

相关推荐
EricStone2 天前
VibeCoding工程流程学习二:iOS项目架构
ios·vibecoding
天桥吴彦祖4 天前
判断iOS如何监听手机屏幕是否锁屏
ios
东坡肘子5 天前
SPI 加入 Apple,Swift 迈向自举 -- 肘子的 Swift 周报 #142
人工智能·swiftui·swift
敲代码的鱼5 天前
PDF 预览与签名批注写回 支持安卓 iOS 鸿蒙 UTS插件
android·前端·ios
时光足迹5 天前
uni-app 视频通话实战:康复师与患者视频问诊的 6 个致命 Bug 与解决方案
android·ios·uni-app
时光足迹5 天前
JPush UniApp UTS 插件完全参考手册:API、事件与厂商通道一网打尽
vue.js·ios·uni-app
时光足迹5 天前
极光推送全攻略(下):uni-app 代码实现与 iOS 排查实战
vue.js·ios·uni-app
时光足迹5 天前
极光推送全攻略(上):被iOS证书折磨了三天,我写了一份前端也能看懂的避坑指南
前端·ios·uni-app
编程范式7 天前
SwiftUI 中图片如何适配可用空间
ios
songgeb8 天前
启发式 UI 自动化:从线性剧本到每步读屏决策
ios·测试