底层八:内存管理

403 阅读8分钟

定时器

CADisplayLink、NSTimer使用注意

强引用问题

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 也会产生强引用
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    
    // 使用weakSelf还会产生强引用,本质上还是传递指针过去,内部会通过强指针获取,NSTimer内部
    // 是保存强指针或者弱指针,跟外部传入内容无关
    __weak typeof(self) weakself = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakself selector:@selector(timerTest) userInfo:nil repeats:YES];
    
    // 也会产生循环引用(并没有block方案)
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    
}

解决方案1 : 使用block

 __weak typeof(self) weakself = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakself timerTest];
    }];

解决方案2 : NSProxy

NSProxy是跟NSObject同一级别,都是作为基类,NSProxy是专门做消息转发的类

截屏2021-04-12 下午2.39.38.png

截屏2021-04-12 下午3.12.00.png

NSProxy使用


// ViewController.m

#import "ViewController.h"
#import "MJProxy.h"
#import "MJProxy1.h"

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}

@end


// MJProxy.h

#import <Foundation/Foundation.h>

@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

// MJProxy.m

#import "MJProxy.h"

@implementation MJProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    MJProxy *proxy = [MJProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end

NSProxy 做转发效率更高,首先查看自己的类对象,如果类对象没有,直接会转发(不会去父类查找)

GCD 定时器

使用GCD原因CADisplayLinkNSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。 而GCD的定时器会更加准时(GCD 定时器是系统内核级的)

- (void)test
{
    
    // 队列
    //    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_queue_t queue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
    
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 设置时间
    uint64_t start = 2.0; // 2秒后开始执行
    uint64_t interval = 1.0; // 每隔1秒执行
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);
    
    // 设置回调
    //    dispatch_source_set_event_handler(timer, ^{
    //        NSLog(@"1111");
    //    });
    dispatch_source_set_event_handler_f(timer, timerFire);
    
    // 启动定时器
    dispatch_resume(timer);
    // 控制器内要强引用报活
    self.timer = timer;
}

void timerFire(void *param)
{
    NSLog(@"2222 - %@", [NSThread currentThread]);
}

封装一个GCD定时器

// .h

#import <Foundation/Foundation.h>

@interface MJTimer : NSObject

+ (NSString *)execTask:(void(^)(void))task
           start:(NSTimeInterval)start
        interval:(NSTimeInterval)interval
         repeats:(BOOL)repeats
           async:(BOOL)async;

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async;

+ (void)cancelTask:(NSString *)name;

@end


// .m
#import "MJTimer.h"

@implementation MJTimer

static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        timers_ = [NSMutableDictionary dictionary];
        semaphore_ = dispatch_semaphore_create(1);
    });
}

+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
    
    // 队列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 设置时间
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);
    
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    // 定时器的唯一标识
    NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
    // 存放到字典中
    timers_[name] = timer;
    dispatch_semaphore_signal(semaphore_);
    
    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        task();
        
        if (!repeats) { // 不重复的任务
            [self cancelTask:name];
        }
    });
    
    // 启动定时器
    dispatch_resume(timer);
    
    return name;
}

+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!target || !selector) return nil;
    
    return [self execTask:^{
        if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [target performSelector:selector];
#pragma clang diagnostic pop
        }
    } start:start interval:interval repeats:repeats async:async];
}

+ (void)cancelTask:(NSString *)name
{
    if (name.length == 0) return;
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    
    dispatch_source_t timer = timers_[name];
    if (timer) {
        dispatch_source_cancel(timer);
        [timers_ removeObjectForKey:name];
    }

    dispatch_semaphore_signal(semaphore_);
}

@end

iOS程序的内存布局

截屏2021-04-12 下午2.41.02.png

TagedPointer

1.64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumberNSDateNSString等小对象的存储

2. 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

