iOS内存管理(内存布局/nonpointer/sidetable/alloc/init/retain/release/weak/dealloc/自动释放池)

本篇主作总结笔记,参考和摘抄了很多优质博客,在最底部。

内存布局

内存分五大区,App 启动时,系统会把程序拷贝到内存,在内存中执行代码。

首先说一下排在内存五大区之外的内核区和保留区:

  1. 内核区:主要处理内核模块,比如我们的系统内存为 4GB,那么我们实际上能使用 3GB,剩下的 1GB 就是给了内核区,指针地址 0xc0000000(3x1024x1024x1024)
  2. 保留区:用来给系统提供一些必要空间

当一个 app 启动后,代码区、常量区、全局区大小就已经固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)

1. 栈区(Stack)

栈区里面会放一些函数参数以及一些局部变量,逐渐增多并在内存地址中由高向低延伸,由于栈的数据结构的原因,它的内存地址是连续的,栈区的内存大小在App启动时就确定下来的,若在压入时申请的内存空间大于栈的剩余空间,就会出现栈溢出(内存泄露),打印的地址为0x7在栈区

  • 栈区是一块连续的内存空间,内存大小在 App 启动时就确定下来,若在压入时申请的内存空间大于栈的剩余空间,就会出现栈溢出,即内存泄漏
  • 存储结构从高地址往低地址延伸
  • 栈区存储的是局部变量函数方法参数指针
  • 栈区的地址空间一般是以0x7开头

举例:

objc 复制代码
- (void)testStack{
    // 栈区
    int a = 10;
    int b = 20;
    NSObject *object = [NSObject new];
    NSLog(@"a == %p",&a);
    NSLog(@"b == %p",&b);
    NSLog(@"object == %p",&object);
    NSLog(@"%lu",sizeof(&object));
    NSLog(@"%lu",sizeof(a));
}

打印结果如下:

举个栈溢出的例子:

objc 复制代码
while (10000) {
  int a = 2;
}

上面这段代码会造成内存暴涨,因为栈区只有1M大小,一个int类型是4字节,创建一个局部变量就会压入一个4字节到栈区,当超过栈区的上限时就会造成栈溢出。

解决方法可以给代码加一个自动释放池

objc 复制代码

栈区在内存中是由高向低存储,堆区是由低向高存储,当两者相遇时,就出现了堆栈溢出

2. 堆区(heap)

  • 堆内存大小是动态变化的,取决于系统的虚拟内存
  • 存储结构是从低地址向高地址扩展
  • 系统是用链表来管理堆的内存的,所以它的内存地址是不连续
  • 堆区存储的是对象allocnew出来的变量。
  • 堆区地址空间一般以0x6开头

举例:

objc 复制代码
- (void)testHeap{
    // 堆区
    NSObject *object1 = [NSObject new];
    NSObject *object2 = [NSObject new];
    NSObject *object3 = [NSObject new];
    NSObject *object4 = [NSObject new];
    NSObject *object5 = [NSObject new];
    NSObject *object6 = [NSObject new];
    NSObject *object7 = [NSObject new];
    NSObject *object8 = [NSObject new];
    NSObject *object9 = [NSObject new];
    NSLog(@"object1 = %@",object1);
    NSLog(@"object2 = %@",object2);
    NSLog(@"object3 = %@",object3);
    NSLog(@"object4 = %@",object4);
    NSLog(@"object5 = %@",object5);
    NSLog(@"object6 = %@",object6);
    NSLog(@"object7 = %@",object7);
    NSLog(@"object8 = %@",object8);
    NSLog(@"object9 = %@",object9);
}

结果如下:

在 OC,系统是通过引用计数判断是否释放对象,当引用计数为 0 就说明没有任何变量使用该空间,系统将释放对象。

3. 全局区/静态区

全局变量和静态变量的存储是放在一块的,分为.bss段.data段,内存地址一般由0x1开头:

  • Bss段: 未初始化的全局变量和静态变量在一块区域
  • Data段: 已初始化的全局变量和静态变量在相邻的另一块区域

举例:

objc 复制代码
static int bss;
static int bssStr;
static int data = 10;
static NSString *dataStr = @"nihao";
- (void)globalTest {
    // 全局区
    NSLog(@"****bss****");
    NSLog(@"bss == %p",&bss);
    NSLog(@"bssStr == %p",&bssStr);
    NSLog(@"****data****");
    NSLog(@"data == %p",&data);
    NSLog(@"dataStr == %p",&dataStr);
}

结果如下:

