@(iOS開發學習)[溫故而知新,掘金博客]html
[TOC]ios
Person類用於打印對象的釋放時機git
#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
複製代碼
#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
結束後就被釋放了。 github
#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
結束以前就被釋放了。 面試
#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對象
被釋放掉。 objective-c
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
複製代碼
運行結果:bootstrap
Tagged Pointer
類型的Autorelease對象
,系統不會釋放Tagged Pointer
類型的Autorelease對象
,系統會在當前Runloop
結束後釋放
在ARC環境下,以alloc
/new
/copy
/mutableCopy
開頭的方法返回值取得的對象是本身生成而且持有的,其餘狀況是非本身持有的對象,此時對象的持有者就是AutoreleasePool
。swift
當咱們使用@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對象、哨兵對象)的空閒位置。
當調用AutoreleasePoolPage::push()
方法時,首先向當前的page(hotPage)結點next指針指向的位置添加一個哨兵對象(POOL_SENTINEL
,值爲nil)。若是後面嵌套着AutoreleasePool
則繼續添加哨兵對象,不然將Autorelease對象壓入哨兵對象的上面。向高地址移動next指針,直到 next == end()
時,表示當前page已滿。當 next == begin()
時,表示 AutoreleasePoolPage 爲空;當 next == end()
時,表示 AutoreleasePoolPage 已滿。
hotPage
而且當前 page
不滿。調用 page->add(obj)
方法將對象添加至 AutoreleasePoolPage
的棧中hotPage
而且當前 page
已滿。調用 autoreleaseFullPage
初始化一個新的頁,調用 page->add(obj)
方法將對象添加至 AutoreleasePoolPage
的棧中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
的狀況下,Autorelease對象
是在當前的Runloop
迭代結束的時候釋放的。手動添加的Autorelease對象
也是自動計數的,當引用計數爲0的時候,被釋放掉。
實際驗證以前,首先了解幾個私有API,查看自動釋放池的狀態,在ARC下查看對象的引用計數
//先聲明私有的API
extern void _objc_autoreleasePoolPrint(void);
extern uintptr_t _objc_rootRetainCount(id obj);
_objc_autoreleasePoolPrint(); //調用 打印自動釋放池裏的對象
_objc_rootRetainCount(obj); //調用 查看對象的引用計數
複製代碼
根據蘋果官方文檔中對 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 了。
根據 Apple的文檔 ,使用場景以下:
Tagged Pointer是一個可以提高性能、節省內存的有趣的技術。在OS X 10.10中,NSString就採用了這項技術,如今讓咱們來看看該技術的實現過程。
一、進入pool和出pool的時候,引用計數的變化? 二、爲何採用雙向鏈表,而不是單向鏈表? 三、你說到了哨兵,在鏈表中怎麼使用哨兵能夠簡化編程?
自動釋放池的前世此生 ---- 深刻解析 autoreleasepool
iOS開發 自動釋放池(Autorelease Pool)和RunLoop
Objective-C Autorelease Pool 的實現原理
Objective-C高級編程(一) 自動引用計數,看我就夠了
Block
表面上是一個帶自動變量(局部變量)的匿名函數。本質上是對閉包的對象實現,簡單來講Block
就是一個結構體對象。在ARC
下,大多數狀況下Block
從棧上覆制到堆上的代碼是由編譯器實現的(Blcok
做爲返回值或者參數)
#import <Foundation/Foundation.h>
typedef void(^blockVoid)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
blockVoid block = ^() {
NSLog(@"block");
};
block();
}
return 0;
}
複製代碼
因爲命令過長,咱們起一個別名genCpp_mac。 alias genCpp_mac='clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'
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 };
複製代碼
#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 可以訪問它外部的局部變量,就是由於將這些變量(或變量的地址)複製到告終構體中。
};
複製代碼
Block種類 | 存儲區 | 拷貝效果 | 生命週期 |
---|---|---|---|
_NSConcreteStatckBlock | 棧 | 從棧拷貝到堆,往flags中併入BLOCK_NEEDS_FREE這個標誌代表block須要釋放,在release以及再次拷貝時會用到);若是有輔助拷貝函數,就調用,將捕獲的變量從棧拷貝到堆中 | 出了做用域 |
_NSConcreteMallocBlock | 堆 | 單純的引用計數加一 | 引用計數爲0,runloop結束後釋放 |
_NSConcreteGlobalBlock | 全局數據區 | 什麼都不作,直接返回Blcok | app整個生命週期 |
__NSGlobalBlock__
類型的Block,無論是__strong
仍是__weak
修飾(不加默認__strong
)。__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 {
...
}
}
複製代碼
__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);
};
}
複製代碼
/**
__NSStackBlock__:訪問外部變量
__NSGlobalBlock__:沒有訪問外部變量
*/
-(void)blockStatckVsGloable {
NSInteger i = 10;
__weak void (^stackBlock)(void) = ^{
NSLog(@"i = %ld", (long)i);
};
__weak void (^globalBlock2)(void) = ^{
};
}
複製代碼
對於訪問和修改全局變量,Block的結構不會發生變化。
對於訪問和修改全局靜態變量,同全局變量,Block的結構不會發生變化。
ARC下NSConcreteStackBlock若是引入了局部變量,會被NSConcreteMallocBlock 類型的 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
...
}
複製代碼
對於訪問和修改局部靜態變量,Block須要截獲靜態變量的指針,改變的時候直接經過指針改變值
發生循環引用的時候,self強持有Block,從下面能夠看出Block也是強持有self的。
詳情請參考Block技巧與底層解析
[TOC]
@(iOS開發學習)[溫故而知新]
RunLoop
實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop
的邏輯。線程執行了這個函數後,就會一直處於這個函數內部 「接受消息->等待->處理
」 的循環中,直到這個循環結束(好比傳入 quit
的消息),函數返回。通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要RunLoop
,讓線程能隨時處理事件但並不退出。RunLoop
核心是一個有條件的循環。
RunLoop的結構須要涉及到如下4個概念:
Run Loop Mode
、Input Source
、Timer Source
和Run Loop Observer
。
一、一個 Runloop 包含若干個
mode
,每一個mode
又包含若干個source
(InputSource
、TimerSource
、Observers
) 二、Runloop 啓動只能指定一個mode
,若要切換mode
只能從新啓動Runloop
指定另一個mode
。這樣作的目的是爲了處理優先級不一樣的Source
、Timer
、Observer
。
NSUserDefaultRunloopMode
// 默認mode,一般主線程在這個mode下面運行
UITrackingRunloopMode
// 界面追蹤mode,用於ScrollView追蹤界面滑動,保證界面滑動時不受其餘mode的影響
UIInitializationRunloopMode
// 剛啓動App時進入的第一個mode,啓動完成後就不在使用
GSEventReceiveRunloopMode
// 接受系統事件的內部mode,一般用不到。
NSRunloopCommonModes
// 並非一種真正的mode,系統把NSUserDefaultRunloopMode和UITrackingRunloopMode共同標記爲NSRunloopCommonModes
複製代碼
指定事件在運行循環中的優先級。線程的運行須要不一樣的模式,去響應各類不一樣的事件,去處理不一樣情境模式。(好比能夠優化tableview
的時候能夠設置UITrackingRunLoopMode
下不進行一些操做,好比設置圖片等。)
官方文檔分爲三類,
基於端口的
、自定義的
、基於perform selector
。可是也可經過函數調用棧對Source分爲兩類,source0和source1。source1是基於端口的,包含一個match_port和一個回調(函數指針),能主動喚醒Runloop的線程;source0是基於非端口的,只包含一個回調(函數指針),不能主動觸發事件。也就是用戶觸發事件,包括自定義source和perfom selector
注意: 按鈕點擊事件從函數調用棧來看是Source0
事件。其實是點擊屏幕產生event
事件,傳遞給Source1
,而後Source1
派發給Source0
的。
即
CFRunloopTimerRef
,也就是NSTimer
。NSTimer
建立時必須添加到Runloop
中,不然沒法執行,在添加到Runloop
時,必須指定mode
,決定NSTimer
在哪一個mode
下運行。
監聽
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 ,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent() 。 在當前子線程調用[NSRunLoop currentRunLoop] ,若是有就獲取,沒有就建立 |
啓動 | 默認啓動 | 手動啓動 |
獲取 | [NSRunLoop mainRunLoop] 或者CFRunLoopGetMain() |
[NSRunLoop currentRunLoop] 或者CFRunLoopGetCurrent() |
銷燬 | app結束時 | 超時時間到了或者手動結束CFRunLoopStop() |
CFRunLoopStop()
方法只會結束當前的runMode:beforeDate:
調用,而不會結束後續的調用。
Runloop會一直循環檢測事件源CFRunloopSourceRef執行處理函數,首先會產生通知,CoreFundation向線程添加RunloopObserves來監聽事件,並控制Runloop裏面線程的執行和休眠,在有事情作得時候使NSRunloop控制的線程處理事情,空閒的時候讓NSRunloop控制的線程休眠。先處理TimerSource,再處理Source0,而後Source1。
- 一、線程結束
- 二、由於沒有任何事件源而退出( Runloop 只會檢查 Source 和 Timer ,沒有就關閉,不會檢查Observer )
- 三、手動結束 Runloop
啓動方式 | 調用次數 | 描述 |
---|---|---|
run | 循環調用 | 無條件進入是最簡單的作法,但也最不推薦。這會使線程進入死循環,從而不利於控制 runloop,結束 runloop 的惟一方式是 kill 它。它的本質就是無限調用 runMode:beforeDate: 方法。 |
runUntilDate | 循環調用 | 若是咱們設置了超時時間,那麼 runloop 會在處理完事件或超時後結束,此時咱們能夠選擇從新開啓 runloop。也會重複調用 runMode:beforeDate:,區別在於它超時後就不會再調用。這種方式要優於前一種。 |
runMode:beforeDate: | 單次調用 | 這是相對來講最優秀的方式,相比於第二種啓動方式,咱們能夠指定 runloop 以哪一種模式運行。 |
經過run開啓 runloop 會致使內存泄漏,也就是 thread 對象沒法釋放。
Runloop
與線程是一一對應的,主線程的Runloop
默認已經建立好了,子線程的須要本身手動建立(主線程是一一對應的,子線程能夠沒有,也能夠最多有一個Runloop
)
- 一、Timer Source會重複在預設的時間點(建立定時器時指定的時間間隔)向Runloop發送消息,執行任務回調函數。
- 二、主線程因爲默認建立啓動了Runloop因此定時器能夠正常運行,可是子線程要想定時器能夠正常運行,須要手動啓動Runloop。
- 三、另外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 處理的。
利用CFRunLoopMode的特性,能夠將圖片的加載放到NSDefaultRunLoopMode
的mode裏,這樣在滾動UITrackingRunLoopMode
這個mode時不會被加載而影響到。參考:RunLoopWorkDistribution
若是利用scrollView類型的作自動廣告滾動條 須要把定時器加入當前runloop的模式NSRunLoopCommonModes
第一種方法經過子線程監測主線程的 runLoop,判斷兩個狀態區域之間的耗時是否達到必定閾值。
ANREye
就是在子線程設置flag 標記爲YES, 而後在主線程中將flag設置爲NO。利用子線程時闕值時長,判斷標誌位是否成功設置成NO。NSRunLoop調用方法主要就是在kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
之間,還有kCFRunLoopAfterWaiting
以後,也就是若是咱們發現這兩個時間內耗時太長,那麼就能夠斷定出此時主線程卡頓
第二種方式就是FPS監控,App 刷新率應該當努力保持在 60fps,經過
CADisplayLink
記錄兩次刷新時間間隔,就能夠計算出當前的 FPS。CADisplayLink
是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不同,其內部實際是操做了一個 Source)。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 類似),形成界面卡頓的感受。在快速滑動TableView
時,即便一幀的卡頓也會讓用戶有所察覺。
能夠添加Observer監聽RunLoop的狀態。好比監聽點擊事件的處理(在全部點擊事件以前作一些事情)
某些操做,須要重複開闢子線程,重複開闢內存過於消耗性能,能夠設定子線程常駐
若是子線程的NSRunLoop沒有設置source or timer, 那麼子線程的NSRunLoop會馬上關閉
// 無含義,設置子線程爲常住線程,讓子線程不關閉
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
複製代碼
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];
複製代碼
當在操做 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
的即將進入休眠和退出兩種狀態,收到回調是遍歷隊列中待處理的任務一一執行。
蘋果註冊了一個 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進程。
當上面的 _UIApplicationHandleEventQueue()
識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin
/Move
/End
系列回調打斷。隨後系統將對應的 UIGestureRecognizer
標記爲待處理。
蘋果註冊了一個 Observer
監測 BeforeWaiting
(Loop 即將進入休眠) 事件,這個 Observer 的回調函數是 _UIGestureRecognizerUpdateObserver()
,其內部會獲取全部剛被標記爲待處理的 GestureRecognizer
,並執行 GestureRecognizer
的回調。
當有 UIGestureRecognizer
的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。
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
該方法也會失效。// 主線程
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:
複製代碼
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製代碼
一個線程能夠包含多個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); } 複製代碼
Runloop
和GCD
並無直接的關係,當調用了DispatchQueue.main.async
從子線程到主線程進行通訊刷新UI的時候,libDispatch
會向主線程Runloop
發送消息喚醒Runloop
,Runloop
從消息中獲取Block
,而且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
回調裏執行block
操做。dispatch
到非主線程的操做所有是由libDispatch
驅動的。