-
👉 上一节 ,详细介绍了
weak、strong、强引用的解决方案。本节,我们将介绍:
- autorelease自动释放池
- runloop
准备工作:
- 可编译的
objc4-781源码: https://www.jianshu.com/p/45dc31d91000- runloop源码: https://opensource.apple.com/tarballs/CF/
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

@autoreleasepool在编译期转化为了__AtAutoreleasePool结构体。__AtAutoreleasePool的构造函数创建了自动释放池对象__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 自动释放池结构


1.2.2 push自动释放池

1.2.3 pop自动释放池

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;
}

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:
- 使用一个
循环,保持程序的持续运行;
一个线程对应一个runloop,负责处理APP中各种事件(触摸、定时器、performSelector)
节省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

- 可以看到,名为
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
总结:
主线程是static全局唯一的,第一次获取时创建。线程不存在,默认使用主线程,并返回主线程的runloop首次访问,会创建全局唯一的__CFRunLoops字典。key为线程,value为runloop。
(线程与runloop是一一对应)- 每次
优先从__CFRunLoops字典中,通过key(线程),获取value(runloop)。- 如果
runloop不存在,就创建线程对应的runloop,并更新__CFRunLoops字典对应值。- 更新
TSD(线程私有存储),记录runloop。- 返回
runloop
面试题: runLoop与线程的关系
一一对应关系。由全局Runloop字典进行记录,其中key为线程,value为runloop。
2.4 runloop的创建

总结:
- 以
__CFRunLoop为模板,创建Runloop结构体对象属性的初始化赋值Mode的获取:
- 如果通过
__kCFRunLoopModeTypeID读取到Modes,并且Modes中存在kCFRunLoopDefaultMode,就直接返回找到的Mode。- 否则,
创建一个Mode,加入modes中。返回Mode。
拓展:
runLoop本质是__CFRunLoop格式的结构体。记录
线程锁,port唤醒端口,所在线程、所有标记为Common的Mode、加入CommonMode的item事务、当前Mode和所有Modeimage.png
- 理解
commonModes和modes:
image.png
2.5 runloop的运行原理

- 看完源码后,
runloop的运行周期,唤醒方式都十分清晰了。现在奉上经典的Runloop流程图:
image.png
补充说明:
【最外层流程】
kCFRunLoopEntry进入循环 (发通知)
->__CFRunLoopRun运行循环
->kCFRunLoopExit退出循环(发通知)【循环内部】
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的基础探索。

