若在.h中创建一个静态变量ws_number,则使用这个静态变量的每个文件都会生成一个ws_number的静态变量,且初始值都一样。也就是说在多个文件使用同一个静态变量,系统会各自生成一个相同初始值地址不同的静态变量 ,这样在各自文件内使用就不会互相干扰,数据比较安全。这也是为什么静态区也称作静态安全区

4. 常量区

  • 常量字符串就是放在这里的
  • 程序结束后由系统释放

5. 代码段(.text)

  • 存储程序代码,在编译时加载到内存中,代码会被编译成二进制的形式进行存储

举个例子来理解一下:

1、对象查找过程: 在程序查找一个对象时,首先到栈区找到对象的指针,再通过这个对象指针到堆区找到这个对象。

内存管理

NONPOINTER_ISA

正如上面所说,防止地址空间浪费,isa指针设计成了联合体,在isa地址中存储了很多信息。

isa 的结构如下:

c 复制代码
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
#endif
};

OC对象的本质,每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个联合体union结构,同时使用位域来存储更多的信息。 它是通过isabits进行位运算,取出响应位置的值,runtime中的isa是被联合体位域优化过的,它不单单是指向类对象了,而是把64位中的每一位都运用了起来,其中的shiftcls为33位,代表了类对象的地址,其他的位都有各自的用处。

  • nonpointer:表示是否对isa指针开启指针优化 0:不开启,表示纯isa指针。 1:开启,不单单是类对象的地址,isa中包含了类信息和对象的引用计数等。
  • has_assoc:关联对象标识位,0没有,1有,没有关联对象会释放的更快。
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。
  • shiftcls:存储类指针class的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤用来存储类指针。
  • magic:固定值为0xd2,用于在调试时分辨对象是否完成初始化。
  • weakly_referenced:表示对象是否被指向或者曾经指向一个 ARC 的弱引用变量, 没有弱引⽤的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存。
  • has_sidetable_rc:当对象的引用计数大于10,以至于无法存储在isa指针中时,用散列表sidetable去计数。
  • extra_rc:表示该对象的引用计数,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数⼤于 10, 则需要使⽤到has_sidetable_rc。

TaggedPointer

为了节省内存和提高执行效率,苹果提出了 Tagged Pointer 的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。

NSTaggedPointer类型的对象采用和isa一样的联合体位域的方式,可直接从地址中读取出想要的值,一般当数据类型的"value"足够小时,系统会自动转换为NSTaggedPointer类型,比如NSString转换为NSTaggedPointerString

对象的值直接存储在了指针中,不必在堆上为其分配内存,节省了很多内存开销。

更详细的 TaggedPointer 解析可以看iOS - 老生常谈内存管理(五):Tagged Pointer

引用计数

主要摘自iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-一

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象时,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。

SideTables

引用计数要么存放在 isaextra_rc 中,要么存放在引用计数表中,而引用计数表包含在一个叫 SideTable 的结构中,它是一个散列表,也就是哈希表。而 SideTable 又包含在一个全局的 StripeMap 的哈希映射表中,这个表的名字叫 SideTables

NSObject.mmSideTables对应的源码如下

objc 复制代码
// SideTables
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

// SideTable
struct SideTable {
    spinlock_t slock;           // 自旋锁
    RefcountMap refcnts;        // 引用计数表
    weak_table_t weak_table;    // 弱引用表
    
    // other code ...
};

它们的关系如下图:

自旋锁spinlock_t

自旋锁适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

spinlock_t slock用于对sidetable加锁,保证数据安全,使用自旋锁作为安全锁其实是因为引用计数的操作非常快且频繁。

引用计数器RefcountMap refcnts

具体的引用计数数量是记录在这里的,refcntsC++Map,在SideTable中需要再次调用table.refcnts.find(0x0000)或者table.refcnts.find(0x000f)找到真正的引用计数器。

引用计数器的存储结构如下图所示

