分类 Objective-C 下的文章

本文主要讲述以下几个问题:

  1. 什么是block
  2. block有多少种类型
  3. block如何捕获外部变量
  4. 循环引用如何解决
  5. block copy 的不同结果
  6. __block修饰的变量在底层是什么样的

一、 block是什么

查看c++代码

在 xcode 中生成两个文件:TestBlock.h 、TestBlock.m 。其中 TestBlock.m 添加如下代码:

-(void)blockFunc {
    int n = 10;
    int (^myBlock)(int m) = ^(int m){
        return n + m ;
    };
    NSLog(@"%d",myBlock(1));
}

然后通过 clang -rewrite-objc TestBlock.m 命令将 TestBlock.m 转换出 TestBlock.cpp 文件,该文件中是底层如何实现 block 的c++代码。
我们看到,blockFunc 方法在底层代码中变成了 _I_TestBlock_blockFunc 函数:

static void _I_TestBlock_blockFunc(TestBlock * self, SEL _cmd) {
    int n = 10;
    //定义 myBlock
    int (*myBlock)(int m) = ((int (*)(int))&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, n));
    //调用 myBlock
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}

再看 __TestBlock__blockFunc_block_impl_0 的定义:

struct __TestBlock__blockFunc_block_impl_0 {
  struct __block_impl impl;  //block 主体
  struct __TestBlock__blockFunc_block_desc_0* Desc;  //block描述
  int n;  //block 捕获的外部变量 n
  //构造函数
  __TestBlock__blockFunc_block_impl_0(void *fp, struct __TestBlock__blockFunc_block_desc_0 *desc, int _n, int flags=0) : n(_n) {
    impl.isa = &_NSConcreteStackBlock;   //初始化为栈 block
    impl.Flags = flags;
    impl.FuncPtr = fp;  //block实现的函数地址
    Desc = desc;
  }
};

block的实现部分被封装在 __TestBlock__blockFunc_block_func_0 函数中,通过 __TestBlock__blockFunc_block_impl_0 构造函数传递到 __block_impl 结构体中:

static int __TestBlock__blockFunc_block_func_0(struct __TestBlock__blockFunc_block_impl_0 *__cself, int m) {
    int n = __cself->n; // bound by copy

    return n + m ;
}
struct __block_impl {
  void *isa;  //isa指针表示block的类型: _NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;
  int Reserved;
  void *FuncPtr; //函数指针,__TestBlock__blockFunc_block_func_0 函数
};

在上述代码中,我们发现,myBlock 实质上是 __TestBlock__blockFunc_block_impl_0 结构体的对象。

总结

block 是在底层是一个结构体对象,它封装了 block 的实现代码和捕获的外部变量。

二、block的三种类型

全局block

NSGlobalBlock 类型,位于全局区。
当在 block 内部不使用外部变量,或只是用静态变量和全局变量时,则是全局 block。
示例代码:

static int b = 100;
void (^Block)(void) = ^{
   NSLog(@"== %d",b);
};
NSLog(@"%@",Block);
输出结果为:<__NSGlobalBlock__: 0x10eaca050>

栈block

NSStackBlock 类型,位于栈区。
当使用 __weak 修饰 block 时,被弱引用的 block 没有被 copy 到堆上,则为栈区block。
示例代码:

