AutoreleasePool、Block、Runloop整理筆記

@(iOS開發學習)[溫故而知新,掘金博客]html

[TOC]ios

一、AutoreleasePool分析整理

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

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
複製代碼

場景一:對象沒有被加入到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結束後就被釋放了。 github

場景二:對象被加入到手動建立的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迭代中都加入了AutoreleasePoolRunloop開始後建立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結束後釋放

AutoreleasePool定義

  • 自動釋放池是由 AutoreleasePoolPage 以雙向鏈表的方式實現的
  • 當對象調用 Autorelease 方法時,會將對象加入 AutoreleasePoolPage 的棧中
  • 調用 AutoreleasePoolPage::pop 方法會向棧中的對象發送 release 消息

在ARC環境下,以alloc/new/copy/mutableCopy開頭的方法返回值取得的對象是本身生成而且持有的,其餘狀況是非本身持有的對象,此時對象的持有者就是AutoreleasePoolswift

當咱們使用@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 已滿。

  • 一、有 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對象何時釋放?

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

能夠發現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就採用了這項技術,如今讓咱們來看看該技術的實現過程。

  • 一、Tagged Pointer專門用來存儲小的對象,例如NSNumber、NSDate、NSString
  • 二、Tagged Pointer指針的值再也不是地址了,而是真正的值。再也不是一個對象,內存中的位置不在堆中,不須要malloc和free。避免在代碼中直接訪問對象的isa變量,而使用方法isKindOfClass和objc_getClass。
  • 三、在內存讀取上有着3倍的效率,建立時比之前快了106倍。

面試會被問到的問題?

一、進入pool和出pool的時候,引用計數的變化? 二、爲何採用雙向鏈表,而不是單向鏈表? 三、你說到了哨兵,在鏈表中怎麼使用哨兵能夠簡化編程?

參考博客

深刻理解RunLoop

深刻理解AutoreleasePool

黑幕背後的Autorelease

自動釋放池的前世此生 ---- 深刻解析 autoreleasepool

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

Objective-C Autorelease Pool 的實現原理

深刻理解Tagged Pointer

【譯】採用Tagged Pointer的字符串

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

2、Block學習整理

2.0、什麼是Block

Block表面上是一個帶自動變量(局部變量)的匿名函數。本質上是對閉包的對象實現,簡單來講Block就是一個結構體對象。在ARC下,大多數狀況下Block從棧上覆制到堆上的代碼是由編譯器實現的(Blcok做爲返回值或者參數)

2.一、block編譯轉換結構

2.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.二、使用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'

2.1.三、在終端進入.m所在的目錄下面執行genCpp_mac main.m,當前目錄下會生成同名.cpp文件

2.1.四、打開.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.二、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.三、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 {
        ...
    }
}
複製代碼

2.3.一、__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.二、 __NSStackBlock__與__NSGlobalBlock__的區別:是否訪問外部變量

/**
 __NSStackBlock__:訪問外部變量
 __NSGlobalBlock__:沒有訪問外部變量
 */
-(void)blockStatckVsGloable {
    NSInteger i = 10;
    __weak void (^stackBlock)(void) = ^{
        NSLog(@"i = %ld", (long)i);
    };
    __weak void (^globalBlock2)(void) = ^{

    };
}
複製代碼

2.四、訪問修改變量對block結構的影響

2.4.一、全局變量

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

2.4.二、全局靜態變量

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

2.4.三、局部變量

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.四、局部靜態變量(修改不須要__Block修飾)

對於訪問和修改局部靜態變量,Block須要截獲靜態變量的指針,改變的時候直接經過指針改變值

2.4.五、self隱式循環引用

發生循環引用的時候,self強持有Block,從下面能夠看出Block也是強持有self的。

2.六、block的輔助函數

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

2.六、訪問or修改外部變量

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

參考博客

《Objective-C高級編程》Blocks

Block技巧與底層解析

Block源碼解析和深刻理解

談Objective-C block的實現

block沒那麼難(二):block和變量的內存管理

源碼解析之從Block說開去

iOS 中的 block 是如何持有對象的

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

[TOC]

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

問題

一、什麼是RunLoop

RunLoop實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於這個函數內部 「接受消息->等待->處理」 的循環中,直到這個循環結束(好比傳入 quit 的消息),函數返回。通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要RunLoop,讓線程能隨時處理事件但並不退出。RunLoop核心是一個有條件的循環。

Runloop組成

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


一、一個 Runloop 包含若干個mode,每一個mode又包含若干個sourceInputSourceTimerSourceObservers) 二、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下不進行一些操做,好比設置圖片等。)

一、InputSource:

官方文檔分爲三類,基於端口的自定義的、基於perform selector。可是也可經過函數調用棧對Source分爲兩類,source0和source1。source1是基於端口的,包含一個match_port和一個回調(函數指針),能主動喚醒Runloop的線程;source0是基於非端口的,只包含一個回調(函數指針),不能主動觸發事件。也就是用戶觸發事件,包括自定義source和perfom selector

注意: 按鈕點擊事件從函數調用棧來看是Source0事件。其實是點擊屏幕產生event事件,傳遞給Source1,而後Source1派發給Source0的。

二、TimerSource:

CFRunloopTimerRef,也就是NSTimerNSTimer建立時必須添加到Runloop中,不然沒法執行,在添加到Runloop時,必須指定mode,決定NSTimer在哪一個mode下運行。

三、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。

中止:

  • 一、線程結束
  • 二、由於沒有任何事件源而退出( Runloop 只會檢查 SourceTimer ,沒有就關閉,不會檢查Observer
  • 三、手動結束 Runloop

Runloop啓動方式

啓動方式 調用次數 描述
run 循環調用 無條件進入是最簡單的作法,但也最不推薦。這會使線程進入死循環,從而不利於控制 runloop,結束 runloop 的惟一方式是 kill 它。它的本質就是無限調用 runMode:beforeDate: 方法。
runUntilDate 循環調用 若是咱們設置了超時時間,那麼 runloop 會在處理完事件或超時後結束,此時咱們能夠選擇從新開啓 runloop。也會重複調用 runMode:beforeDate:,區別在於它超時後就不會再調用。這種方式要優於前一種。
runMode:beforeDate: 單次調用 這是相對來講最優秀的方式,相比於第二種啓動方式,咱們能夠指定 runloop 以哪一種模式運行。

經過run開啓 runloop 會致使內存泄漏,也就是 thread 對象沒法釋放。

Runloop的做用:

  • 一、保持程序的持續運行(好比主運行循環)接收用戶的輸入
  • 二、處理App中的各類事件(好比觸摸事件、定時器事件、Selector事件)
  • 三、任務調度(主調方產生不少事件,不用等到被調方執行完畢事件,採起執行其餘操做)
  • 四、節省CPU資源,提升程序性能:該作事時作事,該休息時休息

RunLoop處理邏輯

摘自

二、Runloop和線程的關係

Runloop與線程是一一對應的,主線程的Runloop默認已經建立好了,子線程的須要本身手動建立(主線程是一一對應的,子線程能夠沒有,也能夠最多有一個Runloop

三、Runloop與NSTimer的關係

  • 一、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 爲了節省資源,並不會在很是準確的時間點回調這個 TimerTimer 有個屬性叫作 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 處理的。

四、RunLoop應用

4.一、TableView中實現平滑滾動延遲加載圖片

利用CFRunLoopMode的特性,能夠將圖片的加載放到NSDefaultRunLoopMode的mode裏,這樣在滾動UITrackingRunLoopMode這個mode時不會被加載而影響到。參考:RunLoopWorkDistribution

4.二、解決NSTime在ScrollView滾動時無效

若是利用scrollView類型的作自動廣告滾動條 須要把定時器加入當前runloop的模式NSRunLoopCommonModes

4.三、檢測UI卡頓

第一種方法經過子線程監測主線程的 runLoop,判斷兩個狀態區域之間的耗時是否達到必定閾值。ANREye就是在子線程設置flag 標記爲YES, 而後在主線程中將flag設置爲NO。利用子線程時闕值時長,判斷標誌位是否成功設置成NO。NSRunLoop調用方法主要就是在kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting以後,也就是若是咱們發現這兩個時間內耗時太長,那麼就能夠斷定出此時主線程卡頓


第二種方式就是FPS監控,App 刷新率應該當努力保持在 60fps,經過CADisplayLink記錄兩次刷新時間間隔,就能夠計算出當前的 FPS。 CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不同,其內部實際是操做了一個 Source)。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 類似),形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。

4.四、利用空閒時間緩存數據或者作其餘的性能優化相關的任務

能夠添加Observer監聽RunLoop的狀態。好比監聽點擊事件的處理(在全部點擊事件以前作一些事情)

4.五、子線程常駐

某些操做,須要重複開闢子線程,重複開闢內存過於消耗性能,能夠設定子線程常駐

若是子線程的NSRunLoop沒有設置source or timer, 那麼子線程的NSRunLoop會馬上關閉

一、添加Source
// 無含義,設置子線程爲常住線程,讓子線程不關閉
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
複製代碼
二、添加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.六、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.七、事件響應

蘋果註冊了一個 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.八、手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。

蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop 即將進入休眠) 事件,這個 Observer 的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行 GestureRecognizer 的回調。

當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。

五、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;
複製代碼

六、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); } 複製代碼

七、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須要注意的地方

相關文章
相關標籤/搜索