3. 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中 
    * Tag 用来标记类型(NSNumberNSStringNSDate4. 当指针(8字节)不够存储数据时,才会使用动态分配内存的方式来存储数据

5. objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

6. 如何判断一个指针是否为Tagged Pointer?
    * iOS平台,最高有效位是1(第64bit)
    * Mac平台,最低有效位是1
 

截屏2021-04-12 下午4.50.54.png

判断是否为TagedPointer
// 如果是iOS平台(指针的最高有效位是1,就是Tagged Pointer)
#   define _OBJC_TAG_MASK (1UL<<63)

//// 如果是Mac平台(指针的最低有效位是1,就是Tagged Pointer)
//#   define _OBJC_TAG_MASK 1UL

BOOL isTaggedPointer(id pointer)
{
    return ((uintptr_t)pointer & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

        NSNumber *number1 = @4;
        NSNumber *number2 = @5;
        NSNumber *number3 = @(0xFFFFFFFFFFFFFFF);
        
        NSLog(@"%d %d %d", isTaggedPointer(number1), isTaggedPointer(number2), isTaggedPointer(number3));
        NSLog(@"%p %p %p", number1, number2, number3);


TagedPointer面试题

思考以下2段代码能发生什么事?有什么区别? 长字符串(存储到堆)

  dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            // 加锁
            self.name = [NSString stringWithFormat:@"abcdefghijk"];
            // 解锁
        });
    }
    // 崩溃 , 解决办法是加锁

原因: 因为如果字符串足够长,会存储到堆,使用set方法赋值过程如下


//ARC的代码会转换到MRC执行, 因为不同线程同时进入,可能导致[_name release]多次执行,导致崩溃
- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

短字符串(tagPointer机制)

 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }
    // 正常运行

在字符串较短的情况下使用TagedPointer技术,TagedPointer使用指针直接赋值的形式,不通过set方法,不会崩溃

OC对象的内存管理

1. 在iOS中,使用引用计数来管理OC对象的内存

2. 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

3. 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

4. 内存管理的经验总结
    * 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
    * 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1

可以通过以下私有函数来查看自动释放池的情况

// 这是C语言的一个语法格式,可以只这样生命就可以调用方法,可以在控制台中打印出内存的应用
extern void _objc_autoreleasePoolPrint(void);

copy

copy本质是相互之间不影响

深拷贝: 产生新的对象

浅拷贝: 不产生新的对象

可变对象: copymutableCopy都是深拷贝

不可变对象: copy是浅拷贝、 mutableCopy是深拷贝

copymutableCopy
可变深拷贝深拷贝
不可变浅拷贝深拷贝

copy 修饰


- (void)setData:(NSArray*)data {
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}

- (void)dealloc {
    self.data = nil;
    [super dealloc];
}

@property (copy, nonatomoc) NSMutableArray *data 因为是copy修饰,set方法会执行[data copy]生成一个不可变数组,与定义相悖,在使用中容易出错,不要这样定义

copy 修饰字符串(NSString),目的是字符串传入可变字符串之后,字符串对象仍然不可改变

延伸: strong 修饰

- (void)setData:(NSArray*)data {
    if (_data != data) {
        [_data release];
        _data = [data retain];
    }
}

- (void)dealloc {
    self.data = nil;
    [super dealloc];
}

引用计数的存储

在64bit中, 引用计数存储在isa共用体里面,如果比较多(超过19位)就存储到sideTable类里面

截屏2021-04-12 下午2.41.50.png

