前言
本篇文章会大致分析下自动释放池(AutoreleasePool)
和 Runloop
的底层实现原理,这两个知识点也是面试
中经常问到的,希望大家都能掌握这些内容,同时,有不对的地方,希望大家及时指正,谢谢~
一、自动释放池
AutoreleasePool是OC中的一种内存自动回收机制,它可以将加入AutoreleasePool中的变量release的时机延迟,简单来说,就是当创建一个对象,正常情况下,变量会在其作用域的结束时会立即release释放。如果将该对象加入到了AutoreleasePool中,即使作用域结束了,这个对象并不会立即释放,它会等到runloop休眠或超出autoreleasepool作用域{}之后才会被释放。其机制如下图所示👇
- 从程序
App启动
到dyld加载完成
后,主线程对应的runloop会处于休眠
状态,等待用户交互
来唤醒
runloop; - 用户的每一次
交互
都会启动
一次runloop,用于处理用户的所有点击、触摸
事件等; -
CocoaTouch
在监听到交互事件后,就会创建
自动释放池,并将所有延迟释放
的对象添加
到自动释放池中; - 在一次
完整
的runloop结束之前
,CocoaTouch
会向自动释放池中所有对象发送release
消息,然后销毁
自动释放池。
1.1 找入口
那AutoreleasePool对应的底层到底是什么东西呢?首先需要找到入口,我们先clang一下看看@autoreleasepool
对应的C++代码是什么👇
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"来了,老弟");
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));;
}
}
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
我们发现,@autoreleasepool
对应的底层结构是__AtAutoreleasePool
,我们继续在.cpp中搜索__AtAutoreleasePool
👇
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
可见,__AtAutoreleasePool
是一个结构体,包含构造函数
,析构函数
和一个指针atautoreleasepoolobj
。
接下来,我们再打断点👇
看看汇编代码👇
定位到了objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
。这两个函数就是@autoreleasepool
的作用域{}
对应底层的压栈
和出栈
。
1.2 底层解析
通过上面对AutoreleasePool入口的定位,我们知道了objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
,现在我们去到Objc源码,首先搜索objc_autoreleasePoolPush
,看看👇
定位到是类AutoreleasePoolPage
,再搜索这个类👇
通过注释,有以下几点说明
- 自动释放池是一个
指针集合
,指向一个栈
结构空间。 -
指针集合
中的指针
是指向要被释放的对象或者pool_boundary
(之前叫做哨兵
,现在经常被称为边界
) - 自动释放池是一个
页
的结构 ,而且这个页
是一个双向链表
(含有父节点 和 子节点) - 自动释放池和
线程
有很大的关系
。
那么,问题来了👇
- 自动释放池是
什么时候创建
的? - 对象是
如何被加入
到自动释放池的? -
哪些对象
才会被加入
到自动释放池?
带着这些问题,我们进一步看看自动释放池
的底层原理
。
1.2.1 AutoreleasePoolPage
至此,我们知道了自动释放池
是一个页
的结构,就是AutoreleasePoolPage
。大致结构分布如下:
//************宏定义************
#define PAGE_MIN_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096 /* bytes per 80386 page */
//************类定义************
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
//页的大小
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment, power of 2
#endif
private:
...
//构造函数
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),//开始存储的位置
objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
{...}
//析构函数
~AutoreleasePoolPage() {...}
...
//页的开始位置
id * begin() {...}
//页的结束位置
id * end() {...}
//页是否为空
bool empty() {...}
//页是否满了
bool full() {...}
//页的存储是否少于一半
bool lessThanHalfFull() {...}
//添加释放对象
id *add(id obj){...}
//释放所有对象
void releaseAll() {...}
//释放到stop位置之前的所有对象
void releaseUntil(id *stop) {...}
//杀掉
void kill() {...}
//释放本地线程存储空间
static void tls_dealloc(void *p) {...}
//获取AutoreleasePoolPage
static AutoreleasePoolPage *pageForPointer(const void *p) {...}
static AutoreleasePoolPage *pageForPointer(uintptr_t p) {...}
//是否有空池占位符
static inline bool haveEmptyPoolPlaceholder() {...}
//设置空池占位符
static inline id* setEmptyPoolPlaceholder(){...}
//获取当前操作页
static inline AutoreleasePoolPage *hotPage(){...}
//设置当前操作页
static inline void setHotPage(AutoreleasePoolPage *page) {...}
//获取coldPage
static inline AutoreleasePoolPage *coldPage() {...}
//快速释放
static inline id *autoreleaseFast(id obj){...}
//添加自动释放对象,当页满的时候调用这个方法
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {...}
//添加自动释放对象,当没页的时候使用这个方法
static __attribute__((noinline))
id *autoreleaseNoPage(id obj){...}
//创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj) {...}
public:
//自动释放
static inline id autorelease(id obj){...}
//入栈
static inline void *push() {...}
//兼容老的 SDK 出栈方法
__attribute__((noinline, cold))
static void badPop(void *token){...}
//出栈页面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop){...}
__attribute__((noinline, cold))
static void
popPageDebug(void *token, AutoreleasePoolPage *page, id *stop){...}
//出栈
static inline void
pop(void *token){...}
static void init(){...}
//打印
__attribute__((noinline, cold))
void print(){...}
//打印所有
__attribute__((noinline, cold))
static void printAll(){...}
//打印Hiwat
__attribute__((noinline, cold))
static void printHiwat(){...}
它的父类是AutoreleasePoolPageData
👇
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
//用来校验AutoreleasePoolPage的结构是否完整
magic_t const magic;//16个字节
//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
__unsafe_unretained id *next;//8字节
//指向当前线程
pthread_t const thread;//8字节
//指向父节点,第一个结点的parent值为nil
AutoreleasePoolPage * const parent;//8字节
//指向子节点,最后一个结点的child值为nil
AutoreleasePoolPage *child;//8字节
//表示深度,从0开始,往后递增1
uint32_t const depth;//4字节
//表示high water mark 最大入栈数量标记
uint32_t hiwat;//4字节
//默认构造函数
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
这里发现, AutoreleasePoolPage
同时也是一个双向链表
结构 --> 包含parent
和 child
。再看看AutoreleasePoolPageData
结构体所占内存大小,算出来是56字节
。
1.2.2 objc_autoreleasePoolPush
接下来我们看看压栈
操作objc_autoreleasePoolPush
的源码👇
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
还是回到类AutoreleasePoolPage
的push
函数👇
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
流程不难,大致分为以下几步:
- 判断当前是否有池,
- 若没有,则创建 -->autoreleaseNewPage
- 有池,则加入一个边界对象POOL_BOUNDARY
- 返回池的边界大小
autoreleaseNewPage
接下来我们仔细看看创建池
的底层流程👇
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);
}
根据hotPage()
,生成autoreleaseFullPage
,否则就返回autoreleaseNoPage
。
hotPage
继续,我们先看看hotPage()
源码👇
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
然后看看tls_get_direct
👇
static inline void *tls_get_direct(tls_key_t k)
{
ASSERT(is_valid_direct_key(k));
if (_pthread_has_direct_tsd()) {
return _pthread_getspecific_direct(k);
} else {
return pthread_getspecific(k);
}
}
源码可知,是在当前线程pthead中,根据key获取的页AutoreleasePoolPage
,其中key就是自动释放池key
👇
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
由此可见,当前线程pthead指向的空间中,有一个类似字典结构
,从这里根据AUTORELEASE_POOL_KEY
能获取到自动释放池。
autoreleaseFullPage
接下来,看看autoreleaseFullPage
源码👇
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
然后add
的源码
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
流程也很简单,核心代码是do-while循环:寻找当前池页的子页
,没有则创建新的page
,直到达到Size容量
,然后将这个子页
设置为hotPage
,即线程pThread中的 AUTORELEASE_POOL_KEY
所对应的value是这个子页page
,最后将压栈的对象objcadd压栈
到这个子页page
中,实际是对当前page的next++
。
autoreleaseNoPage
objc_autoreleasePoolPush中,如果当前线程pThread中的AUTORELEASE_POOL_KEY
所对应的value没有值时,则会进入autoreleaseNoPage
流程👇
其中,我们发现,autoreleaseNoPage
中会调用AutoreleasePoolPage
的构造函数,创建一个新的页
,构造函数源码如下👇
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),
objc_thread_self(),
newParent,
newParent ? 1+newParent->depth : 0,
newParent ? newParent->hiwat : 0)
{
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
其中,newParent
的值是nil
,而父类结构体的构造方法👇
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
那么,_next
就是begin()
即压栈
的开始位置
,_thread
就是当前线程objc_thread_self()
。
id * begin() {
//等于 首地址+56(AutoreleasePoolPage类所占内存大小)
return (id *) ((uint8_t *)this+sizeof(*this));
}
__attribute__((const))
static inline pthread_t objc_thread_self()
{
//通过tls获取当前线程
return (pthread_t)tls_get_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
}
autoreleaseFast
以上分析了autoreleaseNewPage
,接下来就是else情况autoreleaseFast
了,源码👇
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
分析完autoreleaseFullPage
和 autoreleaseNoPage
之后,现在回头来看autoreleaseFast
就很简单了,大致流程👇:
- 获取当前线程中
AUTORELEASE_POOL_KEY
对应的页
- 如果该
页
存在,且有空间,则压栈objc - 如果
没有空间
,则扩容,加一页
,再压栈objc - 如果该
页
不存在,则创建一页
,其key设为AUTORELEASE_POOL_KEY
,最后压栈objc
小结
综上所述,objc_autoreleasePoolPush
压栈的核心流程如下👇
- 当没有pool,即只有空占位符(存储在线程tls中)时,则
创建页
,压栈边界对象
- 在
页
中压栈普通对象
,通过next++
进行Page容量的标记页
容量满了,就去子节点页
压栈- 如果当前池中所有
页
都满了,那么新创建一个页
,压栈边界对象
,再压栈对象obj
附:打印查看AutoreleasePool的内存结构
在ARC下,是无法手动
调用autorelease
的,所以将Demo切换至MRC模式,修改工程配置BuildSetting,搜索automatic refer
改为NO
,如下图👇
再看示例代码👇
//************打印自动释放池结构************
extern void _objc_autoreleasePoolPrint(void);
//************运行代码************
int main(int argc, const char * argv[]) {
@autoreleasepool {
//循环创建对象,并加入自动释放池
for (int i = 0; i < 5; i++) {
NSObject *objc = [[NSObject alloc] autorelease];
}
//调用
_objc_autoreleasePoolPrint();
}
}
运行👇
上图红框处就是自动释放池的打印信息,其中POOL就是池的边界
。再看池的首地址0x7f9ad8809000
和 边界地址0x7f9ad8809038
,相差38(16进制)
,转换成十进制56
,正好验证了之前说的AutoreleasePool
类所占内存空间的大小。
接着,我们将for循环改为505次,run👇
可以看到,第2页存储了一个,那么第一页存储了504个对象+1个边界对象,共计505个。不信,再将循环次数改为505+505,run👇
第3页是1个,共计是505+505+1个边界对象,那么一页最多可以存储505个对象
。
以上,我们可以得出以下结论:
边界对象
在第1页,第1页最多可存储504个对象,第1页满了,会开辟新的一页- 从第2页开始,最多可存储505个对象
- 一页中所有对象所占内存是:505 * 8字节(因为是指针) = 4040字节
再回头看看类AutoreleasePool
,内部定义了一个SIZE
👇
其中
#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT)
1<<12就是2的12次方=4096,之前说过,边界
对象距离池首地址
相差56
,4040+56 = 4096,完美契合!
AutoreleasePool
的内部结构图如下👇
附面试题:
边界对象
在一个自动释放池有几个?
只有一个边界对象
对象,且在第一页
第一页最多可以存504个对象,第二页开始最多存 505个
autorelease底层分析
MRC中,调用[obj autorelease]来延迟内存的释放是一件简单自然的事,ARC下,我们甚至可以完全不知道Autorelease就能管理好内存。而在这背后,objc和编译器都帮我们做了哪些事呢,它们是如何协作来正确管理内存的呢?
我们还是一样找入口。打断点,看汇编👇
定位到objc_autorelease
,查看源码👇
__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
obj为nil
或为isTaggedPointer小对象
时,直接返回obj,否则进入autorelease()
👇
inline id
objc_object::autorelease()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootAutorelease();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}
其中,ISA()->hasCustomRR()
源码👇
bool hasCustomRR() const {
return !bits.getBit(FAST_HAS_DEFAULT_RR);
}
// 其中FAST_HAS_DEFAULT_RR👇
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<2)
那么autorelease()
流程如下:
-
ISA()->hasCustomRR()
-->判断
当前类或者父类
含有默认的
retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference方法。 - 有,则直接消息发送,调用
autorelease
方法 - 没有,则调用
rootAutorelease()
,源码👇
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
以上,小对象一律不看,最终来到了autorelease(id obj)
,且是调用autoreleaseFast
压栈obj。
注意,从
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
这句代码可以看出,dest既可以是边界对象
,也可以是普通对象
,所以autorelease
是没有
区分边界对象
的。
1.2.3 objc_autoreleasePoolPop
那么,接下来,我们最后来看看出栈objc_autoreleasePoolPop
的底层流程👇
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
和push一样,进入到类AutoreleasePoolPage
的pop函数👇
接着,我们看看popPage
👇
releaseUntil
我们先看看重点函数releaseUntil
👇
上图可知,外层while
循环,其实是在遍历你要释放的obj之前的对象
,然后逐一进行释放
,同时将其对应的内存状态
标识为SCRIBBLE
(释放后的状态)。
kill
最后看看kill👇
小结
综上所述,objc_autoreleasePoolPop
出栈的核心流程如下👇
releaseUntil
,释放当前页出栈对象obj+obj之前的对象,之前的页- 也是通过
next--
标记当前页的容量
- 出栈后,判断当前页的容量是否
少于505/2(一半容量)
,是
则删除子节点页面,不是
则删除孙子节点页面
二、RunLoop
RunLoop
也是近年来面试必问的知识点,而且基本涉及很细很底层,所以本篇也在此探索一些底层实现原理。我们对于RunLoop
,最关心的几个问题无非是以下:
- runloop是什么?
- runloop和线程的关系?
- runloop是什么时候创建的?
2.1 RunLoop简介
RunLoop
是事件接收和分发机制
的一个实现
,是线程
相关的基础框架的一部分
,一个RunLoop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。RunLoop本质是一个do-while循环
,没事做就休息,来活了就干活。与普通的while循环的区别👇
- 普通的while循环会导致CPU进入
忙等待
状态,即一直消耗cpu
- RunLoop则不会,RunLoop是一种
闲等待
,即它具备休眠
功能
RunLoop的作用
- 保持程序的持续运行
- 处理App中的各种事件(触摸、定时器、performSelector)
- 节省cpu资源,提供程序的性能,该做事就做事,该休息就休息
Runloop源码
RunLoop源码的下载地址,在其中找到最新版下载即可。
2.2 RunLoop与线程的关系
RunLoop与线程的关系得从RunLoop的获取方式
这个切入点查看。系统给了两种方式获取RunLoop👇
// 主运行循环
CFRunLoopRef mainRunloop = CFRunLoopGetMain();
// 当前运行循环
CFRunLoopRef currentRunloop = CFRunLoopGetCurrent();
2.2.1 CFRunLoopGetMain
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
//pthread_main_thread_np 主线程
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
// -----------------------------分割线 -----------------------------
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
接着看看_CFRunLoopGet0
👇
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//如果t不存在,则标记为主线程(即默认情况,默认是主线程)
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
//创建全局字典,标记为kCFAllocatorSystemDefault
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//通过主线程 创建主运行循环
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//利用dict,进行key-value绑定操作,即可以说明,线程和runloop是一一对应的
// dict : key value
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//通过其他线程获取runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//如果没有获取到,则新建一个运行循环
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//将新建的runloop 与 线程进行key-value绑定
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
以上可以看出,RunLoop只有两种:一种是主线程
的, 一个是其他线程
的。即runloop和线程是一一对应
的。
2.3 RunLoop的使用
在_CFRunLoopGet0
的流程中,有一个创建步骤__CFRunLoopCreate
,我们先看看其源码。
2.3.1 创建RunLoop
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopTypeID, size, NULL);
//如果loop为空,则直接返回NULL
if (NULL == loop) {
return NULL;
}
//runloop属性配置
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
loop->_winthread = GetCurrentThreadId();
#else
loop->_winthread = 0;
#endif
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}
这个创建,和我们平时写的初始化代码基本一样,我们再看看返回的对象是CFRunLoopRef
类型👇
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
接着看__CFRunLoop
👇
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
从结构体__CFRunLoop
中可以得出,一个RunLoop依赖于多个Mode(_commonModes
),意味着一个RunLoop需要处理多个事务_commonModeItems
,那么一个Mode对应多个Item,而一个item中,包含了timer、source、observer,如下所示👇
Mode类型
其中mode在苹果文档中提及的有五个,而在iOS中公开暴露出来的只有 NSDefaultRunLoopMode
和 NSRunLoopCommonModes
。 而NSRunLoopCommonModes
实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode
和 NSEventTrackingRunLoopMode
。
- NSDefaultRunLoopMode:默认的mode,正常情况下都是在这个mode
- NSConnectionReplyMode
- NSModalPanelRunLoopMode
- NSEventTrackingRunLoopMode:使用这个Mode去跟踪来自用户交互的事件(比如UITableView上下滑动)
- NSRunLoopCommonModes:伪模式,灵活性更好
Source & Timer & Observer
Source表示可以唤醒RunLoop的一些事件,例如用户点击了屏幕,就会创建一个RunLoop,主要分为Source0和Source1:
- Source0 表示 非系统事件,即用户自定义的事
- Source1 表示系统事件,主要负责底层的通讯,具备唤醒能力
Timer 就是常用NSTimer定时器
这一类
Observer 主要用于监听RunLoop的状态变化,并作出一定响应,主要有以下一些状态👇
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
//进入RunLoop
kCFRunLoopEntry = (1UL << 0),
//即将处理Timers
kCFRunLoopBeforeTimers = (1UL << 1),
//即将处理Source
kCFRunLoopBeforeSources = (1UL << 2),
//即将进入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
//被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
//退出RunLoop
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
示例验证:RunLoop和mode是一对多关系
通过lldb命令获取mainRunloop、currentRunloop的currentMode
由此可见,runloop在运行时的mode只有一个
。
获取mainRunloop的所有模型,即
po CFRunLoopCopyAllModes(mainRunloop)
由此可见,runloop
和 CFRunloopMode
是 一对多的关系
。
示例验证:mode和Item也是一对多关系
在RunLoop源码中查看Item类型,有以下几种👇
- 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为例,一般初始化timer时,都会将timer通过addTimer:forMode:
方法添加到Runloop中,于是在源码中查找addTimer
的相关方法,即CFRunLoopAddTimer
方法,其源码实现如下
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
__CFRunLoopLock(rl);
// 重点 : kCFRunLoopCommonModes
if (modeName == kCFRunLoopCommonModes) {
//如果是kCFRunLoopCommonModes 类型
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//runloop与mode 是一对多的, mode与item也是一对多的
CFSetAddValue(rl->_commonModeItems, rlt);
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt};
/* add new item to all common-modes */
//执行
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
//如果是非commonMode类型
//查找runloop的模型
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm) {
if (NULL == rlm->_timers) {
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
}
}
//判断mode是否匹配
if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
__CFRunLoopTimerLock(rlt);
if (NULL == rlt->_runLoop) {
rlt->_runLoop = rl;
} else if (rl != rlt->_runLoop) {
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
return;
}
// 如果匹配,则将runloop加进去,而runloop的执行依赖于 [runloop run]
CFSetAddValue(rlt->_rlModes, rlm->_name);
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopTimerFireTSRLock();
__CFRepositionTimerInMode(rlm, rlt, false);
__CFRunLoopTimerFireTSRUnlock();
if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
// Normally we don't do this on behalf of clients, but for
// backwards compatibility due to the change in timer handling...
if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
}
其实现主要判断kCFRunLoopCommonModes
,然后查找runloop的mode进行匹配
处理:
- 其中
kCFRunLoopCommonModes
不是一种模式,是一种抽象的伪模式
,比defaultMode更加灵活
。 - 通过
CFSetAddValue(rl->_commonModeItems, rlt);
可以得知,runloop与mode 是一对多的,同时可以得出mode 与 item 也是一对多。
2.3.2 执行RunLoop
RunLoop的执行依赖于run方法,从下面的堆栈信息中可以看出,其底层执行的是__CFRunLoopRun
方法👇
其源码如下👇
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
...
do{
...
//通知 Observers: 即将处理timer事件
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知 Observers: 即将处理Source事件
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
//处理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
//处理sources0返回为YES
if (sourceHandledThisLoop) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
...
//如果是timer
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
...
//如果是source1
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
}
...
}while (0 == retVal);
...
}
这是省略了其它模式的处理,只看timer的模式,其实进入__CFRunLoopRun源码,针对不同的对象,有不同的处理
- 如果有observer,则调用 __CFRunLoopDoObservers
- 如果有block,则调用__CFRunLoopDoBlocks
- 如果有timer,则调用 __CFRunLoopDoTimers
- 如果是source0,则调用__CFRunLoopDoSources0
- 如果是source1,则调用__CFRunLoopDoSource1
接着我们看看__CFRunLoopDoTimers
👇
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) { /* DOES CALLOUT */
...
//循环遍历,做下层单个timer的执行
for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
timerHandled = timerHandled || did;
}
...
}
__CFRunLoopDoTimer
的主要逻辑是timer执行完毕后,会主动调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
函数👇
// mode and rl are locked on entry and exit
static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) {
...
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
...
}
正好与timer堆栈调用中的一致👇
timer流程总结:
- 为自定义的timer,设置Mode,并将其
加入RunLoop
中- 在RunLoop的run方法执行时,会调用
__CFRunLoopDoTimers
执行所有timer- 在__CFRunLoopDoTimers方法中,会通过
for循环执行单个timer
的操作- 在__CFRunLoopDoTimer方法中,timer执行完毕后,会执行对应的
timer回调
以上,是针对timer
的执行分析,对于observer、block、source0、source1
,其执行原理与timer是类似的,这里就不再重复说明以下是苹果官方文档针对RunLoop处理不同源的图示👇
2.4 RunLoop底层原理(伪代码)
之前看RunLoop调用栈信息,我们知道RunLoop底层是通过CFRunLoopRun
👇
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
// 1.0e10 : 科学技术 1*10^10
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
果然,是个do-while
循环,其中传入的参数1.0e10(科学计数) 等于 1* e^10,用于表示超时时间
。接着看看CFRunLoopRunSpecific
👇
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 内部函数,进入loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
大致流程如下👇
首先根据modeName
找到对应的mode
类型,然后主要分为三种情况:
- 如果是entry,则通知observer,即将进入runloop
- 如果是exit,则通过observer,即将退出runloop
- 如果是其他中间状态,主要是通过runloop处理各种源
接着看看核心流程__CFRunLoopRun
,由于这部分代码较多,于是这里用伪代码代替。其主要逻辑是根据不同的事件源进行不同的处理,当RunLoop休眠时,可以通过相应的事件唤醒RunLoop👇
//核心函数
/* 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;
}
至此,RunLoop的执行流程分析完毕,可以看看下面的经典图片👇
总结
本篇文章主要分析了两大知识点:AutoreleasePool
和RunLoop
,根据cpp和汇编查找入口,然后找到底层源码分析它们的实现流程,按照这个思路,我们终于清楚了系统是如何处理自动释放,RunLoop是如何处理各种事件源信息。相信大家在面试时碰到这些问题,就胸有成竹,应对自如了,哈哈!
网友评论