iOS概念攻堅之路(七):block

前言

這篇文章主要圍繞幾個問題展開:git

  1. block 是什麼
  2. block 的類型
  3. __block 是什麼,以及它的 forwarding 指針的用處
  4. block 爲何會形成循環引用
  5. block 的拷貝機制
  6. block 的運用

block 是什麼

block 的定義:帶有自動變量(局部變量)匿名函數github

主要是弄清楚「帶有」、「自動變量」和「匿名函數」是什麼,咱們就能知道 block 大概是個什麼東西了。框架

自動變量 指的是局部做用域變量,具體來講便是在控制流進入變量做用域時系統自動爲其分配存儲空間,並在離開做用域時釋放空間的一類變量。在許多程序語言中,自動變量與術語局部變量所指的變量其實是同一種變量,因此一般狀況下 「自動變量" 和 "局部變量" 是同義的。ide

主要意思是自動變量的生命週期由系統控制,當自動變量超過其做用域,會被系統自動釋放。在 iOS 說自動變量,能夠當作局部變量來理解。函數

而匿名函數,就是 沒有名稱的函數。C 語言的標準是不容許這樣的函數存在的,由於調用函數必須知道函數名。固然也可使用函數指針來調用,不過在賦值給函數指針的時候,也是須要知道函數名,否則也沒法得到該函數的地址。post

來看一個簡單的 block:ui

^() {
    printf("a simple block");
}
複製代碼

這個函數就是沒有名字的。spa

那什麼是「帶有」呢?指針

帶有其實就是咱們常說的 捕獲,那爲何一個 block 要去捕獲自動變量呢?其實 block 在 OC 中本質上也是一個 OC 對象,它有它的結構,在它結構內部也有 isa 指針,它是一個 封裝了函數調用以及函數調用環境的 OC 對象。也就是說在你調用這個 block 的時候,它須要保證它的調用環境是可用的,而自動變量的生命週期是由系統控制的,當你調用 block 的時候,極可能其中使用到的自動變量已經被釋放了,因此要把自動變量捕獲進 block 結構體的內部,才能保證函數的調用環境。捕獲的意思指 block 所使用的自動變量值被自動保存到 block 結構體實例中。rest

那麼還會不會捕獲其餘變量?好比靜態變量、全局變量、靜態全局變量?其實不會,雖然這些變量的做用域不一樣,可是在整個程序中,一個變量總保持在一個內存區域,所以,雖然屢次使用,可是無論在任什麼時候候以任何狀態調用,使用的都是相同的內存區域,同一個變量,因此並不須要捕獲這些變量。

block 的類型

那是否是全部的 block 都會捕獲變量呢?也不是,其實只要保證函數調用環境就能夠,block 在 OC 中有三種類型:

  • 全局 block(_NSConcreteGlobalBlock),存在數據區(.data 區)
  • 堆 block(_NSConcreteStackBlock),存在堆區
  • 棧 block(_NSConcreteMallocBlock),存在棧區

在寫全局變量的位置定義 block 的時候,生成的 block 類型是全局 block,由於在使用全局區域的地方不能使用自動變量,因此不存在對自動變量進行捕獲。其實還有一種狀況,就算 block 在日常定義全局變量的地方定義,使用的類型也是 _NSConcreteGlobalBlock 類型,那就是在沒有捕獲自動變量的時候。因此全局 block 有兩種狀況:

  • 在記述全局變量的地方用 block 時
  • block 沒有截獲自動變量時

除此以外的 block 語法生成的 block 就全是 _NSConcreteStackBlock 類型的了,也就是棧 block。還有一個堆 block 是怎麼來的呢?其實不是咱們建立出來的,是系統根據狀況幫咱們從棧上覆制到堆上的。之因此要複製也是由於做用域的問題,設置在棧上的 block,若是其所屬的變量做用域結束,該 block 也會被廢棄,因此得拷貝到堆上,除了系統自動生成,咱們也能夠手動調用 block 的 copy 方法將棧上的 block 拷貝到堆上。

簡單列一下棧上的 block 複製到堆上的狀況(ARC):

自動複製:

  • block 做爲函數返回值時(自動生成複製到堆上的代碼)
  • 將 block 賦值給附有 __strong 修飾符 id 類型的類或 block 類型的成員變量時
  • block 做爲 Cocoa API 中方法名含有 usingBlock 的方法參數時
  • block 做爲 GCD API 的方法參數時

手動複製:

  • 調用 copy 方法

在調用 block 的 copy 實例方法時,若是 block 配置在棧上,那麼該 block 會從棧複製到堆。block 做爲函數返回值返回時,將 block 賦值給附有 __strong 修飾符 id 類型的類或 block 類型的成員變量時,編譯器自動的將對象的 block 做爲參數並調用 _Block_copy 函數,這與調用 block 的 copy 實例方法的效果相同。在方法名中含有 usingBlock 的 Cocoa 框架方法或 Grand Central Dispatch 的 API 中傳遞 block 時,在該方法或函數內部對傳遞過來的 block 調用 block 的 copy 實例方法或者 _Block_copy 函數。