inline uintptr_t 
objc_object::rootRetainCount()
{
    // TaggedPointer 不是OC对象,是一个指针,没有引用计数
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = __c11_atomic_load(&isa.bits, __ATOMIC_RELAXED);
    // 优化过的isa指针
    if (bits.nonpointer) {
        uintptr_t rc = bits.extra_rc;
        if (bits.has_sidetable_rc) { // 引用计数不只是存储在isa指针中的,而是存储在SideTable中
            // 从sidetable中拿出数据,相加
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

weak指针的实现原理

初始化时候runtime调用 id objc_initWeak(id *location, id newObj) 函数

// 1.  locationg: 弱指针地址   newObj 弱指针指向对象
id objc_initWeak(id *location, id newObj) 
// 2.  存表(注销旧的表,创建新的表),更新指针的指向到新的表
static id storeWeak(id *location, objc_object *newObj)

清除表的细节

inline void objc_object::clearDeallocating()
{
    // 判断是不是普通的isa指针
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    //  不是普通的isa指针(是共用体,走下面这个函数)
    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();
    }

    assert(!sidetable_present());
}

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(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

弱引用表也是采用散列表的形式保存的。存放在sideTable(上图)weak_table

将弱引用存放到散列表中,在对象销毁时(dealloc),取出弱引用表,将弱引用清除掉

截屏2021-04-12 下午2.42.25.png

inline void 
objc_object::clearDeallocating()
{
    ..........
         clearDeallocating_slow();
    ..........
}

void
objc_object::clearDeallocating_slow()
{
    ..............
    // 拿到SideTable 中的弱引用表
    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    ..............
    table.unlock();
}

weak_clear_no_lock :根据对象地址,查找到弱引用表中存储的弱引用对象(__weak 修饰的对象),判断对象引用计数为0,将对象设置为nil

MRC

release / autorelease

自动释放池(autorelease)

为了看清本质,首先进行代码转换

 @autoreleasepool {
        JXPerson *person = [[[JXPerson alloc] init] autorelease];
 }

转化CPP代码如下,

{
    __AtAutoreleasePool __autoreleasepool;
    /// 这里放上执行代码
    JXPerson *person = [[[JXPerson alloc] init] autorelease];
}

可以看出有一个 __AtAutoreleasePool 结构体.拿到相关代码可以看出一个构造函数和一个析构函数

struct __AtAutoreleasePool {

    void * atautoreleasepoolobj;

    __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    
    ~__AtAutoreleasePool() {
                        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    
    
};

经转化后分析之前的代码,可以得到一个执行顺序

     atautoreleasepoolobj = objc_autoreleasePoolPush();

    // 业务代码
    JXPerson *person = [[[JXPerson alloc] init] autorelease];

    objc_autoreleasePoolPop(atautoreleasepoolobj);

为了弄明白autoreleasePool的本质,就转变为研究 objc_autoreleasePoolPushobjc_autoreleasePoolPop 的底层实现。

我们可以在runtime源码中,找到NSobject.mm文件,里面有对应的实现。 通过查询源码我们能发现自动释放池的管理由一个AutoreleasePoolPage进行管理。

总结:

  1. 自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage
  2. 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
  3. 源码分析
    • clang重写@autoreleasepool
    • objc4源码:NSObject.mm

截屏2021-04-12 下午2.42.58.png

AutoreleasePoolPage的结构

  1. 每个AutoreleasePoolPage对象占用4096(0x1000)16的三次方 字节内存,除了拿出56(0x38)个字节用来存放它内部的成员变量 ,剩下的空间用来存放autorelease对象的地址

1.1 AutoreleasePoolPage 代码内有begin()end()函数.通过代码可以看出begin是当前对象内存开始 + 当前对象内部成员占用的内存大小 ,也就是存储的autorelease对象 开始的地址,而end值得是当前对象的结束地址

id* begin() {
    return this+sizeof(*this));
}
id* end() {
// SIZE 是宏定义的 4096
    return this+SIZE;
}
  1. 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。 通过child 找寻自己的下一级链表,通过parent找寻自己的上一级链表

关于双向链表 就是如下:

截屏2021-04-12 下午2.43.48.png

代码中定义#define POOL_BOUNDARY nil

  1. 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
  2. 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
  3. id *next指向了下一个能存放autorelease对象地址的区域,开始什么都没有存放的时候,就指向begin() 即0x1038函数 的下一个位置,后来POOL_BOUNDARY入栈 就指向 POOL_BOUNDARY 的下一个位置,当第一个person入栈,就指向第一个person的下一个位置

总结AutoreleasePoolPage本身是一个栈结构,共4096个字节,其中栈底的56个字节存储自己的一些属性,剩下的4040个字节用来存储需要释放的对象(被autorelease修饰的对象),开始push对象前,会先push一个POOL_BOUNDARY (nil的宏定义)并记录这个位置,类似于一个标记,在出栈时候,会从栈顶开始release,遇到POOL_BOUNDARY停止,当存储不够的时候,会采用双向链表的结构,进行多page存储

autoReleasePool嵌套

 @autoreleasepool { // r1 = push()
        JXPerson *person0 = [[[JXPerson alloc] init] autorelease];
        @autoreleasepool {// r2 = push()
            JXPerson *person1 = [[[JXPerson alloc] init] autorelease];
            JXPerson *person2 = [[[JXPerson alloc] init] autorelease];
            @autoreleasepool {// r3 = push()
                JXPerson *person3 = [[[JXPerson alloc] init] autorelease];
            }// pop(r3)
        }// pop(r2)
    }// pop(r1)

在内存中存储结构如下

AutoreleasePoolPage
magic_t magic
id* next
pthread_t thread
AutoreleasePoolPage *parent
AutoreleasePoolPage *child
uint32_t depth
uint32_t hiwat
POOL_BOUNDARY
person0
POOL_BOUNDARY
person1
person2
POOL_BOUNDARY
person3

之后释放的时候,先从person3开始释放,依次释放person2、person1、person0

可以通过extern 声明void _objc_autoreleasePoolPrint(void);这个内部函数,查看AutoreleasePoolPage 的释放情况

@autoreleasepool { //  r1 = push()
        //_objc_autoreleasePoolPrint();
        MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
        MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
        
        //  _objc_autoreleasePoolPrint();
        @autoreleasepool { // r2 = push()
            for (int i = 0; i < 600; i++) {
                MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
            }
            @autoreleasepool { // r3 = push()
                MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
                _objc_autoreleasePoolPrint();
            } // pop(r3)
        } // pop(r2)
    } // pop(r1)
    

RunLoop 和 autoRelease

问题: autoRelease 修饰的对象在什么时机会被释放?

iOS 在主线程注册了两个autoRelease相关的observer,用来观察runLoop的状态


 activities = 0x1(1)  kCFRunLoopEntry
 监听到 kCFRunLoopEntry 调用 `objc_autoreleasePoolPush()`
 
 <CFRunLoopObserver 0x60000013e8c0 [0x101987c80]> {
 ... ...
    callout = _wrapRunLoopWithAutoreleasePoolHandler (0x101b35d92)
 ... ...
 }

 activities = 0xa0(160)      kCFRunLoopExit | kCFRunLoopBeforeWaiting(休眠之前)
 
 监听到 kCFRunLoopBeforeWaiting  调用`objc_autoreleasePoolPop()` 再调用 `objc_autoreleasePoolPush()`
 监听到 kCFRunLoopExit  调用`objc_autoreleasePoolPop()`
 
<CFRunLoopObserver 0x60000013e6e0 [0x101987c80]>{
 ... ...
    callout = _wrapRunLoopWithAutoreleasePoolHandler (0x101b35d92)
 ... ...
}
#import "ViewController.h"
#import "MJPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 这个Person什么时候调用release,是由RunLoop来控制的
    // 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
//    MJPerson *person = [[[MJPerson alloc] init] autorelease];
    
    MJPerson *person = [[MJPerson alloc] init];
    
    NSLog(@"%@",[NSRunLoop mainRunLoop]);
    
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    NSLog(@"%s", __func__);
}

打印[NSRunLoop mainRunLoop]可以看出里面有两个observer,看activities属性,可以看出执行的顺序,看出对象的释放时机

/*
 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
 kCFRunLoopEntry = (1UL << 0),  1
 kCFRunLoopBeforeTimers = (1UL << 1), 2
 kCFRunLoopBeforeSources = (1UL << 2), 4
 kCFRunLoopBeforeWaiting = (1UL << 5), 32
 kCFRunLoopAfterWaiting = (1UL << 6), 64
 kCFRunLoopExit = (1UL << 7), 128
 kCFRunLoopAllActivities = 0x0FFFFFFFU
 };
 */

/*
 kCFRunLoopEntry  push
 
 <CFRunLoopObserver 0x60000013f220 [0x1031c8c80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = <CFArray 0x60000025aa00 [0x1031c8c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
 
 
 kCFRunLoopBeforeWaiting | kCFRunLoopExit
 kCFRunLoopBeforeWaiting pop、push
 kCFRunLoopExit pop
 
 <CFRunLoopObserver 0x60000013f0e0 [0x1031c8c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = <CFArray 0x60000025aa00 [0x1031c8c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
 */

iOS在主线程的Runloop中注册了2个Observer

  1. 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
  2. 第2个Observer
    1. 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
    2. 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()