具体的,引用计数器RefcountMap refcnts经过find查找到的value其实是个位域,类似NONPOINTER_ISA指针:

  • 1UL<<0:WEAKLY_REFERENCED
    表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。
  • 1UL<<1:DEALLOCATING
    表示对象是否正在被释放。1正在释放,0没有。
  • REAL COUNT
    图中REAL COUNT的部分才是对象真正的引用计数存储区。所以咱们说的引用计数加一或者减一,实际上是对整个unsigned long加四或者减四,因为真正的计数是从2^2位开始的。
  • 1UL<<(WORD_BITS-1):SIDE_TABLE_RC_PINNED
    其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。
  • (1UL<<0)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动0位(就是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001
  • (1UL<<1)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010

弱引用列表weak_table_t weak_table

weak_table_t结构如图所示

  • weak_entry_t *weak_entries:是一个数组,上面的RefcountMap是要通过find(key)来找到精确的元素的。weak_entries则是通过循环遍历来找到对应的entry
    • referent:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
    • referrers:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
    • inline_referrers:只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。
  • num_entries:用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2

Q:既然SideTables是一个哈希映射的表,为什么不用 SideTables 直接包含自旋锁,引用计数表和弱引用表呢?

这是因为在众多线程同时访问这个 SideTable 表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张 SideTable 的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,虽然自旋锁已经是效率非常高的锁,这会带来非常不好的用户体验。针对这种情况,将一张 SideTable 分为多张表的 SideTables,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,这就是为什么一个 SideTables 下涵盖众多 SideTable 表的原因。

Q:为什么SideTables已经通过Hash映射了,还需要RefcountMap再映射一次

其实苹果采用的是分块化思想 ,内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。

假设现在内存中有16个对象,0x0000、0x0001、...... 0x000e、0x000f,咱们创建一个SideTables[8]来存放这 16 个对象,那么查找的时候发生Hash冲突的概率就是八分之一。假设SideTables[0x0000]SideTables[0x0x000f]冲突,映射到相同的结果。

objc 复制代码
SideTables[0x0000] == SideTables[0x0x000f]  ==> 都指向同一个SideTable

苹果把两个对象的内存管理都放到同一个SideTable中。你在这个SideTable中需要再次调用table.refcnts.find(0x0000)或者table.refcnts.find(0x000f)来找到他们真正的引用计数器。

内存管理相关操作

alloc

从一个面试题开始探索alloc流程:

问:alloc 之后,引用计数如何变化?

答:在初始化isa的时候,并没有对extra_rc进行操作。也就是说alloc方法实际上并没有设置对象的引用计数值为 1。

验证如下:创建一个LGPerson类,在main方法中创建一个实例对象

objc 复制代码
#import <Foundation/Foundation.h>
#import "LGPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *objc2 = [LGPerson alloc];
        NSLog(@"Hello, World!  %@",objc2);
    }
    return 0;
}

NSObject.mm类的+ (id)alloc方法开始探索,方法(函数)从上往下依次执行,省略了部分影响解读的代码

objc 复制代码
+ (id)alloc {
    return _objc_rootAlloc(self);
}

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

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    // some code ...
    
    id obj = class_createInstance(cls, 0);
    return obj;
    
}

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

// 内部调用 calloc 方法分配内存。
static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{

    // some code ...
    id obj;
    obj = (id)calloc(1, size);  // 此时分配内存
    obj->initInstanceIsa(cls, hasCxxDtor);
    return obj;
}

上面的代码主要目的是分配内存,真正的初始化在initInstanceIsa,内部会初始化isa指针的内容

objc 复制代码
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}


inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        // 将cls右移3位赋值给shiftcls
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}

initIsa里面,将bits赋值了ISA_MAGIC_VALUEISA_MAGIC_VALUE为宏定义0x001f800000000001ULL,断点后可以看到nonpointer1magic59,如下图 执行shiftcls赋值方法后,cls值已经变成了LGPerson(自定义的类名),即已经赋值成功,如下图 此时可以注意到,extra_rc的值是0,表示alloc这一步实际上分配了内存,初始化了对象,但引用计数实际上是 0(调用init之后就变成 1 了)。

值得一提的是callAlloc函数中的slowpathfastpathcallAlloc完整实现如下:

objc 复制代码
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

方法中使用到的 slowpathfastpath,其实这两个都是宏定义,与代码逻辑本身无关,定义如下:

objc 复制代码
// x 很可能为 true,希望编译器进行优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能为 false,希望编译器进行优化
#define slowpath(x) (__builtin_expect(bool(x), 0))

其实它们是所谓的快路径和慢路径,为了解释这个,我们来看一段代码:

objc 复制代码
if (x)
    return 1;
else 
    return 39;

由于计算机并非一次只读取一条指令,而是读取多条指令,所以在读到 if 语句时也会把 return 1 读取进来。如果 x 为 0,那么会重新读取 return 39,重读指令相对来说比较耗时。

如果 x 有非常大的概率是 0,那么 return 1 这条指令每次不可避免的会被读取,并且实际上几乎没有机会执行,造成了不必要的指令重读。

因此,在苹果定义的两个宏中,fastpath(x) 依然返回 x,只是告诉编译器 x 的值一般不为 0,从而编译可以进行优化。同理,slowpath(x) 表示 x 的值很可能为 0,希望编译器进行优化。

