內存管理一直是Objective-C
的重點,在MRC
環境下,經過調用[obj autorelease]
來延遲內存的釋放,在如今ARC環境下,咱們都知道編譯器會在合適的地方插入release/autorelease
內存釋放語句,咱們甚至能夠不須要知道Autorelease
就能很好的管理內存。雖然如今已經幾乎用不到MRC
,可是瞭解 Objective-C
的內存管理機制仍然是十分必要的,看看編譯器幫助咱們怎麼來管理內存。本文僅僅是記錄本身的學習筆記。緩存
AutoreleasePool
:自動釋放池是 Objective-C
開發中的一種自動內存回收管理的機制,爲了替代開發人員手動管理內存,實質上是使用編譯器在適當的位置插入release
、autorelease
等內存釋放操做。當對象調用 autorelease
方法後會被放到自動釋放池中延遲釋放時機,當緩存池須要清除dealloc
時,會向這些 Autoreleased
對象作 release
釋放操做。bash
通常的說法是對象會在當前做用域大括號結束時釋放, 有這樣一個ARC
環境下簡單的例子🌰:首先建立一個ZHPerson
類:app
//// ZHPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ZHPerson : NSObject
+(instancetype)object;
@end
////ZHPerson.m
#import "ZHPerson.h"
@implementation ZHPerson
-(void)dealloc
{
NSLog(@"ZHPerson dealloc");
}
+(instancetype)object
{
return [[ZHPerson alloc] init];
}
@end
複製代碼
而後在ViewController.m
導入頭文件ZHPerson.h
,而後在寫一段這樣的代碼:框架
__weak id temp = nil;
{
ZHPerson *person = [[ZHPerson alloc] init];
temp = person;
}
NSLog(@"temp = %@",temp);
複製代碼
解釋一下這個代碼:先聲明瞭一個 __weak
變量temp
,由於 __weak
變量有一個特性就是它不會影響所指向對象的生命週期,而後讓變量temp
指向建立的person
對象,輸出以下: less
person
的做用域,它就被釋放了,看來是正常的。
把上面的建立對象的方法,變一變寫法:函數
__weak id temp = nil;
{
ZHPerson *person = [ZHPerson object];
temp = person;
}
NSLog(@"temp = %@",temp);
複製代碼
輸出以下: oop
這裏person
對象超出了其做用域仍是存在的,被延遲釋放了,也就是說其內部調用了
autorelease
方法。
小總結:學習
查詢得知:以 alloc
, copy
, ,mutableCopy
和new
這些方法會被默認標記爲 __attribute((ns_returns_retained))
,以這些方法建立的對象,編譯器在會在調用方法外圍要加上內存管理代碼retain/release
,因此其在做用域結束的時候就會釋放,而不以這些關鍵字開頭的方法,會被默認標記爲__attribute((ns_returns_not_retained))
,編譯器會在方法內部自動加上autorelease
方法,這時建立的對象就會被註冊到自動釋放池中,同時其釋放會延遲,等到自動釋放池銷燬的時候才釋放。測試
//1.生成一個NSAutoreleasePool對象
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//2.建立對象
id object = [[NSObject alloc] init];
//3.對象調用autorelease方法
[object autorelease];
//4.廢棄NSAutoreleasePool對象,會對釋放池中的object發送release消息
[pool drain];
複製代碼
@autoreleasepool {
//LLVM會在內部插入autorelease方法
id object = [[NSObject alloc] init];
}
複製代碼
AutoreleasePool
的做用前面有提到過,每當一個對象調用 autorelease
方法時,其實是將該對象放入當前 AutoreleasePool
中,當前AutoreleasePool
釋放時,會對添加進該 AutoreleasePool
中的對象逐一調用 release
方法。在ARC
環境下,並不須要特別的去關注Autoreleasepool
的使用,由於系統已經作了處理。ui
爲了看一下AutoreleasePool
到底作了什麼,先來建立一個main.m
文件(Xcode
-> File
-> New Project
-> macOS
-> Command Line Tool
-> main.m
);
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
複製代碼
而後,使用編譯器clang
編譯main.m
轉化成main.cpp
文件(在終端使用命令:clang -rewrite-objc main.m
),滑到main.cpp
文件的最後,有這樣一段代碼:
@autoreleasePool
轉換成一個
__AtAutoreleasePool
類型的局部私有變量
__AtAutoreleasePool __autoreleasepool
;
接着在 main.cpp
文件中查詢__AtAutoreleasePool
,來看一下它具體的實現:
__AtAutoreleasePool
是結構體類型,而且實現了兩個函數:構造函數
__AtAutoreleasePool()
和析構函數
~__AtAutoreleasePool()
。
也就是說在聲明 __autoreleasepool
變量時,構造函數 __AtAutoreleasePool()
被調用,即執行 atautoreleasepoolobj = objc_autoreleasePoolPush();
;當出了當前做用域時,析構函數 ~__AtAutoreleasePool()
被調用,即執行 objc_autoreleasePoolPop(atautoreleasepoolobj);
那麼上面的main.m
中的代碼能夠用這種形式代替:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
// @autoreleasepool
{
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// insert code here...
NSLog(@"Hello, World!");
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
複製代碼
接下來看一下析構函數和構造函數分別實現了什麼內容?這裏須要一份objc_runtime
的源碼(源碼地址),這裏使用的是objc4-756.2.tar.gz
:
AutoreleasePoolPage
的
push
方法和
pop
方法(這裏::是
C++
調用方法的形式,相似於點語法)。
AutoreleasePoolPage
是一個C++
實現的類,它的具體實現代碼是:
class AutoreleasePoolPage
{
# define POOL_BOUNDARY nil //哨兵對象(能夠看作是一個邊界)
static size_t const COUNT = SIZE / sizeof(id); // 對象數量
magic_t const magic; //用來校驗 `AutoreleasePoolPage`的結構是否完整;
id *next; //指向最新添加的 `autoreleased` 對象的下一個位置,初始化時指向 `begin()` ;
pthread_t const thread; //指向當前線程;
AutoreleasePoolPage * const parent; //指向父結點,第一個結點的 `parent` 值爲 `nil` ;
AutoreleasePoolPage *child; //指向子結點,最後一個結點的 `child` 值爲 `nil` ;
uint32_t const depth; //表明深度,從 `0` 開始,日後遞增 `1`;
uint32_t hiwat; //表明 `high water mark` ;
//剩下代碼省略......
}
複製代碼
經過源碼能夠知道這是一個典型的雙向列表結構,因此
AutoreleasePool
是由若干個
AutoreleasePoolPage
以雙向鏈表的形式組合而成。
AutoreleasePoolPage
每一個對象會開闢4096字節內存(虛擬內存一頁的大小),除了上面的實例變量所佔空間,剩下的空間所有用來儲存autorelease
對象的地址,AutoreleasepoolPage
經過壓棧的方式來存儲每一個autorelease
的對象(從低地址到高地址)。其中next
指針做爲遊標指向棧頂最新add
進來的autorelease
對象的下一個位置,當 next
指針指向begin
時,表示 AutoreleasePoolPage
爲空;當 next
指針指向end
時,表示 AutoreleasePoolPage
已滿,此時會新建一個AutoreleasePoolPage
對象,鏈接鏈表,後來的autorelease
對象在新的AutoreleasePoolPage
插入,一樣的新AutoreleasePoolPage
的next
指針被初始化在棧底(指向begin
的位置)。
既然已經知道了autorelease
的對象會經過壓棧的方式插入到AutoreleasePoolPage
當中,那麼顯然AutoreleasePoolPage
的push
方法就承包了AutoreleasePoolPage
的建立和插入。
接着看下push
方法的源碼:
static inline void *push()
{
id *dest;
//判斷是否已經初始化AutoreleasePoolPage
if (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;
}
複製代碼
這裏的POOL_BOUNDARY
能夠理解爲哨兵對象,或者理解爲一種邊界標識,並且這個POOL_BOUNDARY
值爲0,是個nil
。
接下來,先來看一下autoreleaseFast
這個方法,
static inline id *autoreleaseFast(id obj)
{
//獲取到當前page,這個hotPage是從當前線程的局部私有空間取出來的
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
複製代碼
咱們知道鏈表是有空間的,因此上面👆的源碼能夠理解爲:
(1). 當前page
存在且沒有滿時,直接將對象添加到當前page
中,即next
指向的位置;
(2). 當前page
存在而且已滿時,建立一個新的page
,並將對象添加到新建立的page
中,而後將這兩個鏈表節點進行連接。
(3). 當前page
不存在時,建立第一個page
,並將對象添加到新建立的page
中。
這裏重點看一下page->add(obj)
這個方法,
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
複製代碼
能夠看到這裏返回的ret
其實next
指針指向的地址,由上面的push
方法的源碼可知,這裏page->add(obj)
傳入的obj
其實就是POOL_BOUNDARY
,也就是說每一次調用push
方法,都會插入一個POOL_BOUNDARY
,因此objc_autoreleasePoolPush
的返回值就是這個哨兵對象的地址。
經過上面對構造函數objc_autoreleasePoolPush
的學習,已經知道objc_autoreleasePoolPush
返回的是哨兵對象的地址,那麼在調用析構函數objc_autoreleasePoolPop
的時候傳入的也就是這個哨兵對象的地址。隨着方法的一步步調用,緊接着來看下AutoreleasePoolPage
的pop
方法的實現:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
page = pageForPointer(token); //根據傳入的哨兵對象的地址,獲取到page中的哨兵對象以後的地址空間
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop); //對當前鏈表當中的對象進行release操做
if (DebugPoolAllocation && page->empty()) {
//釋放 `Autoreleased` 對象後,銷燬多餘的 page
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
page->kill();
setHotPage(nil);
}
else if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
複製代碼
這裏重點看一下page->releaseUntil(stop)
方法:
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);
}
複製代碼
這裏的stop
一樣是POOL_BOUNDARY
的地址,這裏分析一下這個方法:
(1). 外部循環挨個遍歷autoreleased
對象,直到遍歷到哨兵對象POOL_BOUNDARY
。
(2). 若是當前page
沒有 POOL_BOUNDARY
,而且爲空,則將hotPage
設置爲當前page
的父節點。
(3). 給當前autoreleased
對象發送release
消息。
(4). 最後再次配置hotPage
。
經過上面的分析已經知道了構造方法objc_autoreleasePoolPush
會建立AutoreleasePoolPage
,並插入哨兵對象POOL_BOUNDARY
,析構方法objc_autoreleasePoolPop
會對哨兵對象以後插入的對象發送release
消息,那麼在這兩個方法之間,對象經過調用autorelease
是怎麼插入到AutoreleasePoolPage
的呢?下面來看下autorelease
的源碼實現:
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;
}
複製代碼
這裏的重點仍是autoreleaseFast(obj)
;因爲這裏插入對象的方法和AutoreleasePoolPage
調用push
方法的實現是同樣的,只不過push
操做插入的是一個 POOL_BOUNDARY
,而autorelease
操做插入的是一個具體的autoreleased
對象,在此處就不作多餘分析。
經過上面👆的這些分析,已經大概知道AutoreleasePool
是怎樣的一個構造,以及內如是如何實現自動釋放的。
對於嵌套的AutoreleasePool
也是一樣的原理,在pop
的時候總會釋放對象到上次push
的位置爲止,也就是哨兵位置,多層的pool
就是插入多個哨兵對象而已,而後根據哨兵對象來進行釋放,就像剝洋蔥同樣一層一層的,互不影響。
那麼這裏有個疑問,若是在AutoreleasePool
多層嵌套中是同一個對象呢,那麼會怎麼釋放?下面經過一個小例子🌰來看一下:
@autoreleasepool {
ZHPerson *person = [ZHPerson object];
NSLog(@"current count %d",_objc_rootRetainCount(person));
@autoreleasepool {
ZHPerson *person1 = person;
NSLog(@"current count %d",_objc_rootRetainCount(person));
@autoreleasepool {
ZHPerson *person2 = person;
NSLog(@"current count %d",_objc_rootRetainCount(person));
}
}
}
複製代碼
打印結果以下:
這裏dealloc
方法只調用了一次,由上面的代碼可知:當前
person1
和
person2
是對
person
的引用,若是系統會爲每一次引用都自動插入一個
autorelease
,那麼對象在執行第一個
autorelease
的時候,會調用
objc_release(obj)
來釋放當前的對象,那麼當調用
rootRelease()
的時候就會報錯,由於當前對象已經被釋放了,那麼也就是說對於引用的對象只會被釋放一次。(同一個對象不可以反覆的
autorelease
)
先來看個簡單的例子:
在temp
的位置設置一個斷點,而後在控制檯輸入
watchpoint set variable temp
,
等到這個線程執行結束以後,來看一下左側邊欄的內容:
當執行到
NSLog(@"thread end");
這句代碼,表示線程執行結束,這裏,其實線程會先調用
[NSthread exit]
,而後執行
_pthread_tsd_cleanup
,清除當前線程的有關資源,而後調用
tls_dealloc
,也就是把當前線程關聯的
AutoReleasePool
釋放掉,最後調用
weak_clear_no_lock
清除指針。
那麼這一系列過程就說明了:在NSThread
退出了以後,與NSThread
對應的AutoReleasePool
也會被自動清空,因此當一個線程結束的時候,就會回收♻️AutoReleasePool
中自動釋放的對象。
結論:
每個線程都會維護本身的
AutoReleasePool
,而每個AutoReleasePool
都會對應惟一一個線程,可是線程能夠對應多個AutoReleasePool
。
對於NSThread
只是一個簡單的線程,若是把它換成一個常駐線程呢?
NSTimer
,並將其常駐。利用一樣的方式,
watchpoint set variable temp
,:
能夠看到這裏
NStimer
是被加入到子線程當中的,可是在子線程中,咱們並無去寫關於
AutoReleasePool
的內容,咱們只知道
test
作了
autorelease
操做。下面回到源碼中來看:
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;
}
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);
}
}
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
}
//這裏省略了部分代碼
複製代碼
因此從上面的源碼咱們能夠得出結論:子線程在使用autorelease
對象的時候,會懶加載出來一個AutoreleasePoolPage
,而後將對象插入進去。
那麼問題又來了,autorelease
對象在何時釋放的呢?也就說AutoreleasePoolPage
在何時調用了pop
方法?
其實在上面建立一個NSThread
的時候,在調用[NSthread exit]
的時候,會釋放當前資源,也就是把當前線程關聯的autoReleasePool
釋放掉,而在這裏當RunLoop
執行完成退出的時候,也會執行pop
方法,這就說明了爲何在子線程當中,咱們沒有顯示的調用pop
,它也能釋放當前AutoreleasePool
的資源的緣由。
那麼在主線程的RunLoop
到底何時把對象進行釋放回收的呢?
簡單粗暴點,直接在控制檯經過po [NSRunloop currentRunloop]
打印主線程的RunLoop
:
這裏,系統在主線程的RunLoop
裏註冊了兩個Observer
,回調都是_wrapRunLoopWithAutoreleasePoolHandler
,第一個Observer
的狀態是activities = 0x1
,第二個Observer
的狀態是activities = 0xa0
,這兩種狀態表明什麼意思呢?
先在這裏插入一點RunLoop
的內容(RunLoop
的狀態枚舉):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1
kCFRunLoopBeforeTimers = (1UL << 1), // 2
kCFRunLoopBeforeSources = (1UL << 2), // 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 32
kCFRunLoopAfterWaiting = (1UL << 6), // 64
kCFRunLoopExit = (1UL << 7), // 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
0x1
表明的是kCFRunLoopEntry
,也就是說第一個 Observer
監視的事件是Entry
(即將進入Loop
時),其回調內會調用_objc_autoreleasePoolPush()
建立一個自動釋放池。其order
優先級是-2147483647,優先級最高,保證建立自動釋放池發生在其餘全部回調以前。
0xa0
對應的是kCFRunLoopBeforeWaiting
和kCFRunLoopExit
,也就是說第二個Observer
監視了兩個事件:kCFRunLoopBeforeWaiting
準備進入休眠,kCFRunLoopExit
即將退出RunLoop
。在kCFRunLoopBeforeWaiting
事件時調用 _objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
釋放舊的自動釋放池並建立新的自動釋放池;在kCFRunLoopExit
事件時調用_objc_autoreleasePoolPop()
來釋放自動釋放池,同時這個Observer
的order
優先級是 2147483647,優先級最低,保證其釋放自動釋放池的操做發生在其餘全部回調以後。
因此在沒有手動增長AutoreleasePool
的狀況下,Autorelease
對象都是在當前的runloop
迭代結束時釋放的,而它可以釋放的緣由是系統在每一個runloop
迭代中都加入了自動釋放池push
和pop
操做。
總結:
對於不一樣線程,應當建立本身的
AutoReleasePool
。若是應用長期存在,應該按期drain
和建立新的AutoReleasePool
,AutoReleasePool
與RunLoop
與線程是一一對應的關係,AutoReleasePool
在RunLoop
在開始迭代時作push
操做,在RunLoop
休眠或者迭代結束時作pop
操做。
一般狀況下咱們是不須要手動建立AutoreleasePool
,可是也有一些特殊的:
編寫的程序不基於UI
框架,如命令行程序。
在循環中建立大量臨時對象時用以下降內存佔用峯值。
在主線程以外建立新的線程,在新線程開始執行處,建立本身的AutoreleasePool
,不然將致使內存泄漏。
下面就來簡單看下第二種狀況,直接來個for
循環:
for (int i = 0; i < 100000000; i ++) {
NSString * str = [NSString stringWithFormat:@"noAutoReleasePool"];
NSString *tempstr = str;
}
}
複製代碼
來看一下Memory
的使用狀況:
相反的,若是加上AutoreleasePool
,來看一下:
for (int i = 0; i < 100000000; i ++) {
@autoreleasepool {
NSString * str = [NSString stringWithFormat:@"AutoReleasePool"];
NSString *tempstr = str;
}
}
複製代碼
來看一下這種狀況下的Memory
的使用狀況:
這個作個備註:在主函數main.m
文件中的@autoreleasepool
,若是在這裏作個測試,使用for
循環建立大量的臨時對象,是否加上這個@autoreleasepool
對Memory
的使用狀況沒有特別大的影響。
寫到這裏,對於AutoReleasePool
學習內容就暫告一段了,正常狀況下,咱們不須要去關心AutoReleasePool
的建立和釋放,可是學習理解了AutoReleasePool
可以使咱們更加理解ARC
模式下系統是怎樣來管理內存的。
文中內容若有不當之處,還請指出,謝謝您!