@(iOS開發學習)
[TOC]
為了分析AutoreleasePool,下面分四種場景進行分析
Person類用於列印物件的釋放時機
#import <
Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject@property (nonatomic, strong) NSString* name;
@endNS_ASSUME_NONNULL_END@implementation Person- (void)dealloc {
NSLog(@"func = %s, name = %@", __func__, self.name);
}@end複製程式碼
場景一:物件沒有被加入到AutoreleasePool中
#import <
UIKit/UIKit.h>
#import "Person.h"NS_ASSUME_NONNULL_BEGIN@interface AutoreleasePoolWithOutVC : UIViewController@endNS_ASSUME_NONNULL_END@interface AutoreleasePoolWithOutVC ()@property (nonatomic, strong) Person* zhangSanStrong;
@property (nonatomic, weak) Person* zhangSanWeak;
@end@implementation AutoreleasePoolWithOutVC- (void)viewDidLoad {
[super viewDidLoad];
Person *xiaoMing = [[Person alloc] init];
xiaoMing.name = @"xiaoMing";
_zhangSanStrong = [[Person alloc] init];
_zhangSanStrong.name = @"zhangSanStrong";
Person *zhangSanWeak = [[Person alloc] init];
zhangSanWeak.name = @"zhangSanWeak";
_zhangSanWeak = zhangSanWeak;
NSLog(@"func = %s, xiaoMing = %@", __func__, xiaoMing);
}- (void)viewWillAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}- (void)viewDidAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}@end複製程式碼
執行結果:棧中建立的臨時物件xiaoMing和weak屬性修飾的物件**_zhangSanWeak**,在viewDidLoad
結束後就被釋放了。
場景二:物件被加入到手動建立的AutoreleasePool中
#import <
UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN@interface AutoreleasePoolManualWithVC : UIViewController@endNS_ASSUME_NONNULL_END#import "AutoreleasePoolManualWithVC.h"#import "Person.h"@interface AutoreleasePoolManualWithVC ()@property (nonatomic, strong) Person* zhangSanStrong;
@property (nonatomic, weak) Person* zhangSanWeak;
@end@implementation AutoreleasePoolManualWithVC- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
Person *xiaoMing = [[Person alloc] init];
xiaoMing.name = @"xiaoMing";
_zhangSanStrong = [[Person alloc] init];
_zhangSanStrong.name = @"zhangSanStrong";
Person *zhangSanWeak = [[Person alloc] init];
zhangSanWeak.name = @"zhangSanWeak";
_zhangSanWeak = zhangSanWeak;
} NSLog(@"func = %s", __func__);
}- (void)viewWillAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}- (void)viewDidAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}@end複製程式碼
執行結果:棧中建立的臨時物件xiaoMing和weak屬性修飾的物件**_zhangSanWeak**,在viewDidLoad
結束之前就被釋放了。
場景三:物件被加入到系統的AutoreleasePool中
#import <
UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN@interface AutoreleasePoolSystermWithVC : UIViewController@endNS_ASSUME_NONNULL_END#import "AutoreleasePoolSystermWithVC.h"@interface AutoreleasePoolSystermWithVC ()@property (nonatomic, strong) NSString* zhangSanStrong;
@property (nonatomic, weak) NSString* zhangSanWeak;
@end@implementation AutoreleasePoolSystermWithVC- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"func = %s start", __func__);
_zhangSanStrong = [NSString stringWithFormat:@"zhangSanStrong"];
NSString* zhangSanWeak = [NSString stringWithFormat:@"zhangSanStrong"];
_zhangSanWeak = zhangSanWeak;
[self printInfo];
NSLog(@"func = %s end", __func__);
}- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}- (void)printInfo {
NSLog(@"self.zhangSanStrong = %@", _zhangSanStrong);
NSLog(@"self.zhangSanWeak = %@", _zhangSanWeak);
}@end複製程式碼
執行結果:系統在每個Runloop
迭代中都加入了AutoreleasePool
,Runloop
開始後建立AutoreleasePool並Autorelease物件
加入到pool中,Runloop
結束後或者休眠的時候Autorelease物件
被釋放掉。
場景四:(Tagged Pointer
)物件被加入到系統的AutoreleasePool中
看了別人的部落格後,決定手動驗證一下,又不想完全copy別人的程式碼,自己仿寫初始化的時候又懶得寫太多內容,索性寫了@“1”,所以導致結果與部落格不一致,因此更加懷疑人生。我想不止我一個要這種情況?。
#import <
UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN@interface AutoreleasePoolSystermWithTaggedPointerVC : UIViewController@endNS_ASSUME_NONNULL_END#import "AutoreleasePoolSystermWithTaggedPointerVC.h"@interface AutoreleasePoolSystermWithTaggedPointerVC ()@property (nonatomic, weak) NSString* tagged_yes_1;
// 是Tagged Pointer@property (nonatomic, weak) NSString* tagged_yes_2;
// 是Tagged Pointer@property (nonatomic, weak) NSString* tagged_yes_3;
// 是Tagged Pointer@property (nonatomic, weak) NSString* tagged_no_1;
// 非Tagged Pointer@property (nonatomic, weak) NSString* tagged_no_2;
// 非Tagged Pointer@property (nonatomic, weak) NSString* tagged_no_3;
// 非Tagged Pointer@end@implementation AutoreleasePoolSystermWithTaggedPointerVC- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"func = %s start", __func__);
NSString* tagged_yes_1_str = [NSString stringWithFormat:@"1"];
_tagged_yes_1 = tagged_yes_1_str;
NSString* tagged_yes_2_str = [NSString stringWithFormat:@"123456789"];
_tagged_yes_2 = tagged_yes_2_str;
NSString* tagged_yes_3_str = [NSString stringWithFormat:@"abcdefghi"];
_tagged_yes_3 = tagged_yes_3_str;
NSString* tagged_no_1_str = [NSString stringWithFormat:@"0123456789"];
_tagged_no_1 = tagged_no_1_str;
NSString* tagged_no_2_str = [NSString stringWithFormat:@"abcdefghij"];
_tagged_no_2 = tagged_no_2_str;
NSString* tagged_no_3_str = [NSString stringWithFormat:@"漢字"];
_tagged_no_3 = tagged_no_3_str;
[self printInfo];
NSLog(@"func = %s end", __func__);
}- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}- (void)printInfo {
NSLog(@"self.tagged_yes_1 = %@", _tagged_yes_1);
NSLog(@"self.tagged_yes_2 = %@", _tagged_yes_2);
NSLog(@"self.tagged_yes_3 = %@", _tagged_yes_3);
NSLog(@"self.tagged_no_1 = %@", _tagged_no_1);
NSLog(@"self.tagged_no_2 = %@", _tagged_no_2);
NSLog(@"self.tagged_no_3 = %@", _tagged_no_3);
}@end複製程式碼
執行結果:
- Tagged Pointer型別的Autorelease物件,系統不會釋放
- 非Tagged Pointer型別的Autorelease物件,系統會在當前Runloop結束後釋放
AutoreleasePool定義
- 自動釋放池是由 AutoreleasePoolPage 以雙向連結串列的方式實現的
- 當物件呼叫 Autorelease 方法時,會將物件加入 AutoreleasePoolPage 的棧中
- 呼叫 AutoreleasePoolPage::pop 方法會向棧中的物件傳送 release 訊息
在ARC環境下,以alloc
/new
/copy
/mutableCopy
開頭的方法返回值取得的物件是自己生成並且持有的,其他情況是非自己持有的物件,此時物件的持有者就是AutoreleasePool
。
當我們使用@autoreleasepool{
時,編譯器會將其轉換成以下形式
}
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
}複製程式碼
__AtAutoreleasePool
定義如下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();
} . . // 將中間物件壓入棧中(atautoreleasepoolobj也是一個物件,相當於哨兵,代表一個 autoreleasepool 的邊界, // 與當前的AutoreleasePool對應,pop的時候用來標記終點位置,被當前的AutoreleasePool第一個壓入棧中, // 出棧的時候最後一個被彈出) . . ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);
} void * atautoreleasepoolobj;
};
複製程式碼
建立時呼叫了objc_autoreleasePoolPush()
方法,而釋放時呼叫objc_autoreleasePoolPop()
方法,只是一層簡單的封裝。
AutoreleasePool
並沒有單獨的結構,本質上是一個雙向連結串列,結點是AutoreleasePoolPage
物件。每個結點的大小是4KB
(4*1024=4096位元組),除去例項變數的大小,剩餘的空間用來儲存Autorelease物件的地址
。
class AutoreleasePoolPage {
magic_t const magic;
// 用於校驗AutoreleasePage的完整性 id *next;
// 指向棧頂最後push進來的Autorelease物件的下一個位置 pthread_t const thread;
// 儲存了當前頁所在的執行緒,每一個 autoreleasepool 只對應一個執行緒 AutoreleasePoolPage * const parent;
// 雙向連結串列中指向上一個節點,第一個結點的 parent 值為 nil AutoreleasePoolPage *child;
// 雙向連結串列中指向下一個節點,最後一個結點的 child 值為 nil uint32_t const depth;
// 深度,從0開始,往後遞增1 uint32_t hiwat;
// high water mark
};
複製程式碼
next指標指向將要新增新物件(Autorelease物件、哨兵物件)的空閒位置。
objc_autoreleasePoolPush 執行過程
當呼叫AutoreleasePoolPage::push()
方法時,首先向當前的page(hotPage)結點next指標指向的位置新增一個哨兵物件(POOL_SENTINEL
,值為nil)。如果後面巢狀著AutoreleasePool
則繼續新增哨兵物件,否則將Autorelease物件壓入哨兵物件的上面。向高地址移動next指標,直到 next == end()
時,表示當前page已滿。當 next == begin()
時,表示 AutoreleasePoolPage 為空;當 next == end()
時,表示 AutoreleasePoolPage 已滿。
- 1、有 hotPage 並且當前 page 不滿。呼叫 page->
add(obj) 方法將物件新增至 AutoreleasePoolPage 的棧中 - 2、有 hotPage 並且當前 page 已滿。呼叫 autoreleaseFullPage 初始化一個新的頁,呼叫 page->
add(obj) 方法將物件新增至 AutoreleasePoolPage 的棧中 - 3、無 hotPage。呼叫 autoreleaseNoPage 建立一個 hotPage,呼叫 page->
add(obj) 方法將物件新增至 AutoreleasePoolPage 的棧中。
objc_autoreleasePoolPop 執行過程
當呼叫AutoreleasePoolPage::pop()
的方法時,pop 函式的入參就是 push 函式的返回值,也就是 POOL_SENTINEL 的記憶體地址,即 pool token 。當執行 pop 操作時,根據傳入的哨兵物件地址找到哨兵物件所處的page,將晚於(上面的)哨兵物件壓入的Autorelease物件進行release。即記憶體地址在 pool token 之後的所有 Autoreleased 物件都會被 release 。直到 pool token 所在 page 的 next 指向 pool token 為止。並向回移動next指標到正確位置。
AutoreleasePool物件什麼時候釋放?
沒有手動加AutoreleasePool
的情況下,Autorelease物件
是在當前的Runloop
迭代結束的時候釋放的。手動新增的Autorelease物件
也是自動計數的,當引用計數為0的時候,被釋放掉。
實際驗證之前,首先了解幾個私有API,檢視自動釋放池的狀態,在ARC下檢視物件的引用計數
//先宣告私有的APIextern void _objc_autoreleasePoolPrint(void);
extern uintptr_t _objc_rootRetainCount(id obj);
_objc_autoreleasePoolPrint();
//呼叫 列印自動釋放池裡的物件_objc_rootRetainCount(obj);
//呼叫 檢視物件的引用計數複製程式碼
NSThread、NSRunLoop 和 NSAutoreleasePool
根據蘋果官方文件中對 NSRunLoop 的描述,我們可以知道每一個執行緒,包括主執行緒,都會擁有一個專屬的 NSRunLoop 物件,並且會在有需要的時候自動建立。同樣的,根據蘋果官方文件中對 NSAutoreleasePool 的描述,我們可知,在主執行緒的 NSRunLoop 物件(在系統級別的其他執行緒中應該也是如此,比如通過 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 獲取到的執行緒)的每個 event loop 開始前,系統會自動建立一個 autoreleasepool ,並在 event loop 結束時 drain 。
新增列印Runloop的程式碼:NSLog(@"[NSRunLoop currentRunLoop] = %@", [NSRunLoop currentRunLoop]);
列印出的日誌部分截圖如下。
可以發現App啟動後,蘋果在主執行緒 RunLoop 裡註冊了兩個 Observer,其回撥callout都是 _wrapRunLoopWithAutoreleasePoolHandler()
。
第一個 Observer 監視的事件是 Entry
(即將進入Loop),其回撥內會呼叫 _objc_autoreleasePoolPush()
建立自動釋放池。其 order 是-2147483647,優先順序最高,保證建立釋放池發生在其他所有回撥之前。
第二個 Observer 監視了兩個事件: BeforeWaiting
(準備進入休眠) 時呼叫_objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
釋放舊的池並建立新池;Exit
(即將退出Loop) 時呼叫 _objc_autoreleasePoolPop()
來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先順序最低,保證其釋放池子發生在其他所有回撥之後。
在主執行緒執行的程式碼,通常是寫在諸如事件回撥、Timer回撥內的。這些回撥會被 RunLoop 建立好的 AutoreleasePool 環繞著,所以不會出現記憶體洩漏,開發者也不必顯示建立 Pool 了。
什麼時候用@autoreleasepool
根據 Apple的文件 ,使用場景如下:
- 寫基於命令列的的程式時,就是沒有UI框架,如AppKit等Cocoa框架時。
- 寫迴圈,迴圈裡面包含了大量臨時建立的物件。(本文的例子)
- 建立了新的執行緒。(非Cocoa程式建立執行緒時才需要)
- 長時間在後臺執行的任務。
Tagged Pointer
Tagged Pointer是一個能夠提升效能、節省記憶體的有趣的技術。在OS X 10.10中,NSString就採用了這項技術,現在讓我們來看看該技術的實現過程。
- 1、Tagged Pointer專門用來儲存小的物件,例如NSNumber、NSDate、NSString
- 2、Tagged Pointer指標的值不再是地址了,而是真正的值。不再是一個物件,記憶體中的位置不在堆中,不需要malloc和free。避免在程式碼中直接訪問物件的isa變數,而使用方法isKindOfClass和objc_getClass。
- 3、在記憶體讀取上有著3倍的效率,建立時比以前快了106倍。
參考部落格
自動釋放池的前世今生 —- 深入解析 autoreleasepool
iOS開發 自動釋放池(Autorelease Pool)和RunLoop