objc 复制代码
// 以下代码表示,
// 很可能 checkNil && !cls 的结果是 false,
// 编译器可以不用每次都读取 return nil 指令
 if (slowpath(checkNil && !cls)) return nil;

当然,当checkNil && !cls判断成立的时候,return nil 指令还是会被读取,然后执行的。
fastpath(expression)也是同样的机制,表示很可能 expression 结果是 true

init

objc 复制代码
// NSObject.mm
// Calls [[cls alloc] init].
id
objc_alloc_init(Class cls)
{
    return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}

- (id)init {
    return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

基类的init方法啥都没干,只是将alloc创建的对象返回。我们可以重写init方法来对alloc创建的实例做一些初始化操作。

retainCount

retainCount方法是取出对象的引用计数值。怎么取值的呢?相信你们已经想到了,isaSidetable,下面我们进入源码看看它的取值过程。

retainCount方法的函数调用栈如下

objc 复制代码
- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}

uintptr_t _objc_rootRetainCount(id obj)
{
    return obj->rootRetainCount();
}

inline uintptr_t 
objc_object::rootRetainCount()
{
    // 如果是 tagged pointer,直接返回 this
    if (isTaggedPointer()) return (uintptr_t)this; 

    sidetable_lock();
    // 获取 isa 
    isa_t bits = LoadExclusive(&isa.bits); 
    ClearExclusive(&isa.bits);
    // 如果 isa 是 nonpointer
    if (bits.nonpointer) { 
        // isa指针里的引用计数字段extra_rc,再+1,为引用计数的值
        // alloc没有让引用计数+1,而获取retainCount却是1原因就在这
        uintptr_t rc = 1 + bits.extra_rc; 
        // 如果还额外使用 sidetable 存储引用计数
        if (bits.has_sidetable_rc) { 
            rc += sidetable_getExtraRC_nolock(); // 加上 sidetable 中引用计数的值
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    // 如果 isa 不是 nonpointer,返回 sidetable_retainCount() 的值
    return sidetable_retainCount(); 
}

size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    ASSERT(isa.nonpointer);
    // 获得 SideTable
    SideTable& table = SideTables()[this]; 
    // 获得 refcnts
    RefcountMap::iterator it = table.refcnts.find(this);
    // 如果没找到,返回 0
    if (it == table.refcnts.end()) return 0;  
    // 如果找到了,通过 SIDE_TABLE_RC_SHIFT 位掩码获取对应的引用计数
    else return it->second >> SIDE_TABLE_RC_SHIFT; 
}

#define SIDE_TABLE_RC_SHIFT 2

小结

  • arm64之前,isa不是nonpointer。对象的引用计数全都存储在SideTable中,retainCount 方法返回的是对象本身的引用计数值 1,加上SideTable中存储的值;
  • arm64开始,isanonpointer。对象的引用计数先存储到它的isa中的extra_rc中,如果 19 位的extra_rc不够存储,那么溢出的部分再存储到SideTable中,retainCount 方法返回的是对象本身的引用计数值 1,加上isa中的extra_rc存储的值,加上SideTable中存储的值。
  • 所以,其实我们通过retainCount方法打印alloc创建的对象的引用计数为 1,这是retainCount方法的功劳,alloc方法并没有设置对象的引用计数。

alloc方法没有设置对象的引用计数为 1,它内部也没有调用retainCount方法,init时也只是返回alloc创建的对象,按照引用计数的定义,对象不会直接dealloc吗?

dealloc方法是在release方法内部调用的。只有你直接调用了dealloc,或者调用了release且在release方法中判断对象的引用计数为 0 的时候,才会调用dealloc。详情请参阅release源码分析。

retain&release

  • retain
    • isa中的extra_rc +1,如果溢出,就将extra_rcRC_HALF转移到sidetable中存储,extra_rc19位,而RC_HALF宏是(1ULL<<18),实际上相等于进行了 +1 操作
    • 对于NSTaggedPointer类型,直接返回,不参与引用计数计算,因为NSTaggedPointer对象的值直接存储在了指针中,不必在堆上为其分配内存,这点在objc_msgSend也有体现,首先会判断LNilOrTagged
  • release
    • isa中的extra_rc -1,如果下溢,则判断has_sidetable_rc是否为true,即是否使用了sidetable,如果有的话就从sidetable中转移RC_HALF个引用计数给extra_rc,若不够RC_HALF个,就有多少转移多少,如果extra_rc中引用计数为 0 且has_sidetable_rcfalse或者Sidetable中的引用计数也为 0 了,那就dealloc对象。

详情参阅 [iOS - 老生常谈内存管理 - retain&release]

weak

如果用__weak 修饰一个变量,底层执行了什么呢?以下面代码为例

objc 复制代码
id obj = [[NSObject alloc] init]; 
id __weak obj1 = obj;

底层的操作其实是

objc 复制代码
objc_initWeak(&obj1, obj);
// NSObject.mm 
① objc_initWeak 
② storeWeak 
// objc-weak.mm 
③ weak_unregister_no_lock
④ weak_register_no_lock 

接下来查看源码

objc_initWeak

objc 复制代码
// *location 为 __weak 指针地址(即obj1),newObj(即obj)为被弱引用对象地址
id objc_initWeak(id *location, id newObj) 
{
    // 如果对象为 nil,那就将 weak 指针置为 nil
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

storeWeak

objc 复制代码
// 更新 weak 变量
// 如果 HaveOld == true,表示对象有旧值,即旧地址,它需要被清理掉,这个旧值可能为 nil
// 如果 HaveNew == true,表示一个新值需要赋值给变量,这个新值可能为 nil
// 如果 CrashIfDeallocating == true,则如果对象正在销毁或者对象不支持弱引用,则停止更新
// 如果 CrashIfDeallocating == false,则存储 nil
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry: // 分别获取新旧值相关联的弱引用表
    // 如果变量有旧值,获取已有对象(该旧值对象)和旧表
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    // 如果有新值要赋值给变量,则创建新表
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    // 分别给 oldTable 和 newTable 加锁
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    // 判断 oldObj 和 location 是否是同一对象,如果不是就重新获取旧值相关联的表
    if (haveOld  &&  *location != oldObj) {
        // 解锁
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    // 如果有新值,则判断新值所属的类是否已经初始化,没初始化的话在此初始化
    // 这一步是防止 +initialize 内部调用 storeWeak 产生死锁
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    // 如果有旧值,则调用weak_unregister_no_lock执行清空操作
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    // 如果有新值,则调用weak_register_no_lock把所有 weak 指针重新指向新的对象
    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.
        // 设置 weakly-referenced 标志位
        // 如果对象是 Tagged Pointer,则不做操作
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        // 将 location 指向新的对象
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    // 解锁
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

store_weak函数的执行过程如下:

  1. 分别获取新旧值相关联的弱引用表
  2. 如果有旧值,调用weak_unregister_no_lock函数清除旧值,移除所有指向旧值的weak引用,而不是赋值为nil
  3. 如果有新值,调用weak_register_no_lock函数分配新值,将所有weak指针重新指向新的对象
  4. 设置isaweakly_referenced弱引用标志位

weak_unregister_no_lock

weak_unregister_no_lock用来移除弱引用对象

objc 复制代码
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    // 被弱引用的对象
    objc_object *referent = (objc_object *)referent_id;
    // 弱引用变量的地址
    objc_object **referrer = (objc_object **)referrer_id;
    
    /**
     * weak_entry_t 结构
     * - referent 被弱引用的对象
     * - referrers 当有超过4个弱引用对象时,则存储到 referrers 中
     * - inline_referrers 存储小于4个的弱引用对象
     */
    weak_entry_t *entry;
    
    if (!referent) return;
    // 调用 weak_entry_for_referent 找到 entry 弱引用指针 item
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        // 从内层 inline_referrers 中移除 entry
        // inline_referrers 中只能存储 4 个弱引用指针,
        // 多了就要存储到 referrers 中,所以要多一步 empty 判空操作
        remove_referrer(entry, referrer);
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false;
                    break;
                }
            }
        }
        
        //从 weak_table 中移除 entry 弱引用条目
        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }

    // Do not set *referrer = nil. objc_storeWeak() requires that the 
    // value not change.
}

