OC底层原理三十七:内存管理(autorelease & runloop)

OC底层原理 学习大纲

  • 👉 上一节 ,详细介绍了weak、strong、强引用的解决方案。本节,我们将介绍:
  1. autorelease自动释放池
  2. runloop

准备工作:


1.autorelease自动释放池

  • autorelease自动释放池: 自动管理作用域内对象引用计数的池子。

面试题1:临时变量什么时候释放?
面试题2:简述自动释放池原理
面试题3:自动释放池能否嵌套使用?

1.1 初探autorelease

  • APP的入口函数main,包含了@autoreleasepool:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}
  • 使用clang将main.m编译后输出main.cpp,在cpp文件中,可以看到:
    image.png
image.png
  1. @autoreleasepool在编译期转化为了__AtAutoreleasePool结构体。
  2. __AtAutoreleasePool的构造函数创建了自动释放池对象
  3. __AtAutoreleasePool的析构函数释放了自动释放池对象
  • 仿__AtAutoreleasePool实现一个构造和析构函数,观察生命周期:
    image.png

利用结构体的构造和析构函数,有效的匹配作用域。

1.2 源码分析

  • 定位源码: (libobjc库)
  • 在main.m文件的@autoreleasepool处加上断点,打开汇编模式,运行代码:

    image.png

  • 加入objc_autoreleasePoolPush符号断点,运行代码,发现源码在libobjc库

    image.png

  • 打开objc4源码,搜索objc_autoreleasePoolPush:
    image.png
1.2.1 自动释放池结构
image.png
image.png
1.2.2 push自动释放池
image.png
1.2.3 pop自动释放池
image.png

1.3 代码验证:

必须在MRC环境下,才可以使用autorelease

image.png

#import <objc/runtime.h>
#import <malloc/malloc.h>

// 声明外部实现
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        for (int i = 0 ; i<505; i++) {
            NSObject * objc = [[NSObject alloc] autorelease];
        }
        // 打印自动释放池的结构信息
        _objc_autoreleasePoolPrint();
    }
    return 0;
}
  • 打印结果:


    image.png

    image.png
  • 数据较多,只截取了第一页开头和第二页数据

1.4 autorelease的嵌套

#import <objc/runtime.h>
#import <malloc/malloc.h>

// 声明外部实现
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        for (int i = 0 ; i<5; i++) {
            NSObject * objc = [[NSObject alloc] autorelease];
        }
        
        @autoreleasepool {
            for (int i = 0 ; i<3; i++) {
                NSObject * objc = [[NSObject alloc] autorelease];
            }
            // 打印自动释放池的结构信息
            _objc_autoreleasePoolPrint();
            printf("\n-----------------------------\n");
        }
        // 打印自动释放池的结构信息
        _objc_autoreleasePoolPrint();
    }
    return 0;
}
image.png
  • autoRelease的嵌套,并没有在结构上进行嵌套。而是利用哨兵的作用,直接多插入一个哨兵。
  • 因为每次遇到哨兵(pop出栈时),都表示一个autorelease可释放

2. runloop

【前言】
关于runloop,我看了一些资料,越看越把我看晕。 停下来稍微理一下。
(ps:runloop版本号是我虚构的,辅助理解)

  • 【使命】runloop的官方文档是在thread线程板块中,他只是线程的一个辅助方式。

    很简单的理解:一个线程,我们创建后,执行完任务,它就释放了。 那每次使用,我都这样创建->执行->释放,岂不是很麻烦?

    我也不知道我啥时候会用到,我就希望有一个线程,在我需要在这个线程执行任务的时候,直接把任务丢过去就可以了。很开心,runloop满足你。

  • 【实现】runloop,顾名思义,就是一个(run)运行(loop)循环。它的作用上面说了,就是让线程一直保持可用状态(保活),如何保持一直在线?

    第一想法是,给个do-while(1)循环,循环内可以接受外部函数,我们每次要执行任务时,给他一个函数就可以了。聪明!runloop1.0版本已经被你开发了👍。

  • 【小结】runloop,本身就是一个函数,函数内创建do-while循环,一直持有着当前线程(在哪个线程调用它,它就一直持有着哪个线程)。

  • 【优化】按照上面使用do-while(1)循环,我们会发现cpu在激增,因为它一直在运行。那有人就有想法了,能不能我需要的时候它就运行,我不用的时候,它就休息,不要占用我的cpu资源。我真不想要这个线程的时候,线程和循环都给我销毁。可以不可以呀?

    要求挺多呀,但挺合理的。😂 于是runloop2.0版本满足你😃。

    【第一个要求:支持销毁】
    runloop给do-while加上条件,你不要了就把这条件设置为不满足就OK啦。

    【第二个要求:支持休眠和唤醒】

    首先,runloop得知道它到底还有没有没干完的活。怎么界定呢? 简单,runloop列个业务清单,打今儿起,我就只接这几种业务,做完了会回调你。(像不像去抽血,1,2,3号抽血,请在4,5,6窗口等结果。血液检测完了,你就可以拿到结果了 😂)。 如果所有业务都处理完了,就进入休眠状态(没活了可以玩会手机,休息下)

    那啥时候唤醒?怎么被唤醒呢?

    runloop直接使用系统内核的mach port的消息机制mach_msg(),当接收业务时,系统会(通过source1)直接唤醒runloop,去执行现在当前接收到的任务。
    每一次循环,都会查询活有没有干完,有没有其他活在排队。没有了就休息。收到系统消息就接着干活。

