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

相关推荐
2501_9160074713 小时前
iOS App 上架实战 从内测到应用商店发布的全周期流程解析
android·ios·小程序·https·uni-app·iphone·webview
wjm0410061 天前
ios八股文 -- Objective-c
开发语言·ios·objective-c
麦兜*1 天前
Swift + Xcode 开发环境搭建终极指南
开发语言·ios·swiftui·xcode·swift·苹果vision pro·swift5.6.3
Digitally2 天前
重置iPhone会删除所有内容吗? 详细回答
ios·iphone
普罗米拉稀2 天前
Flutter 复用艺术:Mixin 与 Abstract 的架构哲学与线性化解密
flutter·ios·面试
kymjs张涛2 天前
零一开源|前沿技术周刊 #12
ios·google·github
2501_915918412 天前
iOS 应用上架全流程实践,从开发内测到正式发布的多工具组合方案
android·ios·小程序·https·uni-app·iphone·webview
笔沫拾光2 天前
iOS 正式包签名指南
flutter·ios·ios签名
Magnetic_h3 天前
【iOS】锁的原理
笔记·学习·macos·ios·objective-c·cocoa·xcode
Digitally3 天前
将 iPhone 联系人转移到 Infinix 的完整指南
ios·cocoa·iphone