weak_unregister_no_lock用来移除已经存在的弱引用表,一般用于弱引用对象已经不再引用,但被弱引用对象还没有死亡的情况,内部执行步骤为:

  1. 查询weak_table,如果有弱引用信息,则得到entry
  2. remove_referrer(entry, referrer)移除相关联的弱引用信息。

weak_register_no_lock

weak_register_no_lock用来保存弱引用对象

scss 复制代码
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    //被弱引用的对象
    objc_object *referent = (objc_object *)referent_id;
    //弱引用变量的地址
    objc_object **referrer = (objc_object **)referrer_id;

    //如果该弱引用对象是taggedPointer对象,则不做处理直接返回该对象
    //taggedPointer对象是为了苹果为了性能最大化做的处理,
    //针对不需要到堆中寻找的对象,可以直接从地址中通过一定的算法得到他们的值。
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }

    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    weak_entry_t *entry;//弱引用指针的条目
    //判断weak_table中是否有该条目
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //如果有,则把弱引用对象追加进去
        append_referrer(entry, referrer);
    } 
    else {
        //如果没有,则创建一个
        weak_entry_t new_entry(referent, referrer);
        //如果索引已经超过原来的3/4,则给weak_table扩容
        weak_grow_maybe(weak_table);
        //将新的entry插入weak_table
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}

