AutoreleasePool、Block、Runloop整理筆記

在路上重名了啊發表於2018-12-13

@(iOS開發學習)[溫故而知新,掘金部落格]

[TOC]

1、AutoreleasePool分析整理

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

AutoreleasePool、Block、Runloop整理筆記

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、Block、Runloop整理筆記

場景二:物件被加入到手動建立的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、Block、Runloop整理筆記

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

AutoreleasePool、Block、Runloop整理筆記

場景四:(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、Block、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物件的地址

AutoreleasePool、Block、Runloop整理筆記

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、Block、Runloop整理筆記

objc_autoreleasePoolPush 執行過程

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

AutoreleasePool、Block、Runloop整理筆記

  • 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、Block、Runloop整理筆記

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

AutoreleasePool、Block、Runloop整理筆記
可以發現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倍。

面試會被問到的問題?

1、進入pool和出pool的時候,引用計數的變化? 2、為什麼採用雙向連結串列,而不是單向連結串列? 3、你說到了哨兵,在連結串列中怎麼使用哨兵可以簡化程式設計?

參考部落格

深入理解RunLoop

深入理解AutoreleasePool

黑幕背後的Autorelease

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

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

Objective-C Autorelease Pool 的實現原理

深入理解Tagged Pointer

【譯】採用Tagged Pointer的字串

Objective-C高階程式設計(一) 自動引用計數,看我就夠了

二、Block學習整理

2.0、什麼是Block

Block表面上是一個帶自動變數(區域性變數)的匿名函式。本質上是對閉包的物件實現,簡單來說Block就是一個結構體物件。在ARC下,大多數情況下Block從棧上覆制到堆上的程式碼是由編譯器實現的(Blcok作為返回值或者引數)

2.1、block編譯轉換結構

2.1.1、新建一個macOS專案,編寫一個最簡單的Block。

AutoreleasePool、Block、Runloop整理筆記

#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_macalias genCpp_mac='clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'

AutoreleasePool、Block、Runloop整理筆記

2.1.3、在終端進入.m所在的目錄下面執行genCpp_mac main.m,當前目錄下會生成同名.cpp檔案

AutoreleasePool、Block、Runloop整理筆記

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整個生命週期

AutoreleasePool、Block、Runloop整理筆記

AutoreleasePool、Block、Runloop整理筆記

  • 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);
    };
}
複製程式碼

AutoreleasePool、Block、Runloop整理筆記

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) = ^{

    };
}
複製程式碼

AutoreleasePool、Block、Runloop整理筆記

2.4、訪問修改變數對block結構的影響

2.4.1、全域性變數

對於訪問和修改全域性變數,Block的結構不會發生變化。

AutoreleasePool、Block、Runloop整理筆記

2.4.2、全域性靜態變數

對於訪問和修改全域性靜態變數,同全域性變數,Block的結構不會發生變化。

2.4.3、區域性變數

ARC下NSConcreteStackBlock如果引入了區域性變數,會被NSConcreteMallocBlock 型別的 block 替代。

訪問區域性變數

AutoreleasePool、Block、Runloop整理筆記

修改區域性變數(區域性變數需要使用__Block修飾)

AutoreleasePool、Block、Runloop整理筆記
函式__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需要截獲靜態變數的指標,改變的時候直接通過指標改變值

AutoreleasePool、Block、Runloop整理筆記

2.4.5、self隱式迴圈引用

發生迴圈引用的時候,self強持有Block,從下面可以看出Block也是強持有self的。

AutoreleasePool、Block、Runloop整理筆記

2.6、block的輔助函式

詳情請參考Block技巧與底層解析

2.6、訪問or修改外部變數

截圖來自談Objective-C block的實現

AutoreleasePool、Block、Runloop整理筆記

參考部落格

《Objective-C高階程式設計》Blocks

Block技巧與底層解析

Block原始碼解析和深入理解

談Objective-C block的實現

block沒那麼難(二):block和變數的記憶體管理

原始碼解析之從Block說開去

iOS 中的 block 是如何持有物件的

三、Runloop學習整理(持續更新糾正)

[TOC]

@(iOS開發學習)[溫故而知新]

問題

AutoreleasePool、Block、Runloop整理筆記

1、什麼是RunLoop

RunLoop實際上就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面 Event Loop 的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 “接受訊息->等待->處理” 的迴圈中,直到這個迴圈結束(比如傳入 quit 的訊息),函式返回。一般來講,一個執行緒一次只能執行一個任務,執行完成後執行緒就會退出。如果我們需要RunLoop,讓執行緒能隨時處理事件但並不退出。RunLoop核心是一個有條件的迴圈。

AutoreleasePool、Block、Runloop整理筆記

AutoreleasePool、Block、Runloop整理筆記

Runloop組成

RunLoop的結構需要涉及到以下4個概念:Run Loop ModeInput SourceTimer SourceRun Loop Observer

AutoreleasePool、Block、Runloop整理筆記


1、一個 Runloop 包含若干個mode,每個mode又包含若干個sourceInputSourceTimerSourceObservers) 2、Runloop 啟動只能指定一個mode,若要切換mode只能重新啟動Runloop指定另外一個mode。這樣做的目的是為了處理優先順序不同的SourceTimerObserver

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,也就是NSTimerNSTimer建立時必須新增到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 只會檢查 SourceTimer ,沒有就關閉,不會檢查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處理邏輯

AutoreleasePool、Block、Runloop整理筆記

AutoreleasePool、Block、Runloop整理筆記

AutoreleasePool、Block、Runloop整理筆記

摘自

AutoreleasePool、Block、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 為了節省資源,並不會在非常準確的時間點回撥這個 TimerTimer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到後,容許有多少最大誤差。

如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回撥也會跳過去,不會延後執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

建立定時器的時候,會以NSUerDefaultRunloopMode的mode自動加入到當前執行緒中。因此下面兩種效果是等價的。

AutoreleasePool、Block、Runloop整理筆記

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呼叫方法主要就是在kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之間,還有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最終的更新操作放到主執行緒中,同時提供了一套類似UIViewCAlayer的相關屬性,儘可能保證開發者的開發習慣。監聽主執行緒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 SourceRunloop的啟動;Runloop依賴Source(不限於Timer Source),沒有Sources就會退出

參考:關於 performSelector 的一些小探討

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()
    }
}
複製程式碼
  • - 當呼叫 NSObjectperformSelecter: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]);列印出的日誌部分截圖如下:

AutoreleasePool、Block、Runloop整理筆記

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

RunloopGCD並沒有直接的關係,當呼叫了DispatchQueue.main.async從子執行緒到主執行緒進行通訊重新整理UI的時候,libDispatch會向主執行緒Runloop傳送訊息喚醒RunloopRunloop從訊息中獲取Block,並且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回撥裡執行block操作。dispatch到非主執行緒的操作全部是由libDispatch驅動的。

參考部落格

Run Loops 官方文件

RunLoop在iOS開發中的應用

格而知之5:我所理解的Run Loop

深入理解RunLoop

深入研究 Runloop 與執行緒保活

解密-神祕的 RunLoop

視訊學習

runLoop學習筆記

iOS學習之深入理解RunLoop

RunLoop與Timer以及常用Mode
NSTimer需要注意的地方

相關文章