NSString *str = @"haha";
void (^__weak Block)(void) = ^{
   NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSStackBlock__: 0x7ffee1136168>

堆block

NSMallocBlock 类型,位于堆区。
当 block 内部引用了外部变量,且block被强引用时,则为堆区block
示例代码:

NSString *str = @"haha";
void (^Block)(void) = ^{
   NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSMallocBlock__: 0x60000227ef10>

理解不同类型的 block

示例代码:

-(void)testBlock{
    NSString *str = @"hello";  //对象类型的 str 局部变量
    int num = 10;  //值类型的 num 局部变量
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^__weak myBlock)(void) = ^{
            NSLog(@"num : %d",num);
            NSLog(@"str : %@",str);
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSStackBlock__: 0x7ffeee7b2170>
weakBlock : <__NSStackBlock__: 0x7ffeee7b2170>
num : 10
str : (null)

通过代码与结果分析,我们发现 myBlock 和 weakBlock 都是栈block,并且都指向同一块内存空间 0x7ffeee7b2170 ,而这块内存的生命周期是在 testBlock 函数的作用域之内,即使 myBlock 出了代码块范围,也依然存在于该函数的栈空间中,因此调用 weakBlock() 依然有效。

示例代码:

-(void)testBlock{
    int num = 10;  //值类型的 num 局部变量
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^myBlock)(void) = ^{
            NSLog(@"num : %d",num);
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSMallocBlock__: 0x6000034e55f0>
weakBlock : (null)

程序崩溃:EXC_BAD_ACCESS (code=1, address=0x10)
myBlock 去掉__weak修饰后,变成堆 block,而它的生命周期只在代码块中,出了代码块就被释放掉了,因此再调用weakBlock() 程序崩溃。

示例代码:

-(void)testBlock{
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^myBlock)(void) = ^{
            NSLog(@"hello~");
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSGlobalBlock__: 0x10e4790d0>
weakBlock : <__NSGlobalBlock__: 0x10e4790d0>
hello~

不使用外部变量后,myBlock 变成全局 block,在内存中类似于单例,一直存在直到程序终止,因此调用 weakBlock()有效。

三、捕获外部变量

在实践中发现,block 对不同类型和不同作用域的外部变量捕获方式是不尽相同的,总结起来如下列表格:

变量类型是否捕获捕获内容
局部变量值类型:捕获值 ; 对象类型: 捕获变量地址和修饰符
静态局部变量捕获变量地址
全局变量/静态全局变量不捕获,直接访问

四、解决 block 循环引用的几个方法

循环引用的问题在 iOS 经常遇到:A对象引用了B对象,B对象中又引用了A对象,导致两个对象都无法释放。
循环引用经常发生在 block 中,下面我们来看个例子:

@interface TestObject()

@property(nonatomic,strong) void(^myBlock)(void);

@end

@implementation TestObject

-(void)blockFunc{
    self.myBlock = ^{
        NSLog(@"self : %@",self);
    };
    self.myBlock();
}

在上述代码中,self 持有了 myBlock ,而在 myBlock 中又捕获了 self,因而构成循环引用,下面我们再来看看解决办法。

1、__weak__strong

常用手法,不再赘述。

2、__block

使用 __block 将 block 中的 self 置为nil,从而使得 block 不再持有 self 。

-(void)blockFunc{
    __block TestObject *temp = self;
    self.myBlock = ^{
        NSLog(@"self:%@",temp);
        temp = nil;
    };
    self.myBlock();
}

3、将强引用对象写成 block 的入参

block 的入参不需要被捕获,因此通过参数传入可以使得 block 不对 self 进行持有。

//修改 myBlock 定义,使其可以传入参数
@property(nonatomic,strong) void(^myBlock)(TestObject *obj);
-(void)blockFunc{
    self.myBlock = ^(TestObject *obj){
        NSLog(@"self:%@",obj);
    };
    self.myBlock(self);
}

五、block copy

copy操作对于不同类型 block 的结果是不一样的,总结起来看表格:

block类型copy结果
NSGlobalBlockcopy无效,结果还是同一个block,不会增加引用计数
NSStackBlockcopy 之后变成 NSMallocBlock,得到新内存,类似深拷贝
NSMallocBlockcopy之后还是 NSMallocBlock,但是同一块内存,会增加引用计数,类似浅拷贝

在 MRC 模式下,以下这段代码将生成一个 NSStackBlock,然后通过 copy 变成 NSMallocBlock

int n = 0;
void (^block)(void) = ^{
    NSLog(@"n = %d",n);
};
NSLog(@"b : %@",block);
block = [block copy];
NSLog(@"bb : %@",block);  

在 MRC 模式下输出为:

b : <__NSStackBlock__: 0x7ffee701c158>
bb : <__NSMallocBlock__: 0x60000178b000>

在 ARC模式下输出为:

b : <__NSMallocBlock__: 0x600003854420>
bb : <__NSMallocBlock__: 0x600003854420>

通过上面的比较,我们发现,block 创建时是 NSStackBlock ,而在 ARC 模式下,block创建时就执行了copy操作变成了 NSMallocBlock,并且 NSMallocBlock copy 之后还是同一块内存。

六、__block修饰的变量

__block修饰的变量在底层是一个结构体,我们来看一下代码:

__block int number = 100;
    void (^myBlock)(void) = ^{
        number = 101;
    };
    number = 2;

通过 clang rewrite 命令转换为 c++代码:

// __block number 在底层是一个名为 __Block_byref_number_0 的结构体
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 100};
// myBlock 定义
    void (*myBlock)(void) = ((void (*)())&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));

//block外部修改number的值为 2
    (number.__forwarding->number) = 2;  

__Block_byref_number_0 结构体:

struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

从上述代码中,我们可以看到,在block内部和外部,对 number 的赋值都是通过对 number.__forwarding->number 赋值完成的,那么我们来了解一下其缘由。

__forwarding 指针

还是使用上面 __block int number = 100; 的例子,__forwarding指针是 __Block_byref_number_0 结构体的成员,在栈block中,它指向了__Block_byref_number_0 结构体自身(如图1),当栈block 被 copy 变成堆block时,栈区block中的 __forwarding指针指向了堆区block 的__Block_byref_number_0 结构体,而堆区block 的__forwarding指针还是指向结构体自身(如图2)。
图1:
图1
图2:
图2

通过上述代码,我们看到对 number 复制或访问都是通过__forwarding 指针完成的,这样做的好处是无论是栈block还是堆block,通过__forwarding 指针,都能够正确的访问到 __block 修饰的变量。

总结

1、当没有应用外部变量时是全局Block。
2、当 __weak 修饰Block 时是栈区Block
3、其他情况属于堆区Block
4、Block的本质是一个封装了实现和应用外部变量的结构体
5、__block 修饰的变量会被封装成一个结构体,并且Block结构体中有一个指针始终指向它,所以可以修改变量。

在我们平常工作中,isMemberOfClassisKindOfClass 出现的频次算比较高的,光看文档,我们知道 isMemberOfClass 用来判断一个对象是否为某个类的实例,而 isKindOfClass 用来判断一个对象是否为某个类的实例或者继承自该类的子类的实例。那么在 Objc 的底层是如何实现这一功能的呢,让我们一起来围观一下。

准备

需要一份 Objc 的源码,官方源码)(需要自己踩坑编译)
快速通道(已编译通过,附带demo)。

本文中使用的源码版本为:objc4-818.2 。

运行示例代码

打开编译好的 Ojbc 工程,工程目录如下图:
工程目录:
obj_project.png

如图中所示,我们已经添加了一个新的taget :TestObjc,接下来的实例代码也是写在它的main.m文件中。
在 main.m 文件中添加调试代码:

void testKindOf(){
    BOOL ret1 = [NSObject.class isMemberOfClass:NSObject.class];
    BOOL ret2 = [TestObject.class isMemberOfClass:TestObject.class];
    BOOL ret3 = [[NSObject alloc] isMemberOfClass:NSObject.class];
    BOOL ret4 = [[TestObject alloc] isMemberOfClass:TestObject.class];
    
    NSLog(@"isMemberOfClass== %d - %d - %d - %d",ret1,ret2,ret3,ret4);
    
    BOOL ret5 = [NSObject.class isKindOfClass:NSObject.class];
    BOOL ret6 = [TestObject.class isKindOfClass:TestObject.class];
    BOOL ret7 = [[NSObject alloc] isKindOfClass:NSObject.class];
    BOOL ret8 = [[TestObject alloc] isKindOfClass:TestObject.class];

    NSLog(@"isKindOfClass == %d - %d - %d - %d",ret5,ret6,ret7,ret8);
}

我们再 ret1 处添加一个断点,并在 源码 NSObject.mm 中给 isKindOfClassisMemberOfClass 方法实现中添加断点,然后运行代码。
运行代码之后,我们发现除了类方法 + (BOOL)isMemberOfClass:(Class)cls 和实例方法 - (BOOL)isMemberOfClass:(Class)cls 被调用外,isKindOfClass 的两个方法并没有被调用。why ?

汇编+断点调试

通过勾选 xcode 的菜单栏 Debug -> Debug Workflow -> Always Show Disassembly 开启汇编调试,我们看到了熟悉的函数调用逻辑。
汇编调试:
msg_send_isMemeber.png

如图中所示,程序并没有走 isKindOfClass 方法的实现,而是走了 objc_opt_isKindOfClass 函数,于是我们在源码中搜索找到该函数,并添加断点。而 isMemberOfClass 则通过 objc_msgSend 进行消息转发而被调用的,相对简单一些。

下面我们先来看看 isMemberOfClass 方法的源码实现:


类方法:+isMemberOfClass

当执行 [NSObject.class isMemberOfClass:NSObject.class];
这行代码时,调用的就是这个类方法。

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

方法内部调用了底层 objc_object.hISA() 函数,该函数经过层层调用(看流程图),最终来到 isa_t::getClass() 函数中返回一个 Class ,我们知道 Class 的本质是 objc_class 结构体的指针,因此当"=="运算符的两边都是同一个地址时,运算结果为真。所以我们稍微修改一下代码,然后用 LLDB 的 x 指令进行调试:
调试画面:
class_isMemberOfClass.png

如上图所示,我们看到,指针 acls 的值为:0x10036a0f0 ,而指针cls 的值为: 0x10036a140 ,并不是同一个地址,因此 ret 的值为 NO 。
虽然两个地址值不相同,但他们都指向了同一个地址 0x10036a0f0 ,和 acls 的值相同。而我们通过调用 object_getClass(cls) 方法获取 cls 的元类,发现依然是 0x10036a0f0 。类、元类、根元类 的补充在文章末尾。

当运行到下一行代码 [TestObject.class isMemberOfClass:TestObject.class]; 时,我们再跳入 +isMemberOfClass 方法去调试:
调试画面:
class_isMemberOfClass2.png

我们发现 acls 的值还是 0x100008160 ,而 cls 的值为 0x100008188 ,两者依然不同,但都指向同一个地址 0x10036a0f0 (NSObject)。
综上所述,我们可以判断 self->ISA(); 获取到的是元类。而上述的两行代码都是拿元类与类比较,因此结果都是 NO。

+isMemberOfClass 的流程图:
流程图:
flow1.png


实例方法:-isMemberOfClass
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

先上流程图:
流程图:
IsMemeberFlow.png

通过对比流程图,我们发现-isMemberOfClass 也是调用了底层的 ISA()函数,不同的是返回了对象的类,而不是元类。(可以使用 class_isMetaClass 函数来判断 Class 是否为元类)

[[NSObject alloc] isMemberOfClass:NSObject.class];
[[TestObject alloc] isMemberOfClass:TestObject.class];

因此,在运行这两行代码时,其结果都为 YES 。


接着,我们再转移到 isKindOfClass 方法,它同样有类方法和实例方法,来看看底层是如何实现的。

类方法:+isKindOfClass

在运行 [NSObject.class isKindOfClass:NSObject.class]; 这行代码时,我们使用单步调试发现,代码执行并不会走入 +isKindOfClass方法的实现,而是走了objc_opt_isKindOfClass 函数,这是因为底层对+isKindOfClass方法作了优化,将其IMP指向了objc_opt_isKindOfClass 函数,函数的具体实现如下:

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    Class cls = obj->getIsa();  
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

NSObject.mm文件中+isKindOfClass方法的源码如下:

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

经过对比发现,该方法 Objective-c2.0版本除了代码优化,核心的内容并没有变化。
由于是类调用的 +isKindOfClass,根据上述的内容,可知 getIsa() 函数或者ISA()函数返回的都是元类。由此可见,[A isKindOfClass:B],实际上是获取 A 的元类以及遍历元类的父类,与 B 相比较。而根据类、元类的关系图(见“补充知识点部分”),我们知道元类的最终父类依然是 NSObject ,所以[NSObject.class isKindOfClass:NSObject.class];这行代码的结果就是 YES。

再看下一行代码:[TestObject.class isKindOfClass:TestObject.class];,TestObject 的元类及其父类,都不会与 TestObject 相等,因此结果为 NO。


实例方法:-isKindOfClass

通过单步调试,我们看到 -isKindofClass还是会调用 objc_opt_isKindOfClass 函数,只不过getIsa() 函数或者ISA()函数返回的都是实例对象的类。
那么[a isKindOfClass:B],实际上是获取实例对象a的类及其父类,与B类相比较。所以

[[NSObject alloc] isKindOfClass:NSObject.class];
[[TestObject alloc] isKindOfClass:TestObject.class];

这两行代码结果都是 YES。

补充知识点

类、元类、根元类

Class (类)对于我们来说很熟悉,想要创建实例对象就需要先定义一个类。而在 OC Runtime 中类是一个 objc_class 类型的对象 :typedef struct objc_class *Class;,我们通常称之为类对象 ,这个类对象的类被称为元类(meta class),而元类的类就是根元类,这个根元类就是 NSObject ,他们之间通过 isa 指针来联系起来,如下图:
类、元类、根元类:
isa_flow.png

关于 \_OBJC2_ 宏定义

\_OBJC2_ 是 objective-c 2.0 版本的宏定义,在宏定义的作用域里面的是2.0版本的代码,它是用来兼容iOS低版本系统的,Stack Overflow 有一个很好的解释,如下图:
OBJC2:
objc2_desc.png

Stack Overflow 链接

总结

1、类和实例对象对底层 getIsa() 函数或者ISA()函数 的调用结果是不同的:类调用会返回元类,而对象调用会返回类。
2、对 isKindOfClass类似的底层方法调用,并不一定会直接执行方法的 IMP ,因为底层优化会修改IMP的指向。比如调用 isKindOfClass 会执行 objc_opt_isKindOfClass 函数,而不是走isKindOfClass 的IMP实现。

参考文章

1、《内存管理之Tagged pointer》
2、《iOS标记指针(Tagged Pointer)技术》

autoreleasepool 深度解析

什么是 autoreleasepool

autoreleasepool(自动释放池)是 Objective-C 和 Swift 中用于延迟对象释放的一种内存管理机制。它允许你将对象的释放操作"延迟"到池被销毁时,而不是立即执行。

底层数据结构

自动释放池是一个结构体对象,在底层维护一个以栈为节点的双向链表,它是有所属的线程的。

AutoreleasePoolPage 类,它维护一个栈结构,自动释放池维护多个page为节点的双向链表。

class AutoreleasePoolPage {
        magic_t const magic;
        id *next; // 指向栈的下一个空闲地址
        pthread_t const thread; //所属线程
        AutoreleasePoolPage * const parent; //指向链表前一个节点
        AutoreleasePoolPage *child; //指向链表后一个节点
        uint32_t const depth;
        uint32_t hiwat;
};

AutoreleasePoolPage 类(每个池由多个 Page 组成)

  • 每个 Page 大小为 4096 字节(一页内存)
  • 以双向链表形式连接
  • 包含以下关键部分:

    • magic:校验字段
    • next:指向下一个空闲位置
    • thread:所属线程
    • parentchild:构成双向链表

工作原理

入栈过程(添加对象)

  1. 当对象调用 autorelease 时:

    [obj autorelease]; // 相当于压栈
  2. 系统会获取当前线程的"hot page"(活跃页面)
  3. 将对象地址存入 next 指向的位置,然后 next++

出栈过程(释放对象)

  1. 当池销毁时(objc_autoreleasePoolPop):
  2. 从当前页面的 next 指针开始向前遍历
  3. 对每个对象发送 release 消息
  4. 重置 next 指针位置

多页面管理

当单个页面空间不足时,系统会:

  1. 创建新的 AutoreleasePoolPage 作为 child
  2. 新页面成为新的"hot page"
  3. 形成双向链表结构:
[Page1] <--> [Page2] <--> [Page3]
  ↑             ↑           ↑ 
parent        parent      (最新页面)

为什么说是栈结构?

尽管物理实现是链表,但从行为上看是栈:

  • 压栈autorelease 操作相当于 push
  • 弹栈pool pop 操作相当于批量 pop
  • 嵌套池:外层池在内层池销毁后依然有效

线程局部存储

每个线程有自己独立的自动释放池栈:

  • 通过 thread 字段关联特定线程
  • TLS(Thread Local Storage)存储当前页指针

实际内存布局示例

假设有3个对象被 autorelease:

AutoreleasePoolPage:
+---------------+ 
| magic         |
| next ───────┐ |
| thread      | |
| parent      | |
| child       | |
| depth       | |
| hiwat       | |
+---------------+ 
| obj1        | ← next 最初指向这里
| obj2        |
| obj3        | ← next 现在指向这里
| ...         |

这种设计既保持了栈的逻辑特性,又通过链表实现了动态扩容,是工程实践中经典的"逻辑栈-物理链表"实现方式。

处理大型数据时的作用

处理大型数据(如循环中创建大量临时对象)时,autoreleasepool 可以:

  1. 及时释放内存

    for i in 0..<100000 {
        autoreleasepool {
            let tempImage = processLargeImage(at: i) // 临时大对象
            // 使用tempImage...
        } // 这里tempImage会被立即释放
    }
  2. 避免内存峰值

    • 没有 autoreleasepool:所有临时对象会累积直到循环结束
    • 使用 autoreleasepool:每次迭代后立即释放

实际应用场景

  1. 主线程

    • RunLoop 每次循环会自动创建和销毁自动释放池
    • 所以主线程通常不需要手动管理
  2. 子线程

    DispatchQueue.global().async {
        autoreleasepool {
            // 处理大量数据
            let data = loadHugeData()
            process(data)
        } // 数据在这里被释放
    }
  3. Swift 与 Objective-C 交互

    • 纯 Swift 代码通常不需要 autoreleasepool
    • 但当调用 Objective-C API 返回自动释放对象时仍然有用

性能考虑

  1. 创建代价

    • 创建和销毁池本身有开销
    • 只应在处理大量临时对象时使用
  2. 最佳实践

    // 好:处理大量迭代时使用
    for item in hugeCollection {
        autoreleasepool {
            process(item)
        }
    }
    
    // 不好:单个小对象没必要
    autoreleasepool {
        let smallObject = createObject()
        use(smallObject)
    }

通过理解 autoreleasepool 的底层机制,我们可以在处理内存密集型操作时更精确地控制内存使用,避免不必要的内存增长。