总之,目前我就是这么理解runloop的,总结一下:

  • 作用:为线程保活(所以一个线程一个runloop,一一对应)
  • 实现:线程内的一个函数,弄个do-while循环让这个线程一直在线。可以处理几类事务并回调处理结果。支持休眠和被唤醒,也支持销毁。

相关链接:
👉 RunLoop 官方文档
👉 逻辑教育kody老师的公开课
👉 ibireme大神的Runloop分析
👉 RunLoop 源码阅读

  • 至此,我想你内心对runloop已经有了一个大体认知。
    (一开始就一头扎进源码的我,可没这么幸运😂)

  • 现在,我们来正式了解runloop:

2.1 runloop是什么

runloop:

    1. 使用一个循环,保持程序的持续运行;
    1. 一个线程对应一个runloop,负责处理APP中各种事件(触摸、定时器、performSelector)
    1. 节省cpu资源。(无任务时自动休眠,被唤醒后继续工作)
  • 经典runloop流程图:


    image.png

2.1.1 runloop的循环

  • 简单循环案例,会占用cpu。

    image.png

  • runloop的循环,闲时不会占用cpu。
    (app启动就会启动主线程,主线程内就维持着一个runloop,一直给程序保活。)

    image.png

  • 我们下载runloop源码,将CFRunLoop.c和CFRunLoop.h文件拖入demo文件夹,搜索void CFRunloopRun:

    image.png

  • 发现runloop的[do-while]循环在stop或finished时,会结束。

ps: 验证了runloop1.0到2.0的过渡😃

2.2 runloop线程保活

上面我们说了,一个runloop对应一个线程,作用就是线程保活:

2.2.1 原始线程(用完即销毁):
//MARK: - HTThread
// 继承NSThread,为了打印dealloc - 线程释放
@interface HTThread : NSThread
@end
@implementation HTThread
-(void)dealloc{ NSLog(@"%@ %s",[HTThread currentThread].name,__func__); }
@end

//MARK: - ViewController
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 调用完,直接销毁
    HTThread * thread = [[HTThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    thread.name = @"ht_Thread";
    [thread start];
}

- (void)threadTest {
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);
}
@end
image.png
  • 可以看到,名为ht_thread的线程,执行完任务(threadTest函数)后,就被销毁了。
2.2.2 runloop线程保活:
@interface HTThread : NSThread
@end
@implementation HTThread
-(void)dealloc{ NSLog(@"%@ %s",[HTThread currentThread].name,__func__); }
@end

//MARK: -ViewController
@interface ViewController ()
@property(nonatomic, strong) HTThread * thread;
@property(nonatomic, strong) NSRunLoop * runloop; // 常驻线程

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createThread];
}