棧 block 也是否是去持有外部對象的,只有堆 block 纔會去持有外部對象,棧 block 不捕獲是由於它的生命週期大於等於它所使用的自動變量的生命週期。堆 block 對去持有外部對象,也就是捕獲自動變量,在堆 block 將被釋放的時候,會對它所持有的對象進行一次 release 操做。

編譯器大多數狀況下都能判斷出是否須要複製,可是有一種狀況是判斷不出來的,那就是:

  • 向方法或函數的參數傳遞 block 時

可是若是在方法或函數中適當的複製了傳遞過來的參數,那麼就沒必要在調用該方法或函數前手動複製了。

要注意一個問題就是,將 block 從棧上覆制到堆上是很消耗 CPU 的,因此當 block 設置在棧上就可以知足需求的話,將其複製到堆上是一種資源的浪費。

棧上的 block 調用 copy 會將 block 複製到堆中,那麼堆 block 和全局 block 調用 copy 方法又會發生什麼呢?列了一個表,以下:

Block 的類 副本源的配置存儲域 複製效果
_NSConcreteStackBlock 從棧複製到堆
_NSConcreteGlobalBlock 程序的數據區域(全局區) 什麼也不作
_NSConcreteMallocBlock 引用計數增長

前面提到堆 block 將被釋放會對所持有的對象進行一次 release 操做,來看一下堆 block 對一個自動變量的捕獲過程:

  1. 調用 block 內部的 copy 函數
  2. copy 函數內部會調用 _Block_object_assign 函數
  3. _Block_object_assign 函數會根據自動變量的修飾符(__strong__weak__unsafe_unretained)作出相應操做,相似於 retain,造成強引用、弱應用

當 block 從堆上移除:

  1. 會調用 block 內部的 dispose 函數
  2. dispose 函數內部會調用 _Block_object_dispose 函數
  3. _Block_object_dispose 函數會自動釋放引用的自動變量,相似於 release

其中主要涉及兩個函數,copy 函數和 dispose 函數,當棧上的 block 複製到堆時調用 copy 函數,當堆上的 block 被廢棄時調用 dispose 函數。

來個小結,block 分三種類型,全局 block、堆 block、棧 block,只有堆纔會捕獲變量,而且只捕獲自動變量,也就是局部變量。

__block

block 有一個現象,那就是沒法修改外部變量,如:

int a = 1;
void (^blk)(void) = ^{
    a = 2;
};
複製代碼

上面代碼會如下錯誤:

Variable is not assignable (missing __block type specifier)
複製代碼

提示咱們須要使用 __block 對變量進行修飾,也就是在 int a 前使用 __block 來修飾。爲何 block 不能修改外部對象?__block 後又能夠修改外部對象了?

先來看看它爲何不能修改外部的對象,前面提到 block 能夠捕獲自動變量,可是 block 只捕獲自動變量的值,而並不捕獲它的地址,至關於在 block 內部新建了一個屬性,存儲了所使用的對象的自動變量的 ,因此在 block 內部使用的自動變量已經不是以前的那個自動變量,即便你修改也影響不了以前的自動變量。基於這個緣由,蘋果在編譯器編譯的過程檢測到給截獲的自動變量賦值操做時,就會產生一個編譯錯誤。

那爲何加上了 __block 做爲修飾就能夠了呢?實際上是由於系統幫咱們從新生成了一個新的對象,來看一段代碼:

__block int val = 10;
void (^blk)(void) = ^{ val = 1; };
複製代碼

該代碼編譯成 C++ 代碼後以下:

int main() {
    __Block_byref_val_0 val = {
        0,
        &val,
        0,
        sizeof(__Block_byref_val_0),
        10
    };
    
    blk = &__main_block_impl_0 (
        __main_block_func_0, &__main_block_desc_0_DAT, &val, 0x22000000);
    
    return 0;
}
複製代碼

也就是以前 int 類型的 val 被轉變成了 __Block_byref_val_0 類型的一種結構體,該結構體的聲明以下:

struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;
}
複製代碼

該結構體中最後的成員變量 val 就是以前的 int val

^{ val = 1; } 被轉成了什麼呢?以下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    __Block_byref_val_0 *val = __cself->val;
    (val->__forwarding->val) = 1;
}
複製代碼

看一下它的查找過程 (val->__forwarding->val),block 的 __main_block_impl_0 結構體實例 __cself 指向 __block 變量的 __Block_byref_val_0 結構體的指針,__Block_byref_val_0 結構體實例的成員變量 __forwarding 持有指向該實例自身的指針,經過成員變量 __forwarding 訪問成員變量 val。(成員變量 val 是該實例自身持有的變量,它至關於原自動變量。)查找過程以下圖:

