網站首頁 編程語言 正文
使用場景
在ARC
下,AutoreleasePool
主要應用在大量創建臨時對象的場景,通過AutoreleasePool
控制內存峰值,是一個很好的選擇。
NSAutoreleasePool
在MRC
可以調用NSAutoreleasePool
使對象延遲釋放,在ARC
下這個API
已經被禁用。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // ... [pool release];
@autoreleasepool
除了NSAutoreleasePool
還可以使用@autoreleasepool
,并且蘋果推薦使用@autoreleasepool
,因為這個API
性能更好,在ARC
下依然可以使用@autoreleasepool
。
無論是MRC
還是ARC
,autorelease
最大的作用,是在大量創建對象的同時,通過修飾讓內存得到提前釋放,從而降低內存峰值。
@autoreleasepool { NSMutableArray *channelItemsJSONArray = [NSMutableArray arrayWithContentsOfFile:[self cachedChannelItemsFile]]; NSArray *items = [self channelItemsJSONArray]; if (![items writeToFile:[self cachedChannelItemsFile] atomically:YES]) { [channelItemsJSONArray writeToFile:[self cachedChannelItemsFile] atomically:YES]; } items = nil; }
__autoreleasing
在ARC
下,需要被自動釋放的對象,可以用__autoreleasing
修飾,讓對象延遲釋放。
+ (NSArray *)parseString:(NSString *)originalM3U8Str m3u8Host:(NSString *)m3u8url error:(NSError *__autoreleasing *)errorPtr;
源碼分析
__AtAutoreleasePool結構體
struct __AtAutoreleasePool { __AtAutoreleasePool() { atautoreleasepoolobj = objc_autoreleasePoolPush(); } ~__AtAutoreleasePool() { objc_autoreleasePoolPop(atautoreleasepoolobj); } void * atautoreleasepoolobj; };
@autoreleasepool
本質上會被系統轉換成C++
的__AtAutoreleasePool
結構體,@autoreleasepool
的大括號開始,對應著objc_autoreleasePoolPush
函數。大括號結束,對應著objc_autoreleasePoolPop
函數。通過clang
命令將OC
代碼轉成C++
代碼,可以看到有一個__AtAutoreleasePool
結構體。
__AtAutoreleasePool
結構體在創建的時候會執行objc_autoreleasePoolPush
函數,在釋放的時候會執行析構函數,并執行objc_autoreleasePoolPop
函數。在這兩個函數內部,會調用AutoreleasePoolPage
的push
和pop
函數。
AutoreleasePoolPage
在運行時代碼中,objc_autoreleasePoolPop
和objc_autoreleasePoolPush
,都調用了AutoreleasePoolPage
類的實現。
void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }
在AutoreleasePoolPage
的定義中,可以看到有parent
和child
的定義,當page
中對象太多存儲不下時,會創建其他的page
對象來存儲,AutoreleasePoolPage
的結構是一個雙向鏈表。在插入新的autorelease
對象時,也會從鏈表頭向后查找,直到找到未滿的page
。
class AutoreleasePoolPage { magic_t const magic; // 校驗page的結構是否完整 id *next; // 指向下一個可以存放autorelease對象的地址 pthread_t const thread; // 當前所在的線程 AutoreleasePoolPage * const parent; // 當前page的父節點 AutoreleasePoolPage *child; // 當前page的子節點 uint32_t const depth; // page的深度 uint32_t hiwat; }
AutoreleasePoolPage
是一個C++
的類,每個page
占4096
個字節,也就是16
進制的0x1000
,也就是4kb
的空間。這些空間中,其自身的成員變量只占56
個字節,也就是下面七個成員變量,每個占8
字節,總共56
個字節。其他的四千多個字節,都用來存放被autorelease
修飾的對象內存地址。
POOL_BOUNDARY
POOL_BOUNDARY
的作用是,區分不同的自動釋放池,也就是不同的@autoreleasepool
。調用push
時,會傳入POOL_BOUNDARY
并返回一個地址例如0x1038
,0x1038
是不存儲@autorelease
對象的地址的,起到一個標識作用,用來分割不同的@autoreleasepool
。
調用pop
時,會傳入end
的地址,并從后到前調用對象的release
方法,直到POOL_BOUNDARY
為止。如果存在多個page
,會從child
的page
的最末尾開始調用,直到POOL_BOUNDARY
。page
的結構是一個棧結構,釋放的時候也是從棧頂開始釋放。
next
指針指向棧頂,是棧里面很常見的一個設計。AutoreleasePoolPage
和POOL_BOUNDARY
的區別在于,AutoreleasePoolPage
負責維護存儲區域,而POOL_BOUNDARY
則負責分割存儲在page
中的對象地址,以@autoreleasepool
為單位進行分割。
多層嵌套
@autoreleasepool { NSObject *p1 = [[NSObject alloc] init]; NSObject *p2 = [[NSObject alloc] init]; @autoreleasepool { NSObject *p3 = [[NSObject alloc] init]; @autoreleasepool { NSObject *p4 = [[NSObject alloc] init]; } } }
如果是多層@autoreleasepool
的嵌套,會用同一個AutoreleasePoolPage
對象。以下面的三個嵌套為例,在同一個page
中的順序是下圖這樣。不同的@autoreleasepool
以POOL_BOUNDARY
做分割。
push
創建一個autoreleasePool
之后,就會調用push
函數。在push
函數中會判斷是否調試模式下,如果調試模式會每次生成一個新的page
。debug
環境代碼可以直接忽略,只保留autoreleaseFast
函數。
static inline void *push() { id *dest; if (DebugPoolAllocation) { dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } return dest; }
autoreleaseFast
在函數內部,會通過hotPage
獲取當前的page
,hotPage
函數內部本質上是一個page
和key
的映射。
- 如果
page
不為空并且有空間,則調用page
的add
函數將對象添加到page
中,并將POOL_BOUNDARY
添加在當前的位置。 - 如果
page
已經被創建但沒有空間,會調用autoreleaseFullPage
函數創建新的page
,并且將鏈表的末尾指向新創建的page
。 - 如果沒有創建
page
,則調用autoreleaseNoPage
函數創建一個新的page
,并且將當前線程的hotPage
設置為新創建的page
。
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
- 在
autoreleaseFullPage
函數中,會從page
的鏈表中,從前往后找到末尾的節點。 - 創建一個新的
page
,在創建函數AutoreleasePoolPage
中會處理parent
和child
指針的問題,返回的page
可以直接用。 - 調用
setHotPage
將page
設置到哈希表中,并且調用page
的add
函數將autorelease
修飾的對象,添加到page
中。
static __attribute__((noinline)) id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }
autoreleaseNoPage
autoreleaseNoPage
函數的核心代碼比較簡單,就是創建一個新的page
,隨后設置POOL_BOUNDARY
標志,并且把對象添加進去。在函數中需要留意POOL_BOUNDARY
標志,很多地方都用來做page
是否為空的判斷。
static __attribute__((noinline)) id *autoreleaseNoPage(id obj) { AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } return page->add(obj); }
add
add
函數比較簡單,核心邏輯就是將obj
放入next
指針的位置,并且對next
指針進行++
,指向下一個位置。*next++
表示先用后加,先將obj
存入next的地址,隨后+1
。
id *add(id obj) { ASSERT(!full()); unprotect(); id *ret = next; *next++ = obj; protect(); return ret; }
pop
調用pop
函數時,有三步處理。
- 判斷
autoreleasepool
是否為空,通過EMPTY_POOL_PLACEHOLDER
占位符判斷,為空則清空這個page
。 - 傳入的
stop
是否不等于POOL_BOUNDARY
標識,如果不等于則可能是一個有問題的page
。 - 調用
popPage
方法,釋放對象。
static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; // 1. if (token == (void*)EMPTY_POOL_PLACEHOLDER) { page = hotPage(); if (!page) { return setHotPage(nil); } page = coldPage(); token = page->begin(); } else { page = pageForPointer(token); } // 2. stop = (id *)token; if (*stop != POOL_BOUNDARY) { if (stop == page->begin() && !page->parent) { } else { return badPop(token); } } // 3. return popPage<false>(token, page, stop); }
popPage
popPage
函數核心代碼就是調用releaseUntil
函數,在最開始會調用releaseUntil
函數去完成釋放操作。
按照page
達到一半就擴容的原則,后面的if
語句會判斷執行pop
后page
鏈表的狀態。
如果少于半滿,就將子節點刪除。
如果大于半滿,則保留子節點,并刪除后面的節點。
static void popPage(void *token, AutoreleasePoolPage *page, id *stop) { page->releaseUntil(stop); if (page->child) { if (page->lessThanHalfFull()) { page->child->kill(); } else if (page->child->child) { page->child->child->kill(); } } }
releaseUntil
在releaseUntil
函數內部,核心邏輯是從當前page
,從后到前調用objc_release
,釋放被autorelease
修飾的對象。
- 獲取當前的
hotPage
。 - 判斷
page
是否為空,如果為空則表示里面的對象被釋放完,則將page
的父節點page
設置為hotPage
。 - 獲得上一個節點,
->
的算數優先級比--
要高,所以是先通過next
獲取當前節點地址,這是一個為空的待存入節點,隨后執行--
操作獲取上一個對象地址。 - 通過
memset
將上一個節點釋放。 - 判斷上一個節點是否占位符號
POOL_BOUNDARY
,如果不是則調用objc_release
釋放對象。 - 在
while
循環結束后,將當前page
設置為hotPage
。
void releaseUntil(id *stop) { while (this->next != stop) { AutoreleasePoolPage *page = hotPage(); while (page->empty()) { page = page->parent; setHotPage(page); } page->unprotect(); id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); if (obj != POOL_BOUNDARY) { objc_release(obj); } } setHotPage(this); }
autorelease
對象調用autorelease
方法會被編譯器轉換為objc_autoreleaseReturnValue
方法,并且經過多層調用,會來到底層的autorelease
函數。
在這個函數中會判斷傳入的對象是否tagged pointer
,因為tagged pointer
沒有引用計數的概念。隨后會調用autoreleaseFast
函數,函數內部調用add
函數將obj
對象加入到page
中,并且會判斷是否需要創建新的page
。
static inline id autorelease(id obj) { assert(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); return obj; }
hotPage
hotPage
可以被理解為,page
鏈表的末尾,也就是調用push
函數被插入的位置。執行hotPage
函數獲取,以及調用setHotPage
設置,都是操作的鏈表的末尾page
。
AutoreleasePoolPage
對象和線程一一對應,并且都被存儲在tls
的哈希表中。通過tls_get_direct
函數并傳入key
可以獲取到對應的自動釋放池。
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; }
hotPage
函數中的判斷是下面的定義,這個標示意思是當前page
為空,也就是從未存儲過任何對象。是一個標志位,下面是標志位的定義。
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
coldPage
coldPage
只有獲取函數,沒有設置函數。這是因為coldPage
函數本質上,就是尋找page
鏈表的根節點,從源碼中的while
循環可以看到。
static inline AutoreleasePoolPage *coldPage() { AutoreleasePoolPage *result = hotPage(); if (result) { while (result->parent) { result = result->parent; result->fastcheck(); } } return result; }
調試
_objc_autoreleasePoolPrint
如果想調試自動釋放池,可以通過_objc_autoreleasePoolPrint
私有API
來進行。將項目改為MRC
,并且在命令行項目中增加下面這些調試代碼。
int main(int argc, const char * argv[]) { _objc_autoreleasePoolPrint(); // print1 @autoreleasepool { _objc_autoreleasePoolPrint(); // print2 Person *p1 = [[[Person alloc] init] autorelease]; Person *p2 = [[[Person alloc] init] autorelease]; _objc_autoreleasePoolPrint(); // print3 } _objc_autoreleasePoolPrint(); // print4 return 0; }
打印結果如下,可以看到POOL_BOUNDARY
在page
中也占了一個位置。
objc[68122]: ############## (print1) objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0 objc[68122]: 0 releases pending. // 當前自動釋放池中沒有任何對象 objc[68122]: [0x102802000] ................ PAGE (hot) (cold) objc[68122]: ############## objc[68122]: ############## (print2) objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0 objc[68122]: 1 releases pending. // 當前自動釋放池中有1個對象,這個對象為POOL_BOUNDARY objc[68122]: [0x102802000] ................ PAGE (hot) (cold) objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY objc[68122]: ############## objc[68122]: ############## (print3) objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0 objc[68122]: 3 releases pending. // 當前自動釋放池中有3個對象 objc[68122]: [0x102802000] ................ PAGE (hot) (cold) objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY objc[68122]: [0x102802040] 0x100704a10 HTPerson //p1 objc[68122]: [0x102802048] 0x10075cc30 HTPerson //p2 objc[68122]: ############## objc[68156]: ############## (print4) objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0 objc[68156]: 0 releases pending. // 當前自動釋放池中沒有任何對象,因為@autoreleasepool作用域結束,調用pop方法釋放了對象 objc[68156]: [0x100810000] ................ PAGE (hot) (cold) objc[68156]: ##############
UIApplicationMain
項目中經常會看到下面的代碼,很多人的解釋是“這個autoreleasepool
是為了釋放主線程的autorelease
對象的”。但是,這個說法是錯誤的。autoreleasepool
只負責自己作用域中添加的對象,而主線程在運行過程中,也會隱式創建autoreleasepool
對象,這個pool
是包含在main
函數的pool
里面的。
所以,主線程runloop
每次執行循環后,釋放的對象是主線程的。而main
函數的autoreleasepool
釋放的,是main
函數中直接創建的對象。
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
釋放時機
區分
如果是在viewDidLoad
方法中創建一個autorelease
對象,并不是在這個方法結束后釋放對象,這個說法是錯誤的。即便執行到viewDidAppear
,依然不會釋放對象。
被autorelease
修飾的對象,釋放時機有兩種。
- 如果通過代碼添加一個
autoreleasepool
,在作用域結束時,隨著pool
的釋放,就會釋放pool
中的對象。這種情況是及時釋放的,并不依賴于runloop
。 - 另一種就是由系統自動進行釋放,系統會在
runloop
開始的時候創建一個pool
,結束的時候會對pool
中的對象執行release
操作。
runloop
如果是系統創建的pool
,需要手動開啟runloop
,主線程默認已經開啟并運行,子線程需要調用currentRunLoop
方法開啟并運行runloop
,子線程中系統創建pool
的流程才會正常工作。
包括主線程在內的每個線程,如果在線程中使用到了AutoreleasePool
,則會創建兩個Observer
并添加到當前線程的Runloop
中,通過這兩個Observer
進行對象的自動內存管理。
// activities = 0x1,kCFRunLoopEntry <CFRunLoopObserver 0x60000012f000 [0x1135c2bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)} // activities = 0xa0,kCFRunLoopBeforeWaiting | kCFRunLoopExit <CFRunLoopObserver 0x60000012ef60 [0x1135c2bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
首先會創建一個Observer
并監聽kCFRunLoopEntry
消息,時機是在進入Runloop
前,此Observer
的優先級設置為-2147483647
的最高優先級,以保證回調發生在Runloop
其他事件前。
然后創建另一個Observer
,并監聽kCFRunLoopBeforeWaiting
和kCFRunLoopExit
消息,時機分別在進入Runloop
休眠和退出Runloop
時,將Observer
的優先級設置為2147483647
,以保證回調發生在Runloop
其他事件之后。
兩個Observer
都有相同的回調函數_wrapRunLoopWithAutoreleasePoolHandler
,在第一次回調時會在內部調用_objc_autoreleasePoolPush
函數,創建自動釋放池。
在kCFRunLoopBeforeWaiting
將要進入休眠前,調用_objc_autoreleasePoolPop
函數釋放自動釋放池中的對象,并調用_objc_autoreleasePoolPush
函數創建一個新的釋放池。在kCFRunLoopExit
將要退出Runloop
時調用_objc_autoreleasePoolPop
函數,釋放自動釋放池中的對象。
原文鏈接:https://juejin.cn/post/7141285896250195982
相關推薦
- 2022-01-27 laravel JWTAuth對api接口權限進行鑒權
- 2022-06-28 C#反射調用dll文件中的方法操作泛型與屬性字段_C#教程
- 2023-07-27 element plus 循環form表單校驗
- 2022-07-02 C語言由淺入深理解指針_C 語言
- 2022-12-15 Oracle?listagg去重distinct的三種方式總結_oracle
- 2022-09-17 C++?中如何結束?while?(cin>>str)?的輸入_C 語言
- 2023-10-28 C++?string和wstring相互轉換方式_C 語言
- 2022-12-09 Python中的main函數與import用法_python
- 最近更新
-
- 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同步修改后的遠程分支