weak_register_no_lock保存弱引用对象具体流程如下:

  • 判断TaggedPointer类型则直接返回,不需要保存弱引用信息(TaggedPointer不需要在堆中分配内存)
  • 如果正在释放且对象不支持弱引用,则停止更新(crashIfDeallocating==true && deallocating == true)
  • 判断weak_table中是否有该对象的弱引用表,
    • 有就追加上append_referrer
    • 没有则创建个weak_table在加入

小结

weak对象在底层的存储流程如下图所示

dealloc

dealloc方法的函数调用栈为:

objc 复制代码
// NSObject.mm
① dealloc
② _objc_rootDealloc
// objc-object.h
③ rootDealloc
// objc-runtime-new.mm
④ object_dispose
⑤ objc_destructInstance
// objc-object.h
⑥ clearDeallocating
// NSObject.mm
⑦ sidetable_clearDeallocating
   clearDeallocating_slow

objc_rootDealloc&rootDealloc

objc 复制代码
- (void)dealloc {
    _objc_rootDealloc(self);
}

void _objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

inline void objc_object::rootDealloc()
{
    // 判断是否为 TaggerPointer 内存管理方案,是的话直接 return
    if (isTaggedPointer()) return;  // fixme necessary? * 

    if (fastpath(isa.nonpointer  &&          // 如果 isa 为 nonpointer
                 !isa.weakly_referenced  &&  // 没有弱引用
                 !isa.has_assoc  &&          // 没有关联对象
                 !isa.has_cxx_dtor  &&       // 没有 C++ 的析构函数
                 !isa.has_sidetable_rc))     // 没有额外采用 SideTabel 进行引用计数存储
    {
        assert(!sidetable_present());
        free(this);               // 如果以上条件成立,直接调用 free 函数销毁对象
    } 
    else {
        object_dispose((id)this); // 如果以上条件不成立,调用 object_dispose 函数
    }
}

fastpath(x)表示内部的x值很可能为true,希望编译器进行优化,如果符合5个条件,则直接free

object_dispose&objc_destructInstance

objc 复制代码
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        // 如果有 C++ 的析构函数,调用 object_cxxDestruct 函数
        if (cxx) object_cxxDestruct(obj);
        // 如果有关联对象,调用 _object_remove_assocations 函数,移除关联对象
        if (assoc) _object_remove_assocations(obj);
        // 调用 clearDeallocating 函数
        obj->clearDeallocating();

    }

    return obj;
}

clearDeallocating

objc 复制代码
inline void 
objc_object::clearDeallocating()
{
    // 如果 isa 不是 nonpointer
    if (slowpath(!isa.nonpointer)) {     
        // Slow path for raw pointer isa.
        // 调用 sidetable_clearDeallocating 函数
        sidetable_clearDeallocating();   
    }
    // 如果 isa 是 nonpointer,且有弱引用或者有额外使用 SideTable 存储引用计数
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) { 
        // Slow path for non-pointer isa with weak refs and/or side table data.
        // 调用 clearDeallocating_slow 函数
        clearDeallocating_slow();        
    }

    assert(!sidetable_present());
}

slowpath(x) 表示 x 的值很可能为false,希望编译器进行优化。先不考虑isa不是nonpointer的情况,我们继续看clearDeallocating_slow清除弱引用及引用计数的操作。

objc 复制代码
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    // 获取 SideTable
    SideTable& table = SideTables()[this]; 
    table.lock();
    // 如果有弱引用
    if (isa.weakly_referenced) { 
        // 调用 weak_clear_no_lock:将指向该对象的弱引用指针置为 nil
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    // 如果有使用 SideTable 存储引用计数
    if (isa.has_sidetable_rc) {  
        // 调用 table.refcnts.erase:从引用计数表中擦除该对象的引用计数
        table.refcnts.erase(this);
    }
    table.unlock();
}

weak_clear_no_lock

清除弱引用的核心方法,在对象dealloc的时候,会调用weak_clear_no_lock函数将指向该对象的弱引用指针置为nil,具体实现如下