__block 變量的 __Block_byref_val_0 結構體並不在 block 內部的結構體中,這樣作是爲了在多個 block 中使用 __block 變量。

那還有一個問題,就是爲何須要有一個 __forwarding 指針去指向本身?其實這是爲了保證無論 __block 變量配置在棧上仍是堆上時都可以正確訪問 __block 變量。怎麼說呢?當 block 從棧上被複制到堆時,在棧區的 __block 變量也會複製一份到堆中,此時會將 __block 的成員變量 forwarding 的值替換爲複製目標堆上的 __block 變量用結構體實例的地址。以下圖:

那麼這樣,不管是 block 語法中、block 語法外使用 __block 變量,仍是 __block 變量配置在棧上仍是堆上,均可以順利的訪問同一個 __block 變量。

block 的循環引用

若是在 block 中使用附有 __strong 修飾符的對象類型自動變量,那麼當 block 從棧複製到堆時,該對象爲 block 持有,這樣容易引發循環引用。

好比:

self 持有 block,block 持有 self,這就造成了循環引用。解決的方式有三種:

  • __weak
  • __unsafe_unretained
  • __block

(1)__weak 的方式:

- (id)init 
{
    self = [super init];
    
    id __weak tmp = self;
    
    blk_ = ^{ NSLog(@"self = %@", tmp); };
    
    return self;
}
複製代碼

(2)__unsafe_unretained 的方式:

- (id)init
{
    self = [super init];
    
    id __unsafe_unretained tmp = self;
    
    blk = ^{ NSLog(@"self = %@", tmp); };
    
    return self;
}
複製代碼

__unsafe_unretained__weak 的區別在於 __unsafe_unretained 所指向的對象被回收以後,__unsafe_unretained 指針並不會自動置爲 nil,此時 __unsafe_unretained 指針就是懸垂指針,對懸垂指針進行操做可能會引起崩潰。

(3)那麼還有一種 __block 來破解循環引用的方式:

typedef void (^blk_t)(void);

@interface MyObject : NSObject
{
    blk_t blk_;
}

@implementation MyObject

- (instancetype)init {
    self = [super init];
    
    __block id tmp = self;
    
    blk_ = ^{
        NSLog(@"self = %@", tmp);
        tmp = nil;
    };
    
    return self;
}

- (void)execBlock {
    blk_();
}

- (void)dealloc {
    NSLog(@"%s",__func__);
}

@end

int main() {
    id o = [[MyObject alloc] init];
    
    [o execBlock];
    
    return 0;
}
複製代碼

這種方式有一個問題,就是必需要執行 block 才能解除引用鏈條,由於 tmp = nil 是寫在 block 內部的。反正就目前來講,99% 狀況下使用的都是 __weak。還有一種狀況是先 __weak,而後在 block 內部再使用一個 __strong 指針去強引用它,以下:

__weak typeof(self) weakSelf = self;
blk_ = ^{
    __strong typeof(self) strongSelf = weakSelf;
    NSLog(@"%@",strongSelf);
};
複製代碼

這樣是爲了保證 block 內部的代碼可以執行完,由於在執行 NSLog 以前,self 有可能會被釋放,因此對其進行一個強引用。若是出現雙層循環或多層循環,要再對 strongSelf 進行 __weak 而後再 __strong。。。這樣一層層嵌套。

放一下我項目中用到的 @weakify@strongify 宏定義:

// 弱引用
#ifndef weakify
#if DEBUG
#if __has_feature(objc_arc)
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
#endif
#else
#if __has_feature(objc_arc)
#define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
#endif
#endif
#endif

// 強引用
#ifndef strongify
#if DEBUG
#if __has_feature(objc_arc)
#define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
#endif
#else
#if __has_feature(objc_arc)
#define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
#endif
#endif
#endif
複製代碼

爲 UIButton 添加 block

最後說一個小例子,就是爲按鈕或者 UIView 類型的控件添加 block,你們能夠按照這篇 文章 來實現這個小輪子。我從新再封裝了一下,放到了 github 之中,使用大概是這樣子的:

#import "UIButton+FRButtonEventBlock.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.view addSubview:self.aButton];
    
    self.aButton.fr_touchUpInside = ^{
        NSLog(@"touchUpInside");
    };
    
    self.aButton.fr_touchUpOutside = ^{
        NSLog(@"touchUpOutside");
    };
    
    self.aButton.fr_touchDown = ^{
        NSLog(@"touchDown");
    };
    
    self.aButton.fr_touchCancel = ^{
        NSLog(@"touchCancel");
    };
}
複製代碼

參考文章

iOS中Block的用法,示例,應用場景,與底層原理解析(這多是最詳細的Block解析)

iOS開發 | 讓你的UIButton自帶block

相關文章
相關標籤/搜索