iOS内存管理 —— 自动释放池和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_autoreleasePoolPushobjc_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 settingsautomatic 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存在,那么就将parentchild设为自己。这里可以看到外面传的是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的时候就看到有两个状态的通知。
在这里插入图片描述
点进去看除了kCFRunLoopEntrykCFRunLoopExit 还有其他的状态。其中kCFRunLoopBeforeWaitingkCFRunLoopAfterWaiting使用的最经常,代表着休眠之前和休眠之后。
在这里插入图片描述
接着看__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;
    
    //处理事务,即处理items
    do {
        
        // 通知 Observers: 即将处理timer事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 通知 Observers: 即将处理Source事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
        
        // 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 处理sources0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        
        // 处理sources0返回为YES
        if (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;
}

流程图:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值