iOS内存管理 —— 自动释放池和runloop
iOS内存管理 —— 自动释放池和runloop
- 1. 自动释放池
- 1.1 自动释放池介绍
- 1.2 自动释放池底层原理
- objc_autoreleasePoolPush
- autoreleaseNoPage
- autoreleaseFullPage
- page->add
- objc_autoreleasePoolPop
- 1.3 自动释放池能否嵌套使用
- 1.4 自动释放池的入池条件
- MRC情况下
- ARC情况下
- 2. Runloop
- 2.1 Runloop 介绍
- 2.2 Runloop 底层分析
- 2.3 Runloop的原理
1. 自动释放池
1.1 自动释放池介绍
自动释放池是OC中的一种内存自动回收机制
,它可以延迟
加入AutoreleasePool
中的变量release
的时机,即当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop
结束或者作用域超出autoreleasepool{}
之后再被释放
1.2 自动释放池底层原理
要探究自动释放池的底层结构,那么就要用clang
或者xcrun
将代码转换成cpp文件。
转化完之后可以看到,@autoreleasepool{}
变成了{__AtAutoreleasePool __autoreleasepool; }
在生成的cpp文件中搜索__AtAutoreleasePool,发现其是一个结构体,那么也就相当于,@autoreleasepool{}
调用了__AtAutoreleasePool
的构造和析构函数:objc_autoreleasePoolPush()
和 objc_autoreleasePoolPop(atautoreleasepoolobj)
;
下断点后发现objc_autoreleasePoolPush
在objc源码中,接下来就去源码中探索。
objc_autoreleasePoolPush
这里可以看到objc_autoreleasePoolPush
和objc_autoreleasePoolPop
分别调用了AutoreleasePoolPage::push();
和AutoreleasePoolPage::pop(ctxt);
。那么这里的AutoreleasePoolPage是什么呢?
点进来看到AutoreleasePoolPage
继承自AutoreleasePoolPageData
,并且可以从注释这里看到,自动释放池和线程
有一定的关系,而且是栈结构存储
,里面储存着指针
。每个指针指向要释放的对象
或者是POOL_BOUNDARY
,也就是自动释放池的边界
。自动释放池是一个类,进行不断的压栈对象,意味着会不断进栈和出栈,但是这里不能无限制的出栈。如果一直不断的出栈,那么指针就会不断的平移和kill,如果没有边界的话,那么就会破坏别人的内存,之后如果访问被破坏的对象就会造成野指针的问题。这里还可以看到自动释放池是一个doubly—linkes list of pages,也就是双向链表
。
再来看AutoreleasePoolPageData
,看到这里的一些属性和构造函数。
magic
: 用来校验 AutoreleasePoolPage 的结构是否完整;next
: 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向
begin() ;thread
: 指向当前线程;parent
: 指向父结点,第一个结点的 parent 值为 nil ;child
: 指向子结点,最后一个结点的 child 值为 nil ;depth
: 代表深度,从 0 开始,往后递增 1;hiwat
: 代表 high water mark 最大入栈数量标记
接下来探索autoreleasepool的压栈情况,将build settings
的 automatic reference counting
设为No。
创建一个nsobject对象并调用autorelease
,引入并调用_objc_autoreleasePoolPrint
方法后运行。
运行后得到下面的的打印。这里的对象一个为哨兵对象,一个为自己添加的NSObject对象。
接下来看objc_autoreleasePoolPush
。这里会走到autoreleaseFast
里面。
看到autoreleaseFast
,这里会进行判断。
- 如果page存在并且没有满,那么调用
page->add
添加这个对象到page里面, - 如果page存在但是page满了,那么调用
autoreleaseFullPage
. - 如果page不存在则调用
autoreleaseNoPage
。
那么第一次来的话,就会调用autoreleaseNoPage。
autoreleaseNoPage
看到autoreleaseNoPage
。这里主要是进行AutoreleasePoolPage的创建,然后将当前页面设为hotpage,添加哨兵对象,最后添加要添加的对象。
看到AutoreleasePoolPage
的构造函数,这里会调用AutoreleasePoolPageData
的构造函数来进行属性的初始化。然后如果下面判断如果parent
存在,那么就将parent
的child
设为自己。这里可以看到外面传的是nil,所以parent是不存在的。
这里还有调用begin
,到begin里面打下断点后运行。
这里看到this是AutoreleasePoolPage
,并且大小为56。
再看到AutoreleasePoolPage的结构,发现大小确实为56
。
这里结构体创建是占用堆区
内存,static修饰的在全局区
不占堆区内存。这里一个uint32_t
4个字节,4个uint32_t就是16个字节。
也就是说,这里从成员变量之下开始插入。结构如下:
这里的56就是结构体的大小,之后0x10480a038是哨兵对象,然后就是自己添加的要销毁的对象。
AutoreleasePoolPageData
里面还调用了objc_thread_self
,这里调用tls_get_direct
获取当前线程。
autoreleaseFullPage
接下来看到当autoreleasePage满的情况下,调用的autoreleaseFullPage。
这里递归找到最后一个子页面,然后创建新的页面,并且把新的页面设为HotPage,最后添加要添加的释放对象。
形成了如下的结构,分页是因为这里会不断的出栈入栈,对内存操作非常频繁,如果只有一个页面,那么所有的对象都在这一个页面里面,那么操作就会变得繁杂,管理变得不便。并且如果这里局部发生问题,那么就会影响整个页面。而如果是分页的话,就只会影响局部的页面。并且,分页不需要在内存上连续。
那么这里什么时候会满呢。这里for循环504次,看到这里页面有个标签full。
这里调整为505后,发现分页了。并且可以看到第二页这里是没有哨兵对象了的。所以这里可以看出,其实一页可以存505个对象的,但是由于第一页多了一个哨兵对象,所以只能存504个对象。所以页的大小为505 * 8 + 56 = 4096,也就是4k。
page->add
这里主要做的就是通过内存平移储存objc。
objc_autoreleasePoolPop
这里会判断hotpage
是否存在,然后对页面进行移动,调整,然后调用popPage移除。
popPage
会拿到parent页面,将当前页面删除之后将parent页面
设为HotPage
page->kill 里面会删除page。
1.3 自动释放池能否嵌套使用
这里嵌套之后运行,发现可以正常运行,里面嵌套的自动释放池被添加到外层的自动释放池里面,并且在作用域结束之后被释放了。
1.4 自动释放池的入池条件
MRC情况下
这里可以看到,在MRC情况下,如果对象没有调用 autorelease方法,是不会被添加到自动释放池里面的。
ARC情况下
这里可以看到,在ARC情况下,如果对象调用 alloc方法,是不会被添加到自动释放池里面的。其实在ARC里面,以alloc,new,copy,mutablecopy命名生成的对象是不会被添加到自动释放池里面的。
2. Runloop
2.1 Runloop 介绍
RunLoop称为事件处理循环,是线程相关的基础框架的一部分,用于安排工作和协调接收传入事件。应用在运行过程中会产生大量的系统和用户事件,包括定时器事件,用户交互事件(鼠标键盘触控板操作),模态窗口事件,各种系统Source事件,应用自定义的Source事件等等,每种事件都会存储到不同的FIFO先进先出的队列,等待事件循环依次处理。RunLoop的目的是在有工作要做的时候让线程保持忙碌,在没有工作的时候让线程休眠。
- block应用:CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
- 调用timer:CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
- 响应source0: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
- 响应source1: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
- GCD主队列:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
- observer源: CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
运行后在timer里面打断点,发现调用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
普通do while 循环可以看到这里占用了15%左右的cpu。
而runloop则基本为0。
2.2 Runloop 底层分析
在底层搜索CFRunLoop,看到这里会调用CFRunLoopGetCurrent
这里会尝试去或者runloop,如果没有,则会从当前线程获取。
这里如果t==0,那么代表着是主线程,那么就会调用pthread_main_thread_np
获取主线程。后面还会创建一个可变字典,然后调用__CFRunLoopCreate创建runloop,再把主线程和创建的runloop放到创建的字典里面。
如果不是主线程,则进行一样的操作,只是线程变成了当前线程。
也就是说线程和runloop会有绑定关系:
接下来看__CFRunLoopCreate
,这里可以在下面副赋值看到CFRunLoopRef的成员变量。
点击属性,看到了整个结构体。这里可以知道,CFRunLoop是一个结构体,里面有很多属性,看到这里_commonModes和_commonModeItems都是集合类型。
而平时往RunLoop添加事务的话,就会指定一个mode,而这个mode在添加别的事务的时候也能使用,那么也就是说,mode和事务是一对多的关系。
RunLoopMode结构体:
事务分三种类型,分别是:
- Source
- Observer
- Timer
那么事务是如何依赖mode运行的呢?在源码中搜索addTimer,发现在__CFRunLoopAddItemsToCommonMode
里面有调用CFRunLoopAddTimer
,同时发现了这里也证明了事务分三种类型。
接下来查看CFRunLoopAddTimer
,看这里是commonModes
下的情况。这里会获取Runloop
里面的_commonModes
集合,然后判断Runloop
里面的事务(_commonModeItems
)是否为空,为空的话就重新创建一个set复制给Runloop
的_commonModeItems
,不为空则直接加到_commonModeItems
集合里面。
这里只是添加timer,那么在那里运行timer呢?看到CFRunLoopRun
里面调用了CFRunLoopRunSpecific
。
CFRunLoopRunSpecific
里面调用了__CFRunLoopRun
,并且在前后进行了状态改变的通知。
__CFRunLoopRun
里面调用了__CFRunLoopDoTimers
。
__CFRunLoopDoTimers
里面会遍历Runloop
里面的timers,然后将符合条件的timer加入到timers里面,遍历完之后,遍历timers,然后执行timers里面的timer,也就是调用__CFRunLoopDoTimer
。
之后__CFRunLoopDoTimer
里面就会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
触发回调。
所以这里的流程是: CFRunLoopAddTimer
CFRunLoopRun
CFRunLoopRunSpecific
__CFRunLoopRun
__CFRunLoopDoTimers
__CFRunLoopDoTimer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
2.3 Runloop的原理
继续看到CFRunLoopRun
,这里参数传了一个1.0e10
也就是1* 10^10
作为超时时间。
之前看到调用__CFRunLoopRun
的时候就看到有两个状态的通知。
点进去看除了kCFRunLoopEntry
和 kCFRunLoopExit
还有其他的状态。其中kCFRunLoopBeforeWaiting
和kCFRunLoopAfterWaiting
使用的最经常,代表着休眠之前和休眠之后。
接着看__CFRunLoopRun
。 这里面用seconds进行了timeout_context的标记,然后创建了一个GCD Source的Timer,然后设置超时回调。
往下走看到了一个doWhile循环,Runloop大部分的事务都是在这个doWhile循环里面处理的。
缩略代码:
//核心函数
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){//通过GCD开启一个定时器,然后开始跑圈dispatch_source_t timeout_timer = NULL;...dispatch_resume(timeout_timer);int32_t retVal = 0;//处理事务,即处理itemsdo {// 通知 Observers: 即将处理timer事件__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);// 通知 Observers: 即将处理Source事件__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)// 处理Blocks__CFRunLoopDoBlocks(rl, rlm);// 处理sources0Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);// 处理sources0返回为YESif (sourceHandledThisLoop) {// 处理Blocks__CFRunLoopDoBlocks(rl, rlm);}// 判断有无端口消息(Source1)if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {// 处理消息goto handle_msg;}// 通知 Observers: 即将进入休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);__CFRunLoopSetSleeping(rl);// 等待被唤醒__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);// user callouts now OK again__CFRunLoopUnsetSleeping(rl);// 通知 Observers: 被唤醒,结束休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);handle_msg:if (被timer唤醒) {// 处理Timers__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());}else if (被GCD唤醒){// 处理gcd__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);}else if (被source1唤醒){// 被Source1唤醒,处理Source1__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)}// 处理block__CFRunLoopDoBlocks(rl, rlm);if (sourceHandledThisLoop && stopAfterHandle) {retVal = kCFRunLoopRunHandledSource;//处理源} else if (timeout_context->termTSR < mach_absolute_time()) {retVal = kCFRunLoopRunTimedOut;//超时} else if (__CFRunLoopIsStopped(rl)) {__CFRunLoopUnsetStopped(rl);retVal = kCFRunLoopRunStopped;//停止} else if (rlm->_stopped) {rlm->_stopped = false;retVal = kCFRunLoopRunStopped;//停止} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {retVal = kCFRunLoopRunFinished;//结束}}while (0 == retVal);return retVal;
}
流程图:
iOS内存管理 —— 自动释放池和runloop相关推荐
- ios中的自动释放池
自动释放池中是否有虑重功能 1 @autoreleasepool { 2 UIView *view = [UIView alloc] init] autorelease]; 3 [view autor ...
- iOS中内存自动释放池
自动释放池 iOS应用的主线程在每次runloop开始的时候创建自动释放池,在runloop结束的时候释放自动释放池.如果在一个runloop内,应用程序创建了大量临时对象,自动释放池可以减少内存峰值 ...
- cocos2d-x游戏开发(六)自动释放池
欢迎转载:http://blog.csdn.net/fylz1125/article/details/8519887 上一篇文章写了引用计数和自动释放的大概流程,其中略过了自动释放池,这里简单述说. ...
- 【iOS高级资深工程师面试篇】④、2022年,金九银十我为你准备了《iOS高级资深工程师面试知识总结》 内存管理部分2/2 引用计数-弱引用-自动释放池-循环引用
iOS高级资深工程师面试篇系列 - 已更新3篇 UI部分1/3 -UITableView-事件传递&视图响应 UI部分2/3 -图像显示原理-UI卡顿&掉帧 UI部分3/3 -UIVi ...
- 内存管理-定时器循环、内存布局、tagged pointer、weak指针、copy、自动释放池
先上代码,我们平时用的定时器,cadisplaylink.nstimer,CADisplayLink.NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用 ...
- 【深入Cocos2d-x】探索Cocos2d-x中的内存管理-引用计数和自动释放池
2019独角兽企业重金招聘Python工程师标准>>> #深入Cocos2d-x-探索Cocos2d-x中的内存管理-引用计数和自动释放池 ###引用计数(Reference Cou ...
- 第六讲:Obj-C 内存管理4 - 自动释放池
转:http://tigercat1977.blog.163.com/blog/static/2141561122012111294616203/ 第六讲:Obj-C 内存管理4 - 自动释放池 主要 ...
- iOS之深入解析自动释放池autoreleasepool的底层原理
一.自动释放池 autoreleasepool 原理 自动释放池是 OC 中的一种内存自动回收机制,它可以将加入 autoreleasePool 中的变量 release 的时机延迟. 简单来说,就是 ...
- iOS 多线程 自动释放池常见面试题代码
同步:代码依次执行,一个人执行多个任务,也是依次执行.一个人在同一时间只执行一个任务 异步:可以同时执行多个任务 进程:指在系统中运行的应用程序,每个进程都是独立的,都有独立的且安全的运行空间 线程: ...
最新文章
- 快速搭建第一个Mybatis程序
- 近期活动盘点:2018数据与媒介发展论坛、大数据应用中日交流论坛(11.04-11.15)...
- IBM 揭晓全球第一项 2纳米芯片技术,为半导体领域实现重大突破
- 您的请求参数与订单信息不一致_长春各学校信息审核结果出炉!这些情况不符合“两个一致”...
- Spring源码解析-applicationContext.xml加载和bean的注册
- [导入]java快速开发平台ajf之ORM组件
- 【编译原理】语言的定义
- http协议报文体_HTTP协议扫盲(七)请求报文之 GET、POST-FORM 和 POST-FILE
- 数据结构基础(12) --双向循环链表的设计与实现
- POJ 3186Treats for the Cows (区间DP)
- Linux 搭建NFS文件服务器实现文件共享
- c语言 结构体声明和引用、,结构体的声明与自引用
- TFIDF的原理及实现
- 《东周列国志》第十九回 擒傅瑕厉公复国 杀子颓惠王反正
- [模型调研]实体消歧
- (很容易懂,你把代码复制粘贴即可解决问题)高等代数/线性代数-基于python实现矩阵法求解齐次方程组
- 高价拍下巴菲特午餐的90后孙宇晨是谁?
- 知乎上演的“变形计“,资本市场会打几分?
- mysql和pg数据库表备份及还原
- 深圳湾口岸过关进入香港的交通方法
热门文章
- Python 3 内置函数 - `setattr()`函数
- gSoap生成C++接口
- 笑到最后的百度网盘将何去何从
- 如何在.config文件自定义配置节
- 序列比对(四)——Smith-Waterman算法之仿射罚分
- 桂花网创始人赵福勇:蓝牙物联网的领先者,让万物互联奔腾起来
- 配置网络接口的“IP“命令
- Java 13---JDBC简介
- android 展示大图,Android 加载超大图(原图)分析
- 【Android】网页广告植入规避方案