- (void)createThread {
    _thread = [[HTThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    _thread.name = @"ht_Thread";
    [_thread start];
}

- (void)threadTest {
    
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);

    // @autoreleasepool 对子线程中的临时变量做优化管理。更高效利用空间
    @autoreleasepool {
        // 使用runloop对当前线程保活(当前`threadTest`函数是在`ht_Thread`线程内执行)
        _runloop = [NSRunLoop currentRunLoop];
        [_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 处理主线程,其他线程都需要手动开启runloop (run内绑定了线程与runloop的关系)
        [_runloop run];
    }
    
    // runloop没被释放,就到不来这一行。threadTest函数也一直不会结束
    NSLog(@"runloop释放了 %@ %s",[NSThread currentThread].name, __func__);
    
}

- (void)threadTask {
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 点击,让_thread线程执行任务(`threadTask`函数)
    // 如果疑惑_thread是被self强持有,本身就可执行的话。 可手动注释`@autoreleasepool`内部代码。再点击检验。会发现崩溃了。
    // 因为虽然_thread在,但是没法让它为你工作,runloop是可以帮你管理_thread并让它为你工作的。
    [self performSelector:@selector(threadTask) onThread:_thread withObject:nil waitUntilDone:YES];
}

@end
  • runloop成功的实现了线程保活。(我想用的时候,都可以用)
    image.png

2.3 runloop的读取

  • runloop的读取,支持2种方式:
    主线程获取(CFRunLoopGetMain) 和 当前线程获取(CFRunLoopGetCurre)。内部调用_CFRunLoopGet0函数。
    image.png

总结:

  1. 主线程是static全局唯一的,第一次获取时创建。
  2. 线程不存在,默认使用主线程,并返回主线程的runloop
  3. 首次访问,会创建全局唯一的__CFRunLoops字典。key为线程,value为runloop。
    (线程与runloop是一一对应)
  4. 每次优先从__CFRunLoops字典中,通过key(线程),获取value(runloop)。
  5. 如果runloop 不存在,就创建线程对应的runloop,并更新__CFRunLoops字典对应值。
  6. 更新TSD(线程私有存储),记录runloop。
  7. 返回runloop

面试题: runLoop与线程的关系
一一对应关系。由全局Runloop字典进行记录,其中key为线程,value为runloop。

2.4 runloop的创建

image.png

总结:

  1. 以__CFRunLoop为模板,创建Runloop结构体对象
  2. 属性的初始化赋值
  3. Mode的获取:
    • 如果通过__kCFRunLoopModeTypeID读取到Modes,并且 Modes中存在kCFRunLoopDefaultMode,就直接返回找到的Mode。
    • 否则,创建一个Mode,加入modes中。返回Mode。

拓展:

  1. runLoop本质是__CFRunLoop格式的结构体。

    记录线程锁,port唤醒端口,所在线程、所有标记为Common的Mode、加入CommonMode的item事务、当前Mode和所有Mode

image.png
  1. 理解commonModes和modes:
    image.png

2.5 runloop的运行原理

image.png
  • 看完源码后,runloop的运行周期,唤醒方式都十分清晰了。现在奉上经典的Runloop流程图:
    image.png

补充说明:

  1. 【最外层流程】
    kCFRunLoopEntry进入循环 (发通知)
    -> __CFRunLoopRun 运行循环
    -> kCFRunLoopExit退出循环(发通知)

  2. 【循环内部】
    kCFRunLoopBeforeTimers即将处理Timer (发通知)
    -> kCFRunLoopBeforeSources 即将处理Sources0 (发通知)
    (__CFRunLoopDoBlocks 处理Blocks)
    -> __CFRunLoopDoSources0 处理Sources0
    (__CFRunLoopDoBlocks 处理Blocks)
    -> __CFRunLoopServiceMachPort : 监听Port端口消息(source1),有消息就跳转handle_msg
    -> kCFRunLoopBeforeWaiting: 将进入休眠 (发通知)
    进入休眠,等待唤醒 (内部的Timer到期、gcd都可唤醒)
    -> 线程被唤醒, (发通知)

3.【handle_msg】处理消息:

  • 被Timers唤醒(CFRUNLOOP_WAKEUP_FOR_TIMER): __CFRunLoopDoTimers (发通知)
  • 被gcd唤醒(CFRUNLOOP_WAKEUP_FOR_DISPATCH):__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ (发通知)
  • 被source唤醒(CFRUNLOOP_WAKEUP_FOR_SOURCE): __CFRunLoopDoSource1
    (__CFRunLoopDoBlocks 处理Blocks)
  • 检查stop或finish

Timer、dispatch、source等回调函数:

// main  dispatch queue
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

// __CFRunLoopDoObservers
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

// __CFRunLoopDoBlocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

// __CFRunLoopDoSources0
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

// __CFRunLoopDoSource1
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

// __CFRunLoopDoTimers
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

在执行回调Block前,我们可以在堆栈中看到上述回调函数。

  • 回调函数检验:
    (每次触发TouchBegin时,所在线程的runloop都会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数)
    image.png

至此,完成了runloop的基础探索。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容