AutoreleasePool分析整理

花丶滿樓發表於2018-12-13

@(iOS開發學習)

[TOC]

為了分析AutoreleasePool,下面分四種場景進行分析

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分析整理

場景二:物件被加入到手動建立的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分析整理

場景三:物件被加入到系統的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迭代中都加入了AutoreleasePoolRunloop開始後建立AutoreleasePool並Autorelease物件加入到pool中,Runloop結束後或者休眠的時候Autorelease物件被釋放掉。

AutoreleasePool分析整理

場景四:(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分析整理

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物件的地址

AutoreleasePool分析整理
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物件、哨兵物件)的空閒位置。

AutoreleasePool分析整理

objc_autoreleasePoolPush 執行過程

當呼叫AutoreleasePoolPage::push()方法時,首先向當前的page(hotPage)結點next指標指向的位置新增一個哨兵物件POOL_SENTINEL,值為nil)。如果後面巢狀著AutoreleasePool則繼續新增哨兵物件,否則將Autorelease物件壓入哨兵物件的上面。向高地址移動next指標,直到 next == end()時,表示當前page已滿。當 next == begin() 時,表示 AutoreleasePoolPage 為空;當 next == end() 時,表示 AutoreleasePoolPage 已滿。

AutoreleasePool分析整理
  • 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物件什麼時候釋放?

沒有手動加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]);
列印出的日誌部分截圖如下。

AutoreleasePool分析整理

可以發現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倍。

參考部落格

深入理解RunLoop

深入理解AutoreleasePool

黑幕背後的Autorelease

自動釋放池的前世今生 —- 深入解析 autoreleasepool

iOS開發 自動釋放池(Autorelease Pool)和RunLoop

Objective-C Autorelease Pool 的實現原理

深入理解Tagged Pointer

【譯】採用Tagged Pointer的字串

來源:https://juejin.im/post/5c123b9cf265da614c4cabac

相關文章