@(iOS開發學習)[溫故而知新,掘金部落格]
[TOC]
1、AutoreleasePool分析整理
為了分析AutoreleasePool,下面分四種場景進行分析
Person類用於列印物件的釋放時機
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, strong) NSString* name;
@end
NS_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
@end
NS_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
@end
NS_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
@end
NS_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
@end
NS_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下檢視物件的引用計數
//先宣告私有的API
extern 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]);
列印出的日誌部分截圖如下。
_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倍。
面試會被問到的問題?
1、進入pool和出pool的時候,引用計數的變化? 2、為什麼採用雙向連結串列,而不是單向連結串列? 3、你說到了哨兵,在連結串列中怎麼使用哨兵可以簡化程式設計?
參考部落格
自動釋放池的前世今生 ---- 深入解析 autoreleasepool
iOS開發 自動釋放池(Autorelease Pool)和RunLoop
Objective-C Autorelease Pool 的實現原理
Objective-C高階程式設計(一) 自動引用計數,看我就夠了
二、Block學習整理
2.0、什麼是Block
Block
表面上是一個帶自動變數(區域性變數)的匿名函式。本質上是對閉包的物件實現,簡單來說Block
就是一個結構體物件。在ARC
下,大多數情況下Block
從棧上覆制到堆上的程式碼是由編譯器實現的(Blcok
作為返回值或者引數)
2.1、block編譯轉換結構
2.1.1、新建一個macOS專案,編寫一個最簡單的Block。
#import <Foundation/Foundation.h>
typedef void(^blockVoid)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
blockVoid block = ^() {
NSLog(@"block");
};
block();
}
return 0;
}
複製程式碼
2.1.2、使用clang命令處理成cpp檔案從而初步認識Block。
由於命令過長,我們起一個別名genCpp_mac。
alias genCpp_mac='clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'
2.1.3、在終端進入.m所在的目錄下面執行genCpp_mac main.m,當前目錄下會生成同名.cpp檔案
2.1.4、開啟.cpp檔案檢視與Block相關的程式碼
struct __block_impl {
void *isa; // 指向所屬類的指標,也就是block的型別(包含isa指標的皆為物件)
int Flags; // 標誌變數,在實現block的內部操作時會用到
int Reserved; // 保留變數
void *FuncPtr; // block執行時呼叫的函式指標
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 顯式的建構函式
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x0_1sgmhpyx6535p2pfkfsbfvww0000gn_T_main_94a22e_mi_0);
}
// 紀錄了block結構體大小等資訊
static struct __main_block_desc_0 {
size_t reserved; // 保留欄位
size_t Block_size; // block大小(sizeof(struct __main_block_impl_0))結構體大小需要儲存是因為,每個 block 因為會 capture 一些變數,這些變數會加到 __main_block_impl_0 這個結構體中,使其體積變大
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
/*
把多餘的轉換去掉,看起來就比較清楚了:
第一部分:block的初始化
引數一:__main_block_func_0(是block語法轉換的C語言函式指標)
引數二:__main_block_desc_0_DATA(作為靜態全域性變數初始化的 __main_block_desc_0 結構體例項指標)
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
第二部分:
block的執行: blk()
去掉轉化部分:
(*blk -> imp.FuncPtr)(blk);
這就是簡單地使用函式指標呼叫函式。由Block語法轉換的 __main_block_func_0 函式的指標被賦值成員變數FuncPtr中,另外 __main_block_func_0的函式的引數 __cself 指向Block的值,通過原始碼可以看出 Block 正式作為引數進行傳遞的。
*/
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
blockVoid block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼
2.2、block實際結構Block_private.h
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
struct Block_layout {
void *isa; // 所有物件都有該指標,用於實現物件相關的功能。
volatile int32_t flags; // contains ref count(block copy 的會對該變數操作)
int32_t reserved; // 保留變數
void (*invoke)(void *, ...); // 函式指標,指向具體的 block 實現的函式呼叫地址。
struct Block_descriptor_1 *descriptor; // 表示該 block 的附加描述資訊,主要是 size 大小,以及 copy 和 dispose 函式的指標。
// imported variables // capture 過來的變數,block 能夠訪問它外部的區域性變數,就是因為將這些變數(或變數的地址)複製到了結構體中。
};
複製程式碼
2.3、block的種類
Block種類 | 儲存區 | 拷貝效果 | 生命週期 |
---|---|---|---|
_NSConcreteStatckBlock | 棧 | 從棧拷貝到堆,往flags中併入BLOCK_NEEDS_FREE這個標誌表明block需要釋放,在release以及再次拷貝時會用到);如果有輔助拷貝函式,就呼叫,將捕獲的變數從棧拷貝到堆中 | 出了作用域 |
_NSConcreteMallocBlock | 堆 | 單純的引用計數加一 | 引用計數為0,runloop結束後釋放 |
_NSConcreteGlobalBlock | 全域性資料區 | 什麼都不做,直接返回Blcok | app整個生命週期 |
- 1、只要不訪問外部變數就是
__NSGlobalBlock__
型別的Block,不管是__strong
還是__weak
修飾(不加預設__strong
)。 - 2、如果訪問外部變數,被
__strong
修飾的是__NSMallocBlock__
型別,被__weak
修飾的是__NSStackBlock__
型別。
注意: 訪問外部變數的Block,在編譯完成後其實都是
__NSStackBlock__
型別的,只是在ARC中被__strong
修飾的會在執行時被自動拷貝一份,最終呼叫_Block_copy_internal
函式,將isa由_NSConcreteStatckBlock
指向_NSConcreteMallocBlock
。
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
...
aBlock = (struct Block_layout *)arg;
...
// Its a stack block. Make a copy.
if (!isGC) {
// 申請block的堆記憶體
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 拷貝棧中block到剛申請的堆記憶體中
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 改變isa指向_NSConcreteMallocBlock,即堆block型別
result->isa = _NSConcreteMallocBlock;
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
//printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock);
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
else {
...
}
}
複製程式碼
2.3.1、__NSStackBlock__與__NSMallocBlock__的區別:修飾型別是__strong
還是__weak
/**
__NSMallocBlock__:__strong修飾
__NSStackBlock__:__weak修飾
*/
-(void)blockStatckVsGloable {
NSInteger i = 10;
__strong void (^mallocBlock)(void) = ^{
NSLog(@"i = %ld", (long)i);
};
__weak void (^stackBlock)(void) = ^{
NSLog(@"self.number = %@", self.number);
};
}
複製程式碼
2.3.2、 __NSStackBlock__與__NSGlobalBlock__的區別:是否訪問外部變數
/**
__NSStackBlock__:訪問外部變數
__NSGlobalBlock__:沒有訪問外部變數
*/
-(void)blockStatckVsGloable {
NSInteger i = 10;
__weak void (^stackBlock)(void) = ^{
NSLog(@"i = %ld", (long)i);
};
__weak void (^globalBlock2)(void) = ^{
};
}
複製程式碼
2.4、訪問修改變數對block結構的影響
2.4.1、全域性變數
對於訪問和修改全域性變數,Block的結構不會發生變化。
2.4.2、全域性靜態變數
對於訪問和修改全域性靜態變數,同全域性變數,Block的結構不會發生變化。
2.4.3、區域性變數
ARC下NSConcreteStackBlock如果引入了區域性變數,會被NSConcreteMallocBlock 型別的 block 替代。
訪問區域性變數
修改區域性變數(區域性變數需要使用__Block修飾)
函式__main_block_copy_0
用於在將Block拷貝到堆中的時候,將包裝區域性變數(localVariable
)的物件(__Block_byref_localVariable_0
)從棧中拷貝到堆中,所以即使區域性變數在棧中被銷燬,Block依然能對堆中的區域性變數進行修改操作。結構體__Block_byref_localVariable_0
中的__forwarding
變數用來指向區域性變數在堆中的拷貝。目的是為了保證操作的值始終是堆中的拷貝,而不是棧中的值。
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;
...
// 堆中拷貝的forwarding指向它自己
copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
// 棧中的forwarding指向堆中的拷貝
src->forwarding = copy; // patch stack to point to heap copy
...
}
複製程式碼
2.4.4、區域性靜態變數(修改不需要__Block修飾)
對於訪問和修改區域性靜態變數,Block需要截獲靜態變數的指標,改變的時候直接通過指標改變值
2.4.5、self隱式迴圈引用
發生迴圈引用的時候,self強持有Block,從下面可以看出Block也是強持有self的。
2.6、block的輔助函式
詳情請參考Block技巧與底層解析
2.6、訪問or修改外部變數
參考部落格
三、Runloop學習整理(持續更新糾正)
[TOC]
@(iOS開發學習)[溫故而知新]
1、什麼是RunLoop
RunLoop
實際上就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面 Event Loop
的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 “接受訊息->等待->處理
” 的迴圈中,直到這個迴圈結束(比如傳入 quit
的訊息),函式返回。一般來講,一個執行緒一次只能執行一個任務,執行完成後執行緒就會退出。如果我們需要RunLoop
,讓執行緒能隨時處理事件但並不退出。RunLoop
核心是一個有條件的迴圈。
Runloop組成
RunLoop的結構需要涉及到以下4個概念:
Run Loop Mode
、Input Source
、Timer Source
和Run Loop Observer
。
1、一個 Runloop 包含若干個
mode
,每個mode
又包含若干個source
(InputSource
、TimerSource
、Observers
) 2、Runloop 啟動只能指定一個mode
,若要切換mode
只能重新啟動Runloop
指定另外一個mode
。這樣做的目的是為了處理優先順序不同的Source
、Timer
、Observer
。
Runloop的各種mode
NSUserDefaultRunloopMode
// 預設mode,通常主執行緒在這個mode下面執行
UITrackingRunloopMode
// 介面追蹤mode,用於ScrollView追蹤介面滑動,保證介面滑動時不受其他mode的影響
UIInitializationRunloopMode
// 剛啟動App時進入的第一個mode,啟動完成後就不在使用
GSEventReceiveRunloopMode
// 接受系統事件的內部mode,通常用不到。
NSRunloopCommonModes
// 並不是一種真正的mode,系統把NSUserDefaultRunloopMode和UITrackingRunloopMode共同標記為NSRunloopCommonModes
複製程式碼
Runloop的各種mode的作用
指定事件在執行迴圈中的優先順序。執行緒的執行需要不同的模式,去響應各種不同的事件,去處理不同情境模式。(比如可以優化tableview
的時候可以設定UITrackingRunLoopMode
下不進行一些操作,比如設定圖片等。)
1、InputSource:
官方文件分為三類,
基於埠的
、自定義的
、基於perform selector
。但是也可通過函式呼叫棧對Source分為兩類,source0和source1。source1是基於埠的,包含一個match_port和一個回撥(函式指標),能主動喚醒Runloop的執行緒;source0是基於非埠的,只包含一個回撥(函式指標),不能主動觸發事件。也就是使用者觸發事件,包括自定義source和perfom selector
注意: 按鈕點選事件從函式呼叫棧來看是Source0
事件。實際上是點選螢幕產生event
事件,傳遞給Source1
,然後Source1
派發給Source0
的。
2、TimerSource:
即
CFRunloopTimerRef
,也就是NSTimer
。NSTimer
建立時必須新增到Runloop
中,否則無法執行,在新增到Runloop
時,必須指定mode
,決定NSTimer
在哪個mode
下執行。
3、observer:
監聽
Runloop
的狀態
Runloop
的各種狀態
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 即將從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將推出Runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製程式碼
Runloop生命週期
生命週期 | 主執行緒 | 子執行緒 |
---|---|---|
建立 | 預設系統建立 | 蘋果不允許直接建立 RunLoop ,它只提供了兩個自動獲取的函式:CFRunLoopGetMain() 和 CFRunLoopGetCurrent() 。 在當前子執行緒呼叫[NSRunLoop currentRunLoop] ,如果有就獲取,沒有就建立 |
啟動 | 預設啟動 | 手動啟動 |
獲取 | [NSRunLoop mainRunLoop] 或者CFRunLoopGetMain() |
[NSRunLoop currentRunLoop] 或者CFRunLoopGetCurrent() |
銷燬 | app結束時 | 超時時間到了或者手動結束CFRunLoopStop() |
CFRunLoopStop()
方法只會結束當前的runMode:beforeDate:
呼叫,而不會結束後續的呼叫。
啟動注意:
- - 使用run啟動後,Runloop會一直執行處理輸入源的資料,在defaultMode模式下重複呼叫runMode:beforeDate:
- - 使用runUntilDate:啟動後與run啟動的區別是,在設定時間到後會停止Runloop
- - 使用runMode:beforeDate:啟動,Runloop只執行一次,設定時間到達或者第一個input source被處理,Runloop會停止
執行:
Runloop會一直迴圈檢測事件源CFRunloopSourceRef執行處理函式,首先會產生通知,CoreFundation向執行緒新增RunloopObserves來監聽事件,並控制Runloop裡面執行緒的執行和休眠,在有事情做得時候使NSRunloop控制的執行緒處理事情,空閒的時候讓NSRunloop控制的執行緒休眠。先處理TimerSource,再處理Source0,然後Source1。
停止:
- 1、執行緒結束
- 2、因為沒有任何事件源而退出( Runloop 只會檢查 Source 和 Timer ,沒有就關閉,不會檢查Observer )
- 3、手動結束 Runloop
Runloop啟動方式
啟動方式 | 呼叫次數 | 描述 |
---|---|---|
run | 迴圈呼叫 | 無條件進入是最簡單的做法,但也最不推薦。這會使執行緒進入死迴圈,從而不利於控制 runloop,結束 runloop 的唯一方式是 kill 它。它的本質就是無限呼叫 runMode:beforeDate: 方法。 |
runUntilDate | 迴圈呼叫 | 如果我們設定了超時時間,那麼 runloop 會在處理完事件或超時後結束,此時我們可以選擇重新開啟 runloop。也會重複呼叫 runMode:beforeDate:,區別在於它超時後就不會再呼叫。這種方式要優於前一種。 |
runMode:beforeDate: | 單次呼叫 | 這是相對來說最優秀的方式,相比於第二種啟動方式,我們可以指定 runloop 以哪種模式執行。 |
通過run開啟 runloop 會導致記憶體洩漏,也就是 thread 物件無法釋放。
Runloop的作用:
- 1、保持程式的持續執行(比如主執行迴圈)接收使用者的輸入
- 2、處理App中的各種事件(比如觸控事件、定時器事件、Selector事件)
- 3、任務排程(主調方產生很多事件,不用等到被調方執行完畢事件,採取執行其他操作)
- 4、節省CPU資源,提高程式效能:該做事時做事,該休息時休息
RunLoop處理邏輯
2、Runloop和執行緒的關係
Runloop
與執行緒是一一對應的,主執行緒的Runloop
預設已經建立好了,子執行緒的需要自己手動建立(主執行緒是一一對應的,子執行緒可以沒有,也可以最多有一個Runloop
)
3、Runloop與NSTimer的關係
- 1、Timer Source會重複在預設的時間點(建立定時器時指定的時間間隔)向Runloop傳送訊息,執行任務回撥函式。
- 2、主執行緒由於預設建立啟動了Runloop所以定時器可以正常執行,但是子執行緒要想定時器可以正常執行,需要手動啟動Runloop。
- 3、另外Timer新增到Runloop指定的預設mode是NSUserDefaultRunloopMode,當UIScrollView滾動的時候Runloop會自動切換到UITrackingRunloopMode,此時定時器是不能正常執行的,如果想正常執行,需要改變Timer新增到Runloop的mode為NSRunloopCommonMode
NSTimer
其實就是 CFRunLoopTimerRef
,他們之間是 toll-free bridged
的。一個 NSTimer
註冊到 RunLoop
後,RunLoop
會為其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop
為了節省資源,並不會在非常準確的時間點回撥這個 Timer
。Timer
有個屬性叫做 Tolerance
(寬容度),標示了當時間點到後,容許有多少最大誤差。
如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回撥也會跳過去,不會延後執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
建立定時器的時候,會以NSUerDefaultRunloopMode
的mode自動加入到當前執行緒中。因此下面兩種效果是等價的。
GCD定時器不受Runloop的mode的影響。GCD 本身與 RunLoop 是屬於平級的關係。 他們誰也不包含誰,但是他們之間存在著協作的關係。當呼叫 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主執行緒的 RunLoop 傳送訊息,RunLoop會被喚醒,並從訊息中取得這個 block,並在回撥 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 裡執行這個 block。但這個邏輯僅限於 dispatch 到主執行緒,dispatch 到其他執行緒仍然是由 libDispatch 處理的。
4、RunLoop應用
4.1、TableView中實現平滑滾動延遲載入圖片
利用CFRunLoopMode的特性,可以將圖片的載入放到NSDefaultRunLoopMode
的mode裡,這樣在滾動UITrackingRunLoopMode
這個mode時不會被載入而影響到。參考:RunLoopWorkDistribution
4.2、解決NSTime在ScrollView滾動時無效
如果利用scrollView型別的做自動廣告滾動條 需要把定時器加入當前runloop的模式NSRunLoopCommonModes
4.3、檢測UI卡頓
第一種方法通過子執行緒監測主執行緒的 runLoop,判斷兩個狀態區域之間的耗時是否達到一定閾值。
ANREye
就是在子執行緒設定flag 標記為YES, 然後在主執行緒中將flag設定為NO。利用子執行緒時闕值時長,判斷標誌位是否成功設定成NO。NSRunLoop呼叫方法主要就是在kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
之間,還有kCFRunLoopAfterWaiting
之後,也就是如果我們發現這兩個時間內耗時太長,那麼就可以判定出此時主執行緒卡頓
第二種方式就是FPS監控,App 重新整理率應該當努力保持在 60fps,通過
CADisplayLink
記錄兩次重新整理時間間隔,就可以計算出當前的 FPS。CADisplayLink
是一個和螢幕重新整理率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不一樣,其內部實際是操作了一個 Source)。如果在兩次螢幕重新整理之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成介面卡頓的感覺。在快速滑動TableView
時,即使一幀的卡頓也會讓使用者有所察覺。
4.4、利用空閒時間快取資料或者做其他的效能優化相關的任務
可以新增Observer監聽RunLoop的狀態。比如監聽點選事件的處理(在所有點選事件之前做一些事情)
4.5、子執行緒常駐
某些操作,需要重複開闢子執行緒,重複開闢記憶體過於消耗效能,可以設定子執行緒常駐
如果子執行緒的NSRunLoop沒有設定source or timer, 那麼子執行緒的NSRunLoop會立刻關閉
1、新增Source
// 無含義,設定子執行緒為常住執行緒,讓子執行緒不關閉
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
複製程式碼
2、新增Timer
NSTimer *timer = [NSTimer timerWithTimeInterval:5.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
// 如果不改變mode,下面這行程式碼去掉後效果一樣
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
複製程式碼
4.6、UI介面更新,監聽UICollectionView重新整理reloadData完畢的時機
當在操作 UI 時,比如改變了
Frame
、更新了UIView/CALayer
的層次時,或者手動呼叫了UIView/CALayer
的 >setNeedsLayout
/setNeedsDisplay
方法後,這個UIView/CALayer
就被標記為待處理,並被提交到一個全域性>的容器去。蘋果註冊了一個
Observer
監聽BeforeWaiting
(即將進入休眠) 和 Exit (即將退出Loop
) 事件,回撥去執行一個>很長的函式:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。這個函式裡會遍歷所有待處理的 >UIView/CAlayer
以執行實際的繪製和調整,並更新 UI 介面。如果主執行緒忙於大量的業務邏輯運算,此時去更新UI可能會卡頓。非同步繪製框架
ASDK(Texture)
就是為了解決這個問題誕生的,根本原理就是將UI排版和繪製運算儘可能的放到後臺,將UI最終的更新操作放到主執行緒中,同時提供了一套類似UIView
和CAlayer
的相關屬性,儘可能保證開發者的開發習慣。監聽主執行緒Runloop Observer
的即將進入休眠和退出兩種狀態,收到回撥是遍歷佇列中待處理的任務一一執行。
4.7、事件響應
蘋果註冊了一個 Source1
(基於 mach port
的) 用來接收系統事件,其回撥函式為 __IOHIDEventSystemClientQueueCallback()
。
當一個硬體事件(觸控/鎖屏/搖晃等)發生後,首先由 IOKit.framework
生成一個 IOHIDEvent
事件並由 SpringBoard
接收。 SpringBoard
只接收按鍵(鎖屏/靜音等),觸控,加速,接近感測器等幾種 Event
,隨後用 mach port
轉發給需要的 App 程式。隨後蘋果註冊的那個 Source1
就會觸發__IOHIDEventSystemClientQueueCallback()
回撥,並呼叫 _UIApplicationHandleEventQueue()
進行應用內部的分發。
_UIApplicationHandleEventQueue()
會把 IOHIDEvent
處理幷包裝成 UIEvent
進行處理或分發,其中包括識別 UIGesture
/處理螢幕旋轉
/傳送給 UIWindow
等。通常事件比如 UIButton
點選、touchesBegin
/Move
/End
/Cancel
事件都是在這個回撥中完成的。
SpringBoard
是什麼?
SpringBoard
其實是一個標準的應用程式,這個應用程式用來管理IOS的主螢幕,除此之外像啟動WindowSever(視窗伺服器)
,bootstrapping(引導應用程式)
,以及在啟動時候系統的一些初始化設定
都是由這個特定的應用程式負責的。它是我們IOS程式中,事件的第一個接受者
。它只能接受少數的事件比如:按鍵(鎖屏/靜音等),觸控,加速,接近感測器等幾種Event,隨後使用macport
轉發給需要的App程式。
4.8、手勢識別
當上面的 _UIApplicationHandleEventQueue()
識別了一個手勢時,其首先會呼叫 Cancel 將當前的 touchesBegin
/Move
/End
系列回撥打斷。隨後系統將對應的 UIGestureRecognizer
標記為待處理。
蘋果註冊了一個 Observer
監測 BeforeWaiting
(Loop 即將進入休眠) 事件,這個 Observer 的回撥函式是 _UIGestureRecognizerUpdateObserver()
,其內部會獲取所有剛被標記為待處理的 GestureRecognizer
,並執行 GestureRecognizer
的回撥。
當有 UIGestureRecognizer
的變化(建立/銷燬/狀態改變)時,這個回撥都會進行相應處理。
5、Runloop與PerformSelecter
performSelecter:after
函式依賴Timer Source
和Runloop
的啟動;Runloop
依賴Source
(不限於Timer Source
),沒有Sources
就會退出
import UIKit
class RunloopVc: UIViewController {
@objc func performSeletor() {
debugPrint("performSeletor \(Thread.current)")
}
func case0() {
// 結果:1 → 2 → 1.1 → performSeletor → 1.2
debugPrint("1")
DispatchQueue.global().async {
debugPrint("1.1 \(Thread.current)")
// 當前子執行緒 非同步執行,1.1 → 1.2 → performSeletor
//self.performSelector(inBackground: #selector(self.performSeletor), with: nil)
// 當前子執行緒 同步執行,1.1 → performSeletor → 1.2
self.perform(#selector(self.performSeletor))
debugPrint("1.2 \(Thread.current)")
}
debugPrint("2")
}
func case1() {
// 結果:1 → 2 → performSeletor(子執行緒非同步執行)
debugPrint("1")
self.performSelector(inBackground: #selector(performSeletor), with: nil)
debugPrint("2")
}
func case2() {
// 結果:1 → 2 → performSeletor(主執行緒非同步執行)
debugPrint("1")
self.perform(#selector(performSeletor), afterDelay: 1)
debugPrint("2")
}
func case3() {
// 結果:1 → 2 → performSeletor(主執行緒非同步執行)
debugPrint("1")
self.perform(#selector(performSeletor), with: nil, afterDelay: 1, inModes: [.default])
debugPrint("2")
}
func case4() {
// 結果:1 → 2 → performSeletor不會執行
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor), afterDelay: 1)
debugPrint("1.2 \(Thread.current)")
}
debugPrint("2")
}
func case5() {
// 結果:1 → 2 → 1.1 → 1.2 → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
// Runloop中沒有source是會自動退出
RunLoop.current.run()
debugPrint("1.2 \(Thread.current)")
self.perform(#selector(self.performSeletor), afterDelay: 10)
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case6() {
// 結果:1 → 2 → 1.1 → 1.2 → performSeletor → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor), afterDelay: 1)
debugPrint("1.2 \(Thread.current)")
// perform後runloop喚醒
RunLoop.current.run()
// 1.3 能夠執行,是因為定時器執行完畢後已經無效,導致Runloop中沒有source,所以執行緒執行完畢後退出
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case7() {
// 結果:1 → 2 → 1.1 → performSeletor → 1.2 → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor),
on: Thread.current,
with: nil,
waitUntilDone: true)
debugPrint("1.2 \(Thread.current)")
RunLoop.current.run()
// 1.3 不執行,是因為定時器source無效,Runloop結束了
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case8() {
// 結果:1 → 2 → 1.1 → 1.2 → performSeletor → 1.3不執行
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor),
on: Thread.current,
with: nil,
waitUntilDone: false)
debugPrint("1.2 \(Thread.current)")
RunLoop.current.run()
// 1.3 不執行,是因為定時器source還存在,Runloop沒有結束
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case9() {
// 結果:1 → 2 → 1.1 → 1.2 → performSeletor → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor),
on: Thread.current,
with: nil,
waitUntilDone: false)
debugPrint("1.2 \(Thread.current)")
// 演示 1s 後結束runloop
RunLoop.current.run(until: NSDate.init(timeIntervalSince1970: NSDate.init().timeIntervalSince1970 + 1) as Date)
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
override func viewDidLoad() {
super.viewDidLoad()
case5()
}
}
複製程式碼
- - 當呼叫
NSObject
的performSelecter:afterDelay:
後,實際上其內部會建立一個Timer Source
並新增到當前執行緒的RunLoop
中。所以如果當前執行緒沒有RunLoop
,則這個方法會失效。 - - 當呼叫
performSelector:onThread:
時,實際上其會建立一個Timer Source
加到對應的執行緒去,同樣的,如果對應執行緒沒有RunLoop
該方法也會失效。
是事件源的performSelector方法有:
// 主執行緒
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
/// 指定執行緒
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
/// 針對當前執行緒
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
/// 取消,在當前執行緒,和上面兩個方法對應
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
複製程式碼
不是事件源的performSelector方法有:
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製程式碼
6、Runloop與AutoreleasePool
一個執行緒可以包含多個AutoReleasePool,但是一個AutoReleasePool只能對應一個唯一的執行緒
新增列印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 了。
疑問:子執行緒預設不會開啟 Runloop,那出現 Autorelease 物件如何處理?不手動處理會記憶體洩漏嗎?
解釋:
在子執行緒你建立了 Pool 的話,產生的
Autorelease 物件
就會交給 pool 去管理。 如果你沒有建立 Pool ,但是產生了Autorelease 物件
,就會呼叫autoreleaseNoPage
方法。在這個方法中,會自動幫你建立一個 hotpage(hotPage 可以理解為當前正在使用的AutoreleasePoolPage
,如果你還是不理解,可以先看看 Autoreleasepool 的原始碼,再來看這個問題 ),並呼叫page->add(obj)
將物件新增到AutoreleasePoolPage
的棧中,也就是說你不進行手動的記憶體管理,也不會記憶體洩漏啦!StackOverFlow 的作者也說道,這個是 OS X 10.9+和 iOS 7+ 才加入的特性。並且蘋果沒有對應的官方文件闡述此事,但是你可以通過原始碼瞭解。這裡張貼部分原始碼:
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// No pool in place.
// hotPage 可以理解為當前正在使用的 AutoreleasePoolPage。
assert(!hotPage());
// POOL_SENTINEL 只是 nil 的別名
if (obj != POOL_SENTINEL && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
(void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
// Install the first page.
// 幫你建立一個 hotpage(hotPage 可以理解為當前正在使用的 AutoreleasePoolPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push an autorelease pool boundary if it wasn't already requested.
// POOL_SENTINEL 只是 nil 的別名,哨兵物件
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
// Push the requested object.
// 把物件新增到 自動釋放池 進行管理
return page->add(obj);
}
複製程式碼
7、Runloop與GCD
Runloop
和GCD
並沒有直接的關係,當呼叫了DispatchQueue.main.async
從子執行緒到主執行緒進行通訊重新整理UI的時候,libDispatch
會向主執行緒Runloop
傳送訊息喚醒Runloop
,Runloop
從訊息中獲取Block
,並且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
回撥裡執行block
操作。dispatch
到非主執行緒的操作全部是由libDispatch
驅動的。