網站首頁 編程語言 正文
1、概述
-
OC 中的一種
內存自動回收機制
,它可以將加入AutoreleasePool
中的變量release
的時機延遲 - 當創建一個對象,在正常情況下,變量會在
超出其作用域時
立即 release ,如果將其加入到自動釋放池中,這個對象并不會立即釋放,而會等到runloop 休眠 / 超出autoreleasepool作用域之后
進行釋放
從程序啟動到加載完成,主線程對應的 Runloop 會處于休眠狀態,等待用戶交互來喚醒 Runloop
用戶每次交互都會啟動一次 Runloop ,用于處理用戶的所有點擊、觸摸等事件
Runloop 在監聽到交互事件后,就會創建自動釋放池,并將所有延遲釋放的對象添加到自動釋放池中
在一次完整的 Runloop 結束之前,會向自動釋放池中所有對象發送 release 消息,然后銷毀自動釋放池
2、底層探索
準備簡單代碼
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); } return 0; }
轉換成.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; };
- 前邊了解過這種寫法是在結構體 構造 時調用
objc_autoreleasePoolPush
函數,在 結構體退出作用域析構 時調用objc_autoreleasePoolPop
函數,這兩個函數也是下邊研究的重點(在 main 函數中 autoreleasepool 處設置斷點查看匯編也可以看到這兩個函數的符號調用)
2.1、打印自動釋放池結構
- 測試項目,關閉
ARC
模式
- 手動添加一個對象到自動釋放池,并打印自動釋放池結構
// 導入 _objc_autoreleasePoolPrint 函數,用于打印自動釋放池的結構 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
調用 AutoreleasePoolPage::printAll()(通過AutoreleasePoolPage 的命名空間調用printAll()
);按照自動釋放池的結構,通過雙向鏈表遍歷page
,依次讀取 page 中的內容并進行打印 - 打印了當前自動釋放池所屬線程,與 2 個需要釋放的對象:
哨兵對象:POOL
和 手動加入自動釋放池 的對象objc - 當前的
Page
信息,占56字節
,因為只有一頁,即是冷頁面,也是熱頁面
2.2、objc_autoreleasePoolPush
void * objc_autoreleasePoolPush(void) { // 調用 AutoreleasePoolPage 命名空間下的 push 函數 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. 堆棧被分成一個雙鏈接的頁面列表。根據需要添加和刪除頁面 Thread-local storage points to the hot page, where newly autoreleased objects are stored. 線程本地存儲指向熱頁,其中存儲新自動釋放的對象 **********************************************************************/
AutoreleasePoolPage 繼承于AutoreleasePoolPageData
(有用的內容基本都在 AutoreleasePoolPageData 結構體中)
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) { } };
結構體中,包含以下成員變量:(根據下邊的成員大小得出 一個page占56字節)
magic
:用來校驗 AutoreleasePoolPage 的結構是否完整(16字節)next
:指向最新添加的autoreleased
對象的下一個位置,初始化時執行begin()
:獲取對象壓棧的起始位置(8字節)thread
:指向當前線程(8字節)parent
:指向父節點,第一個節點的parent
值為nil
(8字節)child
:指向子節點,最后一個節點的child
值為nil
(8字節)depth
:代表深度,從0
開始,往后遞增1
(4字節)hiwat
:代表high water mark
最大入棧數量標記(4字節)
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
:當自動釋放池按順序彈出時停止,并允許堆調試器跟蹤自動釋放池 - 不存在,調用
autoreleaseNewPage
函數,從一個新的池頁開始創建 - 否則,調用
autoreleaseFast
函數,將哨兵對象壓棧
autoreleaseFast
- 若存在 page,且未存滿,調用
add
函數 - 若存在 page,但已存滿,調用
autoreleaseFullPage
函數遍歷鏈表,找到最后一個空白的子頁面
對其進行創建新頁
設置為熱頁面
添加對象
- 不存在 page,調用
autoreleaseNoPage
函數調用 AutoreleasePoolPage 構造函數,創建新頁- 通過父類 AutoreleasePoolPageData 進行初始化
-
begin
:獲取對象壓棧的起始位置(sizeof(*this)
:大小取決于自身結構體中的成員變量、返回對象可壓棧的真正開始地址,在成員變量以下) -
objc_thread_self
:通過tls
獲取當前線程 - 鏈接雙向鏈表
設置為熱頁面
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 對象循環加入自動釋放池,當存儲 504 個對象時,池頁已滿,第 505 個對象創建新池頁存儲
- 一頁的容量:
504 * 8 = 4032
,加上56字節
成員變量和8字節
哨兵對象,共計4096
字節 - 每一頁都存在
56字節
的成員變量 - 一個自動釋放池,只會壓棧一個哨兵對象
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. //不存在熱頁面,將標記設置為nil return setHotPage(nil); } // Pool was used. Pop its contents normally. // Pool pages remain allocated for re-use as usual. //存在熱頁面,通過雙向鏈表循環向上找到最冷頁面 page = coldPage(); //將token設置為起始位置 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. //出現異常情況 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 //特殊情況:在逐頁池調試期間刪除所有內容 //獲取父頁面 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 //銷毀當前頁面 page->kill(); //將熱頁面標記設置為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 //內存平移,獲取對象 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; //循環找到最后一個子頁面 while (page->child) page = page->child; AutoreleasePoolPage *deathptr; do { deathptr = page; //找到父頁面 page = page->parent; if (page) { //將子頁面設置為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]: ##############
- 線程的自動釋放池是一個指針堆棧,當嵌套使用時,添加好各自堆棧的哨兵對象;出棧時,先釋放內部,再釋放外部
總結
結構:
- 自動釋放池的壓棧和出棧,通過結構體的構造函數和析構函數觸發
- 壓棧:調用
objc_autoreleasePoolPush
函數 - 出棧:調用
objc_autoreleasePoolPop
函數
特點:
- 自動釋放池是一個存儲指針的棧結構
- 指針要么是一個要釋放的對象,要么是
POOL_BOUNDARY
自動釋放池邊界,俗稱:哨兵對象
- 哨兵對象的作用:當自動釋放池將對象進行
pop
操作時,需要知道邊界在哪里,否則會破壞別人的內存空間。而哨兵對象,就是邊界標識 - 自動釋放池的棧空間被分成一個 雙鏈接 結構的頁面列表,可添加和刪除頁面
- 雙向鏈表的特別,一個頁中同時存在父節點和子節點。可向前找到父頁面,也可向后找到子頁面
- 線程本地存儲指向熱頁,其中存儲新自動釋放的對象
- 棧原則,先進后出,可以理解為最后一個頁面就是熱頁。里面的對象最后被
push
,最先被pop
容量:
- 池頁大小為
4096字節
,每一頁都包含56字節
的成員變量,但一個自動釋放池中,只會壓棧一個哨兵對象,占8字節
原理:
自動釋放池的本質是
__AtAutoreleasePool
結構體,包含構造函數和析構函數結構體聲明,觸發構造函數,調用
objc_autoreleasePoolPush
函數,本質是對象壓棧的push
方法當結構體出作用域空間,觸發析構函數,調用
objc_autoreleasePoolPop
函數,本質是對象出棧的pop
方法-
對象壓棧
- 如果存在
page
,并且沒有存滿,調用add
函數- 使用
*next++
進行內存平移 - 將對象壓棧
- 如果存在
page
,但存儲已滿,調用autoreleaseFullPage
函數 - 遍歷鏈表,找到最后一個空白的子頁面
- 對其進行創建新頁
- 設置為熱頁面
- 添加對象
- 使用
- 否則,不存在
page
,調用autoreleaseNoPage
函數- 通過父類
AutoreleasePoolPageData
進行初始化 -
begin
:獲取對象壓棧的起始位置 -
objc_thread_self
:通過tls
獲取當前線程 - 鏈接雙向鏈表
- 設置為熱頁面
-
pushExtraBoundary
為YES
,哨兵對象壓棧 - 對象壓棧
- 通過父類
- 如果存在
-
對象出棧
- 調用
popPage
函數,傳入stop
為哨兵對象的位置 - 當前頁中對象出棧,到
stop
位置停止 - 調用
kill
函數,銷毀當前頁面
- 調用
嵌套使用:
- 線程的自動釋放池是一個指針堆棧,當嵌套使用時,添加好各自堆棧的哨兵對象。出棧時,先釋放內部,再釋放外部
ARC
模式:
-
ARC
模式,使用alloc
、new
、copy
、mutableCopy
前綴開頭的方法進行對象創建,不會加入到自動釋放池;它們的空間開辟由開發者申請,釋放也由開發者進行管理
與線程的關系:
- 每個線程(包括主線程)維護自己的對象堆棧。隨著新池的創建,它們被添加到堆棧的頂部。當池被釋放時,它們會從堆棧中移除
-
autoreleased
對象被放置在當前線程的頂部自動釋放池中;當一個線程終止時,它會自動清空所有與其關聯的自動釋放池
與 Runloop 的關系:
- 主程序在事件循環的每個循環開始時在主線程上創建一個自動釋放池
- 并在結束時將其排空,從而釋放在處理事件時生成的任何自動釋放對象
使用:
- for循環中大量創建對象時,使用 autorelease 可以有效控制內存的快速增長(原因是釋放沒有創建快,如果不加 autorelease 最終內存也會降下來,但可以 減少內存峰值)
for (int i = 0; i<100000000; i++) { @autoreleasepool { NSLog(@"%d",i); __autoreleasing LZPerson *p =[LZPerson new]; } }
原文鏈接:https://juejin.cn/post/7114677313718976519
相關推薦
- 2022-01-31 element-ui upload組件 上傳文件類型限制
- 2023-03-29 基于WPF實現多選下拉控件的示例代碼_C#教程
- 2022-03-29 C#算法之無重復字符的最長子串_C#教程
- 2022-07-19 安卓TextView的lineHeight*lineCount!=height問題,解決不支持滾動的
- 2023-12-06 Warn: Could not find @TableId
- 2022-10-24 vscode使用Eslint+Prettier格式化代碼的詳細操作_C 語言
- 2022-07-18 Column count doesn’t match value count at row 1
- 2023-10-31 WebSocket消息推送
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支