objc 复制代码
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //referent为被销毁对象的指针
    objc_object *referent = (objc_object *)referent_id;
    //通过被销毁对象的指针获得entry,这个entry里存着这个对象的弱引用指针数组
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    //弱引用指针数组
    weak_referrer_t *referrers;
    size_t count;
    //判断弱引用指针数量,小于4个存在entry的inline_referrers中,大于4个存在entry的referrers中
    if (entry->out_of_line()) {
        //如果大于4个,则到referrers中取弱引用指针的数组
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    //循环把所有弱引用指针置为nil
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    // 一些 free和num_entries--操作
    weak_entry_remove(weak_table, entry);
}

小结

dealloc 的调用流程如下:

  1. dealloc时首先执行dealloc->_objc_rootDealloc(),直接来到rootDealloc()函数
  2. rootDealloc()中判断5个条件决定是否可以直接释放free()
    • NONPointer_ISA // 是否是非指针类型 isa
    • weakly_reference // 是否有若引用
    • has_assoc // 是否有关联对象
    • has_cxx_dtor // 是否有 c++ 相关内容
    • has_sidetable_rc // 是否使用到 sidetable
  3. 不符合5个条件则调用objc_dispose(),进而执行objc_destructInstance()函数逐步释放
    • 先判断hasCxxDtor,销毁 c++ 相关内容
    • 再判断hasAssociatedObjects,销毁关联对象
    • 执行clearDeallocating(),用以销毁弱引用和引用计数
  4. clearDeallocating()销毁弱引用和引用计数
    • 执行waek_clear_no_lock销毁弱引用
    • 获取SideTable执行table.refcnts.eraser()擦除该对象的引用计数

自动释放池AutoReleasePool

这里主要作总结笔记,更详细的内容参考iOS - 聊聊 autorelease 和 @autoreleasepool

AutoreleasePool原理

  • 自动释放池(即所有的AutoreleasePoolPage对象)是以为结点通过双向链表的形式组合而成,每当Page满了的时候,就会创建一个新的Page,并设置它为hotPage,而首个PagecoldPage
  • 有个属性叫POOL_BOUNDARY,称为哨兵对象,用来解决自动释放池嵌套的问题
    • 每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈
    • 当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY
  • 自动释放池与线程一一对应,每个线程都会维护一个自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池
  • 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。

嵌套@autoreleasepool

嵌套的@autoreleasepool其实就是不停的push哨兵对象(POOL_BOUNDARY),在pop时,会先释放里面的,在释放外面的。

举例如下

objc 复制代码
int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();             // print1
    @autoreleasepool { //r1 = push()
        _objc_autoreleasePoolPrint();         // print2
        HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
        HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
        _objc_autoreleasePoolPrint();         // print3
        @autoreleasepool { //r2 = push()
            HTPerson *p3 = [[[HTPerson alloc] init] autorelease];
            _objc_autoreleasePoolPrint();     // print4
            @autoreleasepool { //r3 = push()
                HTPerson *p4 = [[[HTPerson alloc] init] autorelease];
                _objc_autoreleasePoolPrint(); // print5
            } //pop(r3)
            _objc_autoreleasePoolPrint();     // print6
        } //pop(r2)
        _objc_autoreleasePoolPrint();         // print7
    } //pop(r1)
    _objc_autoreleasePoolPrint();             // print8
    return 0;
}

autoreleasePool结构如图所示

Q:Runloop和@autoreleasePool关系

iOSautorelease对象的释放时机是由RunLoop控制的,会在RunLoop每次循环结束时释放。

阅读Runloop源码其实没发现和autoreleasePool有什么关系,但在 App 启动后,苹果在主线程RunLoop里注册了两个Observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler()

  • 第一个Observer监视的事件
    • Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。其order-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  • 第二个Observer监视了两个事件
    • BeforeWaiting(准备进入休眠)时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;
    • Exit(即 将退出 Loop)时调用 _objc_autoreleasePoolPop()来释放自动释放池。
    • 这个Observerorder2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

举例验证

objc 复制代码
__weak id reference = nil;
- (void)viewDidLoad {
    [super viewDidLoad];    
    // str是一个autorelease对象,设置一个weak的引用来观察它
    NSString *str = [NSString stringWithFormat:@"sunnyxx"];    
    reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];    
    NSLog(@"%@", reference); // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];    
    NSLog(@"%@", reference); // Console: (null)
}

viewDidLoadviewWillAppear是在同一个runloop循环下调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。

当然,我们也可以手动干预autorelease对象的释放时机:

