本文主要分析 AutoReleasePool
以及 NSRunLoop
的底層實現
AutoReleasePool 自動釋放池
自動釋放池
是OC中的一種內存自動回收機制
,它可以將加入AutoreleasePool中的變量release的時機延遲
,簡單來說,就是當創建一個對象
,在正常情況下,變量會在超出其作用域的時立即release。如果將對象加入到了自動釋放池中,這個對象并不會立即釋放
,會等到runloop休眠/超出autoreleasepool作用域{}
之后才會被釋放
。其機制如下圖所示
1、從程序啟動到加載完成,主線程對應的runloop會處于休眠狀態,等待用戶交互來喚醒runloop
2、用戶的每一次交互都會啟動一次runloop,用于處理用戶的所有點擊、觸摸事件等
3、runloop在監聽到交互事件后,就會
創建
自動釋放池,并將所有延遲釋放的對象添加到自動釋放池中4、在一次完整的runloop結束之前,會向自動釋放池中所有對象
發送release消息
,然后銷毀
自動釋放池
Clang分析
根據之前源碼的分析經驗,我們先通過clang
來分析
- 定義如下代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
}
- 通過clang編譯成底層實現,命令為:
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
struct __AtAutoreleasePool {
//構造函數
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析構函數
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
{
//是一個結構體
__AtAutoreleasePool __autoreleasepool;
}
return 0;
}
簡單來說,自動釋放池其本質也是一個對象
@autoreleasepool {}
//等價于
{__AtAutoreleasePool __autoreleasepool; }
__AtAutoreleasePool
是一個結構體,有構造函數 + 析構函數
,結構體定義的對象在作用域結束后,會自動調用析構函數其中
{}
是 作用域 ,優點是結構清晰,可讀性強
,可以及時創建銷毀
關于涉及的構造和析構函數的調用時機,可以通過下面一個案例來驗證
struct CJLTest{
CJLTest
(){
printf("1123 - %s\n", __func__);
}
~CJLTest(){
printf("5667 - %s\n", __func__);
}
};
int main(int argc, const char * argv[]) {
{
CJLTest test;
}
}
//**********運行結果**********
1123 - CJLTest
5667 - ~CJLTest
從而可以得出,在CJLTest
創建對象時,會自動調用構造函數
,在出了{}作用域后,會自動調用析構函數
匯編分析
-
在main代碼部分加斷點,運行程序,并開啟匯編調試
匯編分析
通過調試結果發現,證明了我們clang分析的結果
總結
autoreleasepool
其本質是一個結構體對象
,一個自動釋放池對象就是頁,是是棧結構存儲
,符合先進后出
的原則即可頁的棧底是一個
56
字節大小的空占位符
,一頁總大小為4096
字節只有
第一頁
有哨兵
對象,最多存儲504
個對象,從第二頁開始最多存儲505
個對象autoreleasepool
在加入要釋放的對象時,底層調用的是objc_autoreleasePoolPush
方法autoreleasepool
在調用析構函數釋放時,內部的實現是調用objc_autoreleasePoolPop
方法
底層分析
在objc
源碼中,對AutoreleasePool
的解釋如下
Autorelease pool implementation
- A thread's autorelease pool is a stack of pointers.
線程的自動釋放池是指針的堆棧
- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
每個指針都是要釋放的對象,或者是POOL_BOUNDARY,它是自動釋放池的邊界。
- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
池令牌是指向該池的POOL_BOUNDARY的指針。彈出池后,將釋放比哨點更熱的每個對象。
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
堆棧分為兩個雙向鏈接的頁面列表。根據需要添加和刪除頁面。
- Thread-local storage points to the hot page, where newly autoreleased objects are stored.
線程本地存儲指向熱頁面,該頁面存儲新自動釋放的對象。
通過描述,有以下幾點說明
1、自動釋放池 是一個 關于
指針
的棧
結構2、其中的指針是指要
釋放的對象
或者pool_boundary
哨兵(現在經常被稱為邊界
)3、自動釋放池是一個
頁
的結構(虛擬內存中提及過) ,而且這個頁是一個雙向鏈表
(表示有父節點 和 子節點,在類中提及過,即類的繼承鏈)4、自動釋放池和
線程
有關系
對于自動釋放池
,我們主要關心的點有以下三點:
1、自動釋放池什么時候
創建
?2、對象是
如何加入自動釋放池
的?3、
哪些對象才會加入
自動釋放池?
下面帶著這些問題,我們來一步步探索自動釋放池的底層原理
AutoreleasePoolPage
- 從最初的
clang
或者匯編
分析我們了解了自動釋放池其底層是調用的objc_autoreleasePoolPush
和objc_autoreleasePoolPop
兩個方法,其源碼實現如下
//***********push方法***********
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
//***********pop方法***********
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
- 從源碼中我們可以發現,都是調用的
AutoreleasePoolPage
的push
和pop
實現,以下是其定義,從定義中可以看出,自動釋放池是一個頁,同時也是一個對象
,這個頁的大小是4096
字節
//************宏定義************
#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(){...}
- 從其定義中發現,
AutoreleasePoolPage
是繼承自AutoreleasePoolPageData
,且該類的屬性也是來自父類,以下是AutoreleasePoolPageData
的定義,- 發現其中有
AutoreleasePoolPage
對象,所以有以下一個關系鏈AutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage
,從這里可以說明自動釋放池除了是一個頁,還是一個雙向鏈表
結構
- 發現其中有
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)
{
}
};
其中AutoreleasePoolPageData
結構體的內存大小為56
字節:
屬性
magic
的類型是magic_t
結構體,所占內存大小為m[4];
所占內存(即4*4=16
字節)屬性
next(指針)、thread(對象)、parent(對象)、child(對象)
均占8
字節(即4*8=32字節)屬性
depth、hiwat
類型為uint32_t
,實際類型是unsigned int
類型,均占4
字節(即2*4=8字節)
objc_autoreleasePoolPush 源碼分析
進入push
源碼實現,有以下邏輯
判斷是否為有 pool
如果沒有,則通過
autoreleaseNewPage
方法創建如果有,則通過
autoreleaseFast
壓棧哨兵對象
//入棧
static inline void *push()
{
id *dest;
//判斷是否有pool
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.自動釋放池從新池頁面開始
//如果沒有,則創建
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//壓棧一個POOL_BOUNDARY,即壓棧哨兵
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
1、創建頁 autoreleaseNewPage
- 進入
objc_autoreleasePoolPush -> push -> autoreleaseNewPage源碼實現,主要是通過
hotPage`獲取當前頁,判斷當前頁是否存在如果存在,則通過
autoreleaseFullPage
方法壓棧對象
如果不存在,則通過
autoreleaseNoPage
方法創建頁
//創建新頁
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
//獲取當前操作頁
AutoreleasePoolPage *page = hotPage();
//如果存在,則壓棧對象
if (page) return autoreleaseFullPage(obj, page);
//如果不存在,則創建頁
else return autoreleaseNoPage(obj);
}
//******** hotPage方法 ********
//獲取當前操作頁
static inline AutoreleasePoolPage *hotPage()
{
//獲取當前頁
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
//如果是一個空池,則返回nil,否則,返回當前線程的自動釋放池
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
//******** autoreleaseNoPage方法 ********
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
bool pushExtraBoundary = false;
//判斷是否是空占位符,如果是,則壓棧哨兵標識符置為YES
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
//如果對象不是哨兵對象,且沒有Pool,則報錯
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
//如果對象是哨兵對象,且沒有申請自動釋放池內存,則設置一個空占位符存儲在tls中,其目的是為了節省內存
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {//如果傳入參數為哨兵
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();//設置空的占位符
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
//初始化第一頁
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//設置page為當前聚焦頁
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
//壓棧哨兵的標識符為YES,則壓棧哨兵對象
if (pushExtraBoundary) {
//壓棧哨兵
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
//壓棧對象
return page->add(obj);
}
其中autoreleaseNoPage
方法中發現當前線程的自動釋放池
是通過AutoreleasePoolPage
創建的,其定義中有構造方法
,而構造方法的實現是通過父類AutoreleasePoolPageData
的初始化方法(從上面的定義中可以得知)
//**********AutoreleasePoolPage構造方法**********
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),//開始存儲的位置
objc_thread_self(),//傳的是當前線程,當前線程時通過tls獲取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一頁深度為0,往后是前一個的深度+1
newParent ? newParent->hiwat : 0)
{
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
//this 表示 新建頁面,將當前頁面的子節點 賦值為新建頁面
parent->child = this;
parent->protect();
}
protect();
}
//**********AutoreleasePoolPageData初始化方法**********
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)
{
}
其中AutoreleasePoolPageData
方法傳入的參數含義為:
-
begin()
表示壓棧
的位置(即下一個要釋放對象的壓棧地址)。可以通過源碼調試begin
,發現其具體實現等于頁首地址+56
,其中的56
就是結構體AutoreleasePoolPageData
的內存大小
autoreleasePool調試
//********begin()********
//頁的開始位置
id * begin() {
//等于 首地址+56(AutoreleasePoolPage類所占內存大小)
return (id *) ((uint8_t *)this+sizeof(*this));
}
-
objc_thread_self()
表示的是當前線程
,而當前線程時通過tls
獲取的
__attribute__((const))
static inline pthread_t objc_thread_self()
{
//通過tls獲取當前線程
return (pthread_t)tls_get_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
}
newParent
表示父節點
后續兩個參數是
通過父節點的深度、最大入棧個數
計算depth
以及hiwat
查看自動釋放池內存結構
由于在ARC模式下,是無法手動調用autorelease
,所以將Demo切換至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting
設置為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] sutorelease];
}
//調用
_objc_autoreleasePoolPrint();
}
}
運行結果如下,發現是6個,但是我們壓棧的對象其實只有5個,其中的POOL
表示哨兵
,即邊界
,其目的是為了防止越界
查看自動釋放池的內存結構,發現,頁的首地址與
哨兵對象
相差0x38
,轉換成十進制剛好是56
,也就是 AutoreleasePoolPage
自己本身的內存大小
-
將上述的測試代碼的數據改為
505
,其內存結構如下,發現第一頁滿了,存儲了504
個要釋放的對象,第二頁只存儲了一個
結構-3 -
在將數據改為
505+506
,來驗證第二頁是否也是存儲504個對象
結構-4
通過運行發現,第一頁存儲504
,第二頁存儲505
,第三頁存儲2
個
結論
所以通過上述測試,可以得出以下結論:
第一頁可以存放
504
個對象,且只有第一頁有哨兵
,當一頁壓棧滿了,就會開辟新的一頁第二頁開始,最多可以存放
505
個對象一頁的大小等于 505 * 8 = 4040
這個結論同樣可以通過AutoreleasePoolPage
中的SIZE
來得到印證,從其定義中我們可以得出,一頁的大小是4096
字節,而在其構造函數中對象的壓棧位置
,是從首地址+56
開始的,所以可以一頁中實際可以存儲4096-56 = 4040字節
,轉換成對象是4040 / 8 = 505
個,即一頁最多可以存儲505個對象
,其中第一頁有哨兵對象
只能存儲504
個。其結構圖示如下
面試題:哨兵在一個自動釋放池有幾個?
只有一個哨兵對象,且哨兵在第一頁
第一頁最多可以存
504
個對象,第二頁開始最多存505
個
2、壓棧對象 autoreleaseFast
- 進入
autoreleaseFast
源碼,主要有以下幾步:獲取當前操作頁,并判斷頁是否存在以及是否滿了
如果頁
存在,且未滿
,則通過add
方法壓棧對象如果頁
存在,且滿了
,則通過autoreleaseFullPage
方法安排新的頁面如果
頁不存在
,則通過autoreleaseNoPage
方法創建新頁
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 方法
這個方法主要是用于判斷當前頁是否已經存儲滿了,如果當前頁已經滿了,通過do-while循環
查找子節點對應的頁
,如果不存在,則新建頁
,并壓棧對象
//添加自動釋放對象,當頁滿的時候調用這個方法
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-while遍歷循環查找界面是否滿了
do {
//如果子頁面存在,則將頁面替換為子頁面
if (page->child) page = page->child;
//如果子頁面不存在,則新建頁面
else page = new AutoreleasePoolPage(page);
} while (page->full());
//設置為當前操作頁面
setHotPage(page);
//對象壓棧
return page->add(obj);
}
從AutoreleasePoolPage
初始化方法中可以看出,主要是通過操作child
對象,將當前頁的child指向新建頁面
,由此可以得出頁是通過雙向鏈表連接
add 方法
這個方法主要是添加釋放對象
,其底層是實現是通過next
指針存儲釋放對象,并將next指針遞增
,表示下一個釋放對象存儲的位置。從這里可以看出頁
是通過棧結構存儲
//添加釋放對象
id *add(id obj)
{
ASSERT(!full());
unprotect();
//傳入對象存儲的位置
id *ret = next; // faster than `return next-1` because of aliasing
//將obj壓棧到next指針位置,然后next進行++,即下一個對象存儲的位置
*next++ = obj;
protect();
return ret;
}
3、autorelease 底層分析
在demo中,我們通過autorelease
方法,在MRC模式下,將對象壓棧到自動釋放池,下面來分析其底層實現
- 查看
autorelease
方法源碼- 如果不是對象 或者 是小對象,則直接返回
- 如果是對象,則調用對象的
autorelease
進行釋放
__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
//如果不是對象,則直接返回
if (!obj) return obj;
//如果是小對象,也直接返回
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
- 進入對象的
autorelease
實現
??
inline id
objc_object::autorelease()
{
ASSERT(!isTaggedPointer());
//判斷是否是自定義類
if (fastpath(!ISA()->hasCustomRR())) {
return rootAutorelease();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}
??
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());
//autoreleaseFast 壓棧操作
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
從這里看出,無論是壓棧哨兵對象,還是普通對象
,都會來到autoreleaseFast
方法,只是區別標識不同
而以
objc_autoreleasePoolPop 源碼分析
在objc_autoreleasePoolPop
方法中有個參數,在clang分析時,發現傳入的參數是push壓棧后返回的哨兵對象
,即ctxt
,其目的是避免出棧混亂,防止將別的對象出棧
- 進入
pop
源碼實現,主要由以下幾步空頁面的處理,并
根據token獲取page
容錯處理
通過
popPage
出棧頁
//出棧
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
//判斷對象是否是空占位符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
//如果當是空占位符
// Popping the top-level placeholder pool.
//獲取當前頁
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
//如果當前頁不存在,則清除空占位符
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//如果當前頁存在,則將當前頁設置為coldPage,token設置為coldPage的開始位置
page = coldPage();
token = page->begin();
} else {
//獲取token所在的頁
page = pageForPointer(token);
}
stop = (id *)token;
//判斷最后一個位置,是否是哨兵
if (*stop != POOL_BOUNDARY) {
//最后一個位置不是哨兵,即最后一個位置是一個對象
if (stop == page->begin() && !page->parent) {
//如果是第一個位置,且沒有父節點,什么也不做
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
//如果是第一個位置,且有父節點,則出現了混亂
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
//出棧頁
return popPage<false>(token, page, stop);
}
- 進入
popPage
源碼,其中傳入的allowDebug
為false,則通過releaseUntil
出棧當前頁stop
位置之前的所有對象,即向棧中的對象發送release消息
,直到遇到傳入的哨兵對象
//出棧頁面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
//出棧當前操作頁面對象
page->releaseUntil(stop);
// memory: delete empty children 刪除空子項
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
//調試期間刪除每個特殊情況下的所有池
//獲取當前頁面的父節點
AutoreleasePoolPage *parent = page->parent;
//將當前頁面殺掉
page->kill();
//設置操作頁面為父節點頁面
setHotPage(parent);
}
else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
//特殊情況:調試丟失的自動釋放池時刪除pop(top)的所有內容
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full 如果頁面已滿一半以上,則保留一個空子級
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
- 進入
releaseUntil
實現,主要是通過循環遍歷
,判斷對象是否等于stop,其目的是釋放stop之前
的所有的對象,首先通過
獲取page的next釋放對象(即page的最后一個對象)
,并對next
進行遞減
,獲取上一個對象
判斷
是否是哨兵對象
,如果不是則自動調用objc_release
釋放
//釋放到stop位置之前的所有對象
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack 不是遞歸的:我們不想破壞堆棧
// if a thread accumulates a stupendous amount of garbage
//判斷下一個對象是否等于stop,如果不等于,則進入while循環
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects 每次從hotPage()重新啟動,以防-release自動釋放更多對象
//獲取當前操作頁面,即hot頁面
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
//如果當前頁是空的
while (page->empty()) {
//將page賦值為父節點頁
page = page->parent;
//并設置當前頁為父節點頁
setHotPage(page);
}
page->unprotect();
//next進行--操作,即出棧
id obj = *--page->next;
//將頁索引位置置為SCRIBBLE,表示已經被釋放
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
//釋放
objc_release(obj);
}
}
//設置當前頁
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}
- 進入
kill
實現,主要是銷毀當前頁,將當前頁賦值為父節點頁
,并將父節點頁的child對象指針置為nil
//銷毀
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
//獲取最后一個頁
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
//子節點 變成 父節點
page = page->parent;
if (page) {
page->unprotect();
//子節點為nil
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
總結
通過上面的分析,針對自動釋放池的push和pop,總結如下
- 在自動釋放池的
壓棧
(即push
)操作中- 當沒有pool,即只有空占位符(存儲在tls中)時,則創建頁,
壓棧哨兵對象
- 在頁中
壓棧普通對象
主要是通過next
指針遞增
進行的, - 當
頁滿
了時,需要設置頁的child
對象為新建頁
- 當沒有pool,即只有空占位符(存儲在tls中)時,則創建頁,
所以,綜上所述,autorelease
和objc_autoreleasePush
的整體底層的流程如下圖所示
- 在自動釋放池的
出棧
(即pop
)操作中- 在頁中
出棧普通對象
主要是通過next
指針遞減
進行的, - 當
頁空
了時,需要賦值頁的parent
對象為當前頁
- 在頁中
綜上所述,objc_autoreleasePoolPop
出棧的地城流程如下所示
RunLoop
對于RunLoop,主要關心的點有以下幾個
1、runloop是什么?
2、runloop和線程的關系?
3、runloop是什么時候創建的?
1、RunLoop介紹
RunLoop
是事件接收和分發機制的一個實現,是線程相關的基礎框架的一部分,一個RunLoop就是一個事件處理的循環,用來不停的調度工作以及處理輸入事件。
RunLoop
本質是一個 do-while循環
,沒事做就休息,來活了就干活。與普通的while
循環是有區別的,普通的while循環
會導致CPU進入忙等待狀態
,即一直消耗cpu,而RunLoop則不會,RunLoop是一種閑等待
,即RunLoop具備休眠功能
。
RunLoop的作用
保持程序的持續運行
處理App中的各種事件(觸摸、定時器、performSelector)
節省cpu資源,提供程序的性能,
該做事就做事,該休息就休息
RunLoop 源碼分析
RunLoop源碼的下載地址,在其中找到最新版下載即可
2、RunLoop和線程的關系
一般在日常開發中,對于RunLoop的獲取
主要有以下兩種方式
// 主運行循環
CFRunLoopRef mainRunloop = CFRunLoopGetMain();
// 當前運行循環
CFRunLoopRef currentRunloop = CFRunLoopGetCurrent();
- 進入
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;
}
- 進入
_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和線程是一一對應的
3、RunLoop的創建
- 進入
__CFRunLoopCreate
源碼,其中主要是對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
的定義,根據定義得知,其實RunLoop也是一個對象
。是__CFRunLoop
結構體的指針類型
typedef struct __CFRunLoop * CFRunLoopRef;
??
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;
};
從定義中可以得出,一個RunLoop依賴于多個Mode
,意味著一個RunLoop需要處理多個事務,即一個Mode對應多個Item
,而一個item中,包含了timer、source、observer,如下所示
Mode類型
其中mode
在蘋果文檔中提及的有五個,而在iOS中公開暴露出來的只有 NSDefaultRunLoopMode
和 NSRunLoopCommonModes
。 NSRunLoopCommonModes
實際上是一個 Mode 的集合,默認包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode
。
NSDefaultRunLoopMode
:默認
的mode,正常情況下都是在這個modeNSConnectionReplyMode
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
- po CFRunLoopCopyCurrentMode(mainRunloop)
- po CFRunLoopCopyCurrentMode(currentRunloop)
調試-1
從這里,可以說明,runloop在運行時的mode只有一個
-
獲取
mainRunloop
的所有模型,即po CFRunLoopCopyAllModes(mainRunloop)
調試-2
從結果可以驗證runloop
和CFRunloopMode
具有一對多
的關系
驗證:mode和Item也是一對多
-
在上述代碼中,加斷點,通過bt查看堆棧信息,從這里看出timer的item類型如下所示
timer的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__
Item類型源碼
-
在這里以
Timer為例
,一般初始化timer時,都會將timer通過addTimer:forMode:
方法添加到Runloop中,于是在源碼中查找addTimer
的相關方法,即CFRunLoopAddTimer
方法,其源碼實現如下,其實現主要判斷是否是kCFRunLoopCommonModes
,然后查找runloop的mode進行匹配處理- 其中
kCFRunLoopCommonModes
不是一種模式,是一種抽象的偽模式
,比defaultMode更加靈活 - 通過
CFSetAddValue(rl->_commonModeItems, rlt);
可以得知,runloop
與mode
是一對多
的,同時可以得出mode
與item
也是一對多
的
- 其中
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);
}
4、RunLoop執行
眾所周知,RunLoop的執行依賴于run
方法,從下面的堆棧信息中可以看出,其底層執行的是__CFRunLoopRun
方法
- 進入
__CFRunLoopRun
源碼,針對不同的對象,有不同的處理如果有
observer
,則調用__CFRunLoopDoObservers
如果有
block
,則調用__CFRunLoopDoBlocks
如果有
timer
,則調用__CFRunLoopDoTimers
如果是
source0
,則調用__CFRunLoopDoSources0
如果是
source1
,則調用__CFRunLoopDoSource1
/* 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);
...
}
- 進入
__CFRunLoopDoTimers
源碼,主要是通過for循環,對單個timer進行處理
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__
函數,正好與timer堆棧調用中的一致
調試
// 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,
設置Mode
,并將其加入RunLoop
中在RunLoop的
run
方法執行時,會調用__CFRunLoopDoTimers
執行所有timer在
__CFRunLoopDoTimers
方法中,會通過for循環執行單個timer的操作在
__CFRunLoopDoTimer
方法中,timer執行完畢后,會執行對應的timer回調函數
以上,是針對timer
的執行分析,對于observer、block、source0、source1
,其執行原理與timer是類似的,這里就不再重復說明以下是蘋果官方文檔針對RunLoop處理不同源的圖示
5、RunLoop 底層原理
從上述的堆棧信息中可以看出,run在底層的實現路徑為CFRunLoopRun -> CFRunLoopRun -> __CFRunLoopRun
- 進入
CFRunLoopRun
源碼,其中傳入的參數1.0e10
(科學計數) 等于 1* e^10,用于表示超時時間
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);
}
- 進入
CFRunLoopRunSpecific
源碼,,首先根據modeName找到對應的mode,然后主要分為三種情況:如果是
entry
,則通知observer,即將進入runloop
如果是
exit
,則通過observer,即將退出runloop
如果是其他中間狀態,主要是通過
runloop
處理各種源
其偽代碼表示如下
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;
}
- 進入
__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 相關
面試題1:臨時變量什么時候釋放?
如果在
正常情況
下,一般是超出其作用域就會立即釋放
如果將臨時變量加入了
自動釋放池
,會延遲釋放,即在runloop休眠或者autoreleasepool作用域之后釋放
面試題2:AutoreleasePool原理
自動釋放池的本質是一個
AutoreleasePoolPage結構體對象
,是一個棧結構存儲
的頁,每一個AutoreleasePoolPage都是以雙向鏈表
的形式連接自動釋放池的
壓棧
和出棧
主要是通過結構體的構造函數和析構函數調用底層的objc_autoreleasePoolPush
和objc_autoreleasePoolPop
,實際上是調用AutoreleasePoolPage
的push
和pop
兩個方法-
每次調用
push
操作其實就是創建一個新的AutoreleasePoolPage
,而AutoreleasePoolPage
的具體操作就是插入一個POOL_BOUNDARY
,并返回插入POOL_BOUNDARY
的內存地址。而push內部調用autoreleaseFast
方法處理,主要有以下三種情況當
page存在,且不滿
時,調用add方法將對象添加至page的next指針處,并next遞增當
page存在,且已滿
時,調用autoreleaseFullPage
初始化一個新的page,然后調用add方法將對象添加至page棧中當
page不存在
時,調用autoreleaseNoPage
創建一個hotPage,然后調用add方法將對象添加至page棧中
當執行
pop
操作時,會傳入一個值,這個值就是push操作的返回值,即POOL_BOUNDARY
的內存地址token
。所以pop內部的實現就是根據token找到哨兵對象所處的page中,然后使用objc_release
釋放 token之前的對象,并把next
指針到正確位置
面試題3:AutoreleasePool能否嵌套使用?
可以嵌套使用,其目的是可以
控制應用程序的內存峰值
,使其不要太高可以嵌套的原因是因為自動釋放池是以棧為節點,通過雙向鏈表的形式連接的,且是和線程一一對應的
自動釋放池的
多層嵌套
其實就是不停的pushs哨兵對象
,在pop時,會先釋放里面的,在釋放外面的
面試題4:哪些對象可以加入AutoreleasePool?alloc創建可以嗎?
使用
new、alloc、copy
關鍵字生成的對象和retain
了的對象需要手動釋放
,不會被添加到自動釋放池中設置為
autorelease
的對象不需要手動釋放
,會直接進入自動釋放池所有 autorelease 的對象,在出了作用域之后,會被自動添加到最近創建的自動釋放池中
面試題5:AutoreleasePool的釋放時機是什么時候?
App 啟動后,蘋果在主線程
RunLoop
里注冊了兩個Observer
,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()
。第一個 Observer 監視的事件是
Entry
(即將進入 Loop),其回調內會調用_objc_autoreleasePoolPush
()創建自動釋放池
。其 order 是 -2147483647,優先級最高
,保證創 建釋放池發生在其他所有回調之前。第二個 Observer 監視了兩個事件:
BeforeWaiting
(準備進入休眠) 時調用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創建新池
;Exit
(即 將退出 Loop) 時調用_objc_autoreleasePoolPop() 來釋放自動釋放池
。這個 Observer 的 order 是 2147483647,優先級最低
,保證其釋放池子發生在其他所有回調之后。
面試題6:thread 和 AutoreleasePool的關系
在官方文檔中,找到如下說明
Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.
大致意思如下:
每個線程
,包括主線程在內都維護了自己的自動釋放池堆棧結構
新的自動釋放池在被創建時,會被添加到
棧頂
;當自動釋放池銷毀時,會從棧中移除對于
當前線程
來說,會將自動釋放的對象
放入自動釋放池的棧頂
;在線程停止時,會自動釋放掉與該線程關聯的所有自動釋放池
總結:每個線程都有與之關聯的自動釋放池堆棧結構,新的pool在創建時會被壓棧到棧頂,pool銷毀時,會被出棧,對于當前線程來說,釋放對象會被壓棧到棧頂,線程停止時,會自動釋放與之關聯的自動釋放池
面試題7:RunLoop 和 AutoreleasePool的關系
在官方文檔中,找到如下說明
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.
大致意思如下:
主程序的
RunLoop
在每次事件循環
之前之前,會自動創建一個autoreleasePool
并且會在
事件循環
結束時,執行drain
操作,釋放其中的對象
RunLoop相關
面試題1
當前有個子線程,子線程中有個timer
。timer是否能夠執行 并進行持續的打印?
CJLThread *thread = [[CJLThread alloc] initWithBlock:^{
// thread.name = nil 因為這個變量只是捕捉
// CJLThread *thread = nil
// thread = 初始化 捕捉一個nil進來
NSLog(@"%@---%@",[NSThread currentThread],[[NSThread currentThread] name]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"hello word"); // 退出線程--結果runloop也停止了
if (self.isStopping) {
[NSThread exit];
}
}];
}];
thread.name = @"lgcode.com";
[thread start];
- 不可以,因為
子線程的runloop默認不啟動
, 需要runloop run
啟動,需要將上述代碼改成下面這樣:
//改成
CJLThread *thread = [[CJLThread alloc] initWithBlock:^{
// thread.name = nil 因為這個變量只是捕捉
// CJLThread *thread = nil
// thread = 初始化 捕捉一個nil進來
NSLog(@"%@---%@",[NSThread currentThread],[[NSThread currentThread] name]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"hello word"); // 退出線程--結果runloop也停止了
if (self.isStopping) {
[NSThread exit];
}
}];
[[NSRunLoop currentRunLoop] run];
}];
thread.name = @"lgcode.com";
[thread start];
面試題2:RunLoop和線程的關系
每個線程都有一個與之對應的RunLoop,所以
RunLoop與線程是一一對應的
,其綁定關系通過一個全局的DIctionary存儲
,線程為key,runloop為value。線程中的RunLoop主要是用來管理線程的,當線程的RunLoop開啟后,會在執行完任務后進行休眠狀態,當有事件觸發喚醒時,又開始工作,即
有活時干活,沒活就休息
主線程
的RunLoop
是默認開啟
的,在程序啟動之后,會一直運行,不會退出其他線程的RunLoop默認是不開啟的,如果需要,則手動開啟
面試3:NSRunLoop 和 CFRunLoopRef 區別
NSRunLoop
是基于CFRunLoopRef面向對象的API,是不安全
的CFRunLoopRef
是基于C語言,是線程安全
的
面試4:Runloop的mode作用是什么?
mode主要是用于指定RunLoop中事件優先級的
面試5:以+scheduledTimerWithTimeInterval:的方式觸發的timer,在滑動頁面上的列表時,timer會暫停回調, 為什么?如何解決?
timer停止的原因是因為滑動
scrollView
時,主線程的RunLoop會從NSDefaultRunLoopMode
切換到UITrackingRunLoopMode
,而timer是添加在NSDefaultRunLoopMode
。所以timer不會執行將
timer
放入NSRunLoopCommonModes
中執行
以上解釋,均為個人理解,如果不足,請留言補充,謝謝