OC底层原理 学习大纲
- 上一节 ,详细介绍了
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
#import
// 声明外部实现
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
#import
// 声明外部实现
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-whil
e加上条件
,你不要了
就把这条件设置
为不满足
就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 *)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
和所有Mode
image.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
的基础探索
。