objc 复制代码
- (void)viewDidLoad
{
    [super viewDidLoad];
    @autoreleasepool {        
        NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    }    
    NSLog(@"%@", str); // Console: (null)

这样出了@autoreleasepool作用域,内部的autorelease对象就被释放了。

Q:main函数的@autoreleasepool和主线程runloop的@autoreleasepool关系?

Xcode新旧版本main函数里面的@autoreleasepool管理的作用域?

Xcode 新版本

objc 复制代码
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

Xcode 旧版本

objc 复制代码
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

如果你的程序使用了AppKitUIKit框架,那么主线程的RunLoop就会在每次事件循环迭代中创建并处理@autoreleasepool。也就是说,应用程序所有autorelease对象的都是由RunLoop创建的@autoreleasepool来管理。而main()函数中的@autoreleasepool只是负责管理它的作用域中的autorelease对象。

旧版本 Xcode 中 main 函数的@autoreleasepool

Xcode 旧版本的main函数中是将整个应用程序运行(UIApplicationMain)放在@autoreleasepool内,而主线程的RunLoop就是在UIApplicationMain中创建,所以RunLoop创建的@autoreleasepool是嵌套在main函数的@autoreleasepool内的。RunLoop会在每次事件循环中对自动释放池进行poppush,但是它的pop只会释放掉它的POOL_BOUNDARY之后的对象,它并不会影响到外层main函数中@autoreleasepool

新版本 Xcode11 以后的 main 函数发生了哪些变化?

  • 旧版本是将整个应用程序运行放在@autoreleasepool内,由于RunLoop的存在,要return即程序结束后@autoreleasepool作用域才会结束,这意味着程序结束后main函数中的@autoreleasepool中的autorelease对象才会释放。
  • 而在 Xcode 11 中,触发主线程RunLoopUIApplicationMain函数放在了@autoreleasepool外面,这可以保证@autoreleasepool中的autorelease对象在程序启动后立即释放。正如新版本的@autoreleasepool中的注释所写 "Setup code that might create autoreleased objects goes here."(如上代码),可以将autorelease对象放在此处。

Q:什么时候需要手动添加@autoreleasepool?

AppKit 和 UIKit 框架会在RunLoop每次事件循环迭代中创建并处理@autoreleasepool,因此,你通常不必自己创建@autoreleasepool,甚至不需要知道创建@autoreleasepool的代码怎么写。但是,有些情况需要自己创建@autoreleasepool

例如,如果我们需要在循环中创建了很多临时的autorelease对象,则手动添加@autoreleasepool来管理这些对象可以很大程度地减少内存峰值。比如在for循环中alloc图片数据等内存消耗较大的场景,需要手动添加@autoreleasepool

苹果给出了三种需要手动添加@autoreleasepool的情况:

  • ① 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  • ② 如果你编写的循环中创建了大量的临时对象;
    你可以在循环内使用@autoreleasepool在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool有助于减少应用程序的最大内存占用。
  • ③ 如果你创建了辅助线程。
    一旦线程开始执行,就必须创建自己的@autoreleasepool;否则,你的应用程序将存在内存泄漏。

参考博客

iOS底层-内存分区与布局
iOS概念攻坚之路(三):内存管理
iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-一
iOS - 老生常谈内存管理(四):内存管理方法源码分析
iOS - 老生常谈内存管理(三):ARC 面世
iOS - 聊聊 autorelease 和 @autoreleasepool

相关推荐
Digitally33 分钟前
如何用4 种可靠的方法更换 iPhone(2025 年指南)
ios·iphone
9765033354 小时前
iOS 审核 cocos 4.3a【苹果机审的“分层阈值”设计】
flutter·游戏·unity·ios
I烟雨云渊T4 小时前
iOS Alamofire库的使用
ios
程序员老刘·4 小时前
iOS 26 beta1 真机无法执行hot reload
flutter·ios·跨平台开发·客户端开发
EndingCoder4 小时前
React Native 构建与打包发布(iOS + Android)
android·react native·ios
程序员小刘5 小时前
HarmonyOS 5鸿蒙多端编译实战:从Android/iOS到HarmonyOS 5 的跨端迁移指南详
android·ios·华为·harmonyos
I烟雨云渊T5 小时前
iOS swiftUI的实用举例
ios·swiftui·swift
getapi6 小时前
将 App 安装到 iPhone 真机上测试
ios·iphone
90后的晨仔20 小时前
RxSwift 中的 `Single`:单元素响应式编程简单实践
ios
二流小码农20 小时前
鸿蒙开发:CodeGenie万能卡片生成
android·ios·harmonyos