iOS底層探索之自動釋放池原理解析
1、概述
- OC 中的一種
內(nèi)存自動回收機制
,它可以將加入AutoreleasePool
中的變量release
的時機延遲 - 當創(chuàng)建一個對象,在正常情況下,變量會在
超出其作用域時
立即 release ,如果將其加入到自動釋放池中,這個對象并不會立即釋放,而會等到runloop 休眠 / 超出autoreleasepool作用域之后
進行釋放
從程序啟動到加載完成,主線程對應(yīng)的 Runloop 會處于休眠狀態(tài),等待用戶交互來喚醒 Runloop
用戶每次交互都會啟動一次 Runloop ,用于處理用戶的所有點擊、觸摸等事件
Runloop 在監(jiān)聽到交互事件后,就會創(chuàng)建自動釋放池,并將所有延遲釋放的對象添加到自動釋放池中
在一次完整的 Runloop 結(jié)束之前,會向自動釋放池中所有對象發(fā)送 release 消息,然后銷毀自動釋放池
2、底層探索
準備簡單代碼
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); } return 0; }
轉(zhuǎn)換成.cpp文件:
clang -rewrite-objc main.m -o main.cpp
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSLog((NSString *)&__NSConstantStringImpl__var_folders_jl_d06jlfkj2ws74_5g45kms07m0000gn_T_main_da0d58_mi_0); } return 0; }
- autoreleasepool 變成了
__AtAutoreleasePool
類型聲明的代碼
struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };
- 前邊了解過這種寫法是在結(jié)構(gòu)體 構(gòu)造 時調(diào)用
objc_autoreleasePoolPush
函數(shù),在 結(jié)構(gòu)體退出作用域析構(gòu) 時調(diào)用objc_autoreleasePoolPop
函數(shù),這兩個函數(shù)也是下邊研究的重點(在 main 函數(shù)中 autoreleasepool 處設(shè)置斷點查看匯編也可以看到這兩個函數(shù)的符號調(diào)用)
2.1、打印自動釋放池結(jié)構(gòu)
- 測試項目,關(guān)閉
ARC
模式
- 手動添加一個對象到自動釋放池,并打印自動釋放池結(jié)構(gòu)
// 導(dǎo)入 _objc_autoreleasePoolPrint 函數(shù),用于打印自動釋放池的結(jié)構(gòu) extern void _objc_autoreleasePoolPrint(void); int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; _objc_autoreleasePoolPrint(); } return 0; }
############## AUTORELEASE POOLS for thread 0x1000ebe00 2 releases pending. [0x10700b000] ................ PAGE (hot) (cold) [0x10700b038] ################ POOL 0x10700b038 [0x10700b040] 0x100705f60 NSObject ##############
_objc_autoreleasePoolPrint
調(diào)用 AutoreleasePoolPage::printAll()(通過AutoreleasePoolPage 的命名空間調(diào)用printAll()
);按照自動釋放池的結(jié)構(gòu),通過雙向鏈表遍歷page
,依次讀取 page 中的內(nèi)容并進行打印- 打印了當前自動釋放池所屬線程,與 2 個需要釋放的對象:
哨兵對象:POOL
和 手動加入自動釋放池 的對象objc - 當前的
Page
信息,占56字節(jié)
,因為只有一頁,即是冷頁面,也是熱頁面
2.2、objc_autoreleasePoolPush
void * objc_autoreleasePoolPush(void) { // 調(diào)用 AutoreleasePoolPage 命名空間下的 push 函數(shù) return AutoreleasePoolPage::push(); }
2.2.1、AutoreleasePoolPage
AutoreleasePoolPage
的定義,能看到這樣一段注釋
/*********************************************************************** 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. 堆棧被分成一個雙鏈接的頁面列表。根據(jù)需要添加和刪除頁面 Thread-local storage points to the hot page, where newly autoreleased objects are stored. 線程本地存儲指向熱頁,其中存儲新自動釋放的對象 **********************************************************************/
AutoreleasePoolPage 繼承于AutoreleasePoolPageData
(有用的內(nèi)容基本都在 AutoreleasePoolPageData 結(jié)構(gòu)體中)
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: static pthread_key_t const key = AUTORELEASE_POOL_KEY; static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing static size_t const COUNT = SIZE / sizeof(id); static size_t const MAX_FAULTS = 2; ... }
2.2.2、AutoreleasePoolPageData
class AutoreleasePoolPage; struct AutoreleasePoolPageData { #if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS struct AutoreleasePoolEntry { uintptr_t ptr: 48; uintptr_t count: 16; static const uintptr_t maxCount = 65535; // 2^16 - 1 }; static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!"); #endif magic_t const magic; __unsafe_unretained id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; 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) { } };
結(jié)構(gòu)體中,包含以下成員變量:(根據(jù)下邊的成員大小得出 一個page占56字節(jié))
magic
:用來校驗 AutoreleasePoolPage 的結(jié)構(gòu)是否完整(16字節(jié))next
:指向最新添加的autoreleased
對象的下一個位置,初始化時執(zhí)行begin()
:獲取對象壓棧的起始位置(8字節(jié))thread
:指向當前線程(8字節(jié))parent
:指向父節(jié)點,第一個節(jié)點的parent
值為nil
(8字節(jié))child
:指向子節(jié)點,最后一個節(jié)點的child
值為nil
(8字節(jié))depth
:代表深度,從0
開始,往后遞增1
(4字節(jié))hiwat
:代表high water mark
最大入棧數(shù)量標記(4字節(jié))
2.2.3、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; }
DebugPoolAllocation
:當自動釋放池按順序彈出時停止,并允許堆調(diào)試器跟蹤自動釋放池- 不存在,調(diào)用
autoreleaseNewPage
函數(shù),從一個新的池頁開始創(chuàng)建 - 否則,調(diào)用
autoreleaseFast
函數(shù),將哨兵對象壓棧
autoreleaseFast
- 若存在 page,且未存滿,調(diào)用
add
函數(shù) - 若存在 page,但已存滿,調(diào)用
autoreleaseFullPage
函數(shù)遍歷鏈表,找到最后一個空白的子頁面
對其進行創(chuàng)建新頁
設(shè)置為熱頁面
添加對象
- 不存在 page,調(diào)用
autoreleaseNoPage
函數(shù)調(diào)用 AutoreleasePoolPage 構(gòu)造函數(shù),創(chuàng)建新頁- 通過父類 AutoreleasePoolPageData 進行初始化
begin
:獲取對象壓棧的起始位置(sizeof(*this)
:大小取決于自身結(jié)構(gòu)體中的成員變量、返回對象可壓棧的真正開始地址,在成員變量以下)objc_thread_self
:通過tls
獲取當前線程- 鏈接雙向鏈表
設(shè)置為熱頁面
pushExtraBoundary
為YES
,哨兵對象壓棧對象壓棧
2.2.4、池頁容量
int main(int argc, const char * argv[]) { @autoreleasepool { for (int i = 0; i < 505; i++) { NSObject *objc = [[[NSObject alloc] init] autorelease]; } _objc_autoreleasePoolPrint(); } return 0; } ------------------------- objc[1804]: ############## objc[1804]: AUTORELEASE POOLS for thread 0x1000ebe00 objc[1804]: 506 releases pending. objc[1804]: [0x10200c000] ................ PAGE (full) (cold) objc[1804]: [0x10200c038] ################ POOL 0x10200c038 objc[1804]: [0x10200c040] 0x100638420 NSObject objc[1804]: [0x10200c048] 0x100637a40 NSObject objc[1804]: [0x10200c050] 0x100636970 NSObject ... objc[1804]: [0x100809000] ................ PAGE (hot) objc[1804]: [0x100809038] 0x10063a0b0 NSObject objc[1804]: ##############
- 505 個 NSObject 對象循環(huán)加入自動釋放池,當存儲 504 個對象時,池頁已滿,第 505 個對象創(chuàng)建新池頁存儲
- 一頁的容量:
504 * 8 = 4032
,加上56字節(jié)
成員變量和8字節(jié)
哨兵對象,共計4096
字節(jié) - 每一頁都存在
56字節(jié)
的成員變量 - 一個自動釋放池,只會壓棧一個哨兵對象
2.3、objc_autoreleasePoolPop
void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }
2.3.1、pop(對象出棧)
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. //不存在熱頁面,將標記設(shè)置為nil return setHotPage(nil); } // Pool was used. Pop its contents normally. // Pool pages remain allocated for re-use as usual. //存在熱頁面,通過雙向鏈表循環(huán)向上找到最冷頁面 page = coldPage(); //將token設(shè)置為起始位置 token = page->begin(); } else { //獲取token所在的頁 page = pageForPointer(token); } //賦值給stop 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 //最冷頁面的起始可能不是POOL_BOUNDARY: //1. 彈出頂級池,保留冷頁面 //2. 對象在沒有池的情況下被自動釋放 } else { // Error. For bincompat purposes this is not // fatal in executables built with old SDKs. //出現(xiàn)異常情況 return badPop(token); } } if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) { return popPageDebug(token, page, stop); } //出棧 return popPage<false>(token, page, stop); }
2.3.2、popPage
static void popPage(void *token, AutoreleasePoolPage *page, id *stop) { if (allowDebug && PrintPoolHiwat) printHiwat(); //當前頁中對象出棧,到stop位置停止 page->releaseUntil(stop); // memory: delete empty children if (allowDebug && DebugPoolAllocation && page->empty()) { // special case: delete everything during page-per-pool debugging //特殊情況:在逐頁池調(diào)試期間刪除所有內(nèi)容 //獲取父頁面 AutoreleasePoolPage *parent = page->parent; //銷毀當前頁面 page->kill(); //將父頁面設(shè)置為熱頁面 setHotPage(parent); } else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) { // special case: delete everything for pop(top) // when debugging missing autorelease pools //特殊情況:刪除所有的pop //銷毀當前頁面 page->kill(); //將熱頁面標記設(shè)置為nil 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(); } } }
2.3.3、releaseUntil
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 (this->next != stop) { // Restart from hotPage() every time, in case -release // autoreleased more objects //獲取熱頁面 AutoreleasePoolPage *page = hotPage(); // fixme I think this `while` can be `if`, but I can't prove it //如果當前頁面中沒有對象 while (page->empty()) { //獲取父頁面 page = page->parent; //標記為熱頁面 setHotPage(page); } page->unprotect(); #if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next; // create an obj with the zeroed out top byte and release that id obj = (id)entry->ptr; int count = (int)entry->count; // grab these before memset #else //內(nèi)存平移,獲取對象 id obj = *--page->next; #endif memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); //當前對象不是哨兵對象 if (obj != POOL_BOUNDARY) { #if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS // release count+1 times since it is count of the additional // autoreleases beyond the first one for (int i = 0; i < count + 1; i++) { objc_release(obj); } #else //將其釋放 objc_release(obj); #endif } } //將當前頁面標記為熱頁面 setHotPage(this); #if DEBUG // we expect any children to be completely empty for (AutoreleasePoolPage *page = child; page; page = page->child) { ASSERT(page->empty()); } #endif }
2.3.4、kill
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; //循環(huán)找到最后一個子頁面 while (page->child) page = page->child; AutoreleasePoolPage *deathptr; do { deathptr = page; //找到父頁面 page = page->parent; if (page) { //將子頁面設(shè)置為nil page->unprotect(); page->child = nil; page->protect(); } //銷毀子頁面 delete deathptr; //遍歷銷毀到this為止 } while (deathptr != this); }
3、嵌套使用
int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; } _objc_autoreleasePoolPrint(); } return 0; } ------------------------- objc[2511]: ############## objc[2511]: AUTORELEASE POOLS for thread 0x1000ebe00 objc[2511]: 4 releases pending. objc[2511]: [0x10680d000] ................ PAGE (hot) (cold) objc[2511]: [0x10680d038] ################ POOL 0x10680d038 objc[2511]: [0x10680d040] 0x101370c40 NSObject objc[2511]: [0x10680d048] ################ POOL 0x10680d048 objc[2511]: [0x10680d050] 0x101365fb0 NSObject objc[2511]: ##############
- 線程的自動釋放池是一個指針堆棧,當嵌套使用時,添加好各自堆棧的哨兵對象;出棧時,先釋放內(nèi)部,再釋放外部
總結(jié)
結(jié)構(gòu):
- 自動釋放池的壓棧和出棧,通過結(jié)構(gòu)體的構(gòu)造函數(shù)和析構(gòu)函數(shù)觸發(fā)
- 壓棧:調(diào)用
objc_autoreleasePoolPush
函數(shù) - 出棧:調(diào)用
objc_autoreleasePoolPop
函數(shù)
特點:
- 自動釋放池是一個存儲指針的棧結(jié)構(gòu)
- 指針要么是一個要釋放的對象,要么是
POOL_BOUNDARY
自動釋放池邊界,俗稱:哨兵對象
- 哨兵對象的作用:當自動釋放池將對象進行
pop
操作時,需要知道邊界在哪里,否則會破壞別人的內(nèi)存空間。而哨兵對象,就是邊界標識 - 自動釋放池的??臻g被分成一個 雙鏈接 結(jié)構(gòu)的頁面列表,可添加和刪除頁面
- 雙向鏈表的特別,一個頁中同時存在父節(jié)點和子節(jié)點??上蚯罢业礁疙撁?,也可向后找到子頁面
- 線程本地存儲指向熱頁,其中存儲新自動釋放的對象
- 棧原則,先進后出,可以理解為最后一個頁面就是熱頁。里面的對象最后被
push
,最先被pop
容量:
- 池頁大小為
4096字節(jié)
,每一頁都包含56字節(jié)
的成員變量,但一個自動釋放池中,只會壓棧一個哨兵對象,占8字節(jié)
原理:
自動釋放池的本質(zhì)是
__AtAutoreleasePool
結(jié)構(gòu)體,包含構(gòu)造函數(shù)和析構(gòu)函數(shù)結(jié)構(gòu)體聲明,觸發(fā)構(gòu)造函數(shù),調(diào)用
objc_autoreleasePoolPush
函數(shù),本質(zhì)是對象壓棧的push
方法當結(jié)構(gòu)體出作用域空間,觸發(fā)析構(gòu)函數(shù),調(diào)用
objc_autoreleasePoolPop
函數(shù),本質(zhì)是對象出棧的pop
方法對象壓棧
- 如果存在
page
,并且沒有存滿,調(diào)用add
函數(shù)- 使用
*next++
進行內(nèi)存平移 - 將對象壓棧
- 如果存在
page
,但存儲已滿,調(diào)用autoreleaseFullPage
函數(shù) - 遍歷鏈表,找到最后一個空白的子頁面
- 對其進行創(chuàng)建新頁
- 設(shè)置為熱頁面
- 添加對象
- 使用
- 否則,不存在
page
,調(diào)用autoreleaseNoPage
函數(shù)- 通過父類
AutoreleasePoolPageData
進行初始化 begin
:獲取對象壓棧的起始位置objc_thread_self
:通過tls
獲取當前線程- 鏈接雙向鏈表
- 設(shè)置為熱頁面
pushExtraBoundary
為YES
,哨兵對象壓棧- 對象壓棧
- 通過父類
- 如果存在
對象出棧
- 調(diào)用
popPage
函數(shù),傳入stop
為哨兵對象的位置 - 當前頁中對象出棧,到
stop
位置停止 - 調(diào)用
kill
函數(shù),銷毀當前頁面
- 調(diào)用
嵌套使用:
- 線程的自動釋放池是一個指針堆棧,當嵌套使用時,添加好各自堆棧的哨兵對象。出棧時,先釋放內(nèi)部,再釋放外部
ARC
模式:
ARC
模式,使用alloc
、new
、copy
、mutableCopy
前綴開頭的方法進行對象創(chuàng)建,不會加入到自動釋放池;它們的空間開辟由開發(fā)者申請,釋放也由開發(fā)者進行管理
與線程的關(guān)系:
- 每個線程(包括主線程)維護自己的對象堆棧。隨著新池的創(chuàng)建,它們被添加到堆棧的頂部。當池被釋放時,它們會從堆棧中移除
autoreleased
對象被放置在當前線程的頂部自動釋放池中;當一個線程終止時,它會自動清空所有與其關(guān)聯(lián)的自動釋放池
與 Runloop 的關(guān)系:
- 主程序在事件循環(huán)的每個循環(huán)開始時在主線程上創(chuàng)建一個自動釋放池
- 并在結(jié)束時將其排空,從而釋放在處理事件時生成的任何自動釋放對象
使用:
- for循環(huán)中大量創(chuàng)建對象時,使用 autorelease 可以有效控制內(nèi)存的快速增長(原因是釋放沒有創(chuàng)建快,如果不加 autorelease 最終內(nèi)存也會降下來,但可以 減少內(nèi)存峰值)
for (int i = 0; i<100000000; i++) { @autoreleasepool { NSLog(@"%d",i); __autoreleasing LZPerson *p =[LZPerson new]; } }
到此這篇關(guān)于iOS底層探索之自動釋放池的文章就介紹到這了,更多相關(guān)iOS自動釋放池內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
iOS開發(fā)教程之識別圖片中二維碼功能的實現(xiàn)
長按識別二維碼這個功能相信對大家來說都不陌生,最近工作中就遇到了這個需求,所以下面這篇文章主要給大家介紹了關(guān)于利用iOS識別圖片中二維碼的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2018-07-07