Blocks深入理解和詳解

FlyOceanFish發表於2018-04-28

介紹

Block是C級語法和執行時特性。它們類似於標準C函式,但是除了可執行程式碼之外,它們還可能包含對自動(堆疊)或託管(堆)記憶體的變數繫結。因此,Block可以維護一組狀態(資料),它可以用來在執行時影響行為。

您可以使用Blocks來組合函式表示式,這些表示式可以被傳遞給API,可選地儲存,並由多個執行緒使用。Block對於回撥來說特別有用,因為塊包含在回撥上執行的程式碼和執行過程中需要的資料。

可以在GCC和Clang中使用OS X v10.6 Xcode開發工具。您可以使用OS X v10.6和之後的模組,以及隨後的iOS 4.0。Block執行時是開源的,可以在LLVM的編譯器-rt子專案儲存庫中找到。block也被提交給C標準工作組作為N1370:蘋果對C的擴充套件,因為Objective-C和c++都是從C派生出來的,block被設計用於與所有三種語言(以及Objective-C++)一起工作。語法反映了這個目標。

概念普及

  • Block本質是Objective-C的物件,不是函式指標(這個有點混淆,網上普遍都說是函式指標)
  • Block型別變數本質是函式指標(如下宣告其實在編譯成C語言原始碼的時候就是一個函式指標)
  • 截獲自動變數 所有的變數都會截獲嗎?比如全域性變數、靜態變數。其實截獲的只是區域性變數而已
  • __block變數加了它為啥就能被修改了?很多人也許會說是傳入了地址,這種說法很片面的,如果是這樣的其實完全不用加__block,蘋果幫你做了就行了呀。其實__block之後翻譯成C語言原始碼之後,變數會變成一個物件,該物件儲存了原來變數的地址,函式傳入的是一個物件。

探尋Block本質

通過LLVM的編譯器-rt子專案儲存庫中找到runtime下的Block原始碼

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};
複製程式碼

當我們宣告一個Block的時候,編譯器其實會將block轉換成以上struct結構體。 其中isa指向的是Block具體的類。有如下6中,不過其中StackBlockMallocBlockGlobalBlock是比較常見的

/* the raw data space for runtime classes for blocks */
/* class+meta used for stack, malloc, and collectable based blocks */
BLOCK_EXPORT void * _NSConcreteStackBlock[32];
BLOCK_EXPORT void * _NSConcreteMallocBlock[32];
BLOCK_EXPORT void * _NSConcreteAutoBlock[32];
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32];
BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32];
複製程式碼

invoke函式指標則是對應Objective-C中程式碼的具體實現

看到這裡不知大家有沒有想到runtime中objc_object的isa呢? 其實兩個原理是一樣的。這裡就不具體介紹objc_object

所以Block即為Objective-C的物件

Block型別變數

Block的型別變數宣告如下:

int (^blk) (int);

我們宣告一個Block型別變數並且賦值

int (^blk) (int) = ^(int count){return count+1};
複製程式碼

然後我們通過如下

xcrun -sdk iphonesimulator11.2 clang -rewrite-objc -F /Applications/Xcode\ 2.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/System/Library/Frameworks ViewController.m

我們可以得到以下原始碼:

//對應的Block具體struct結構體
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//Block方法的具體實現
static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, int count) {

        return count+1;
    }

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    int (*blk) (int) = ((int (*)(int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA));
    ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 2);
}
複製程式碼

其中int (*blk) (int)則就是對應我們的Block型別變數,這裡就就可以看到了Block型別變數的本質了,其實就是C語言的函式指標

截獲自動變數

說截獲自動變數之前我們先看以下程式碼

int tmp = 2;
int (^blk) (int) = ^(int count){
    return count+tmp;
};
tmp = 3;
int result = blk(2);
NSLog(@"%d",result);
複製程式碼

以上程式碼會列印出多少呢?5還是4?? 正確答案是4

為什麼是4呢?其實就是因為Block截獲自動變數的原因

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int tmp;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _tmp, int flags=0) : tmp(_tmp) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, int count) {
  int tmp = __cself->tmp; // bound by copy

        return count+tmp;
    }

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    int tmp = 2;
    int (*blk) (int) = ((int (*)(int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, tmp));
    tmp = 3;
    int result = ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 2);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6n_mdf6rn0d5f5cw6r1h2620h3w0000gn_T_ViewController_b80e84_mi_0,result);
}`__ViewController__viewDidLoad_block_impl_0`
複製程式碼

通過以下程式碼可以看出,當我們在給Block型別變數賦值的時候,tmp變數同時被傳入,並且被儲存到了__ViewController__viewDidLoad_block_impl_0的struct中。這時候其實就是截獲了自動變數,由於已經在struct類中儲存了一份,即使後邊更改,也不會影響Block截獲的值。

int (*blk) (int) = ((int (*)(int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, tmp));
複製程式碼

為什麼要對區域性變數進行截獲呢?而全域性變數和靜態變數不需要截獲,並且修改的的時候也不需要加__block呢?

主要原因就是變數的生命週期。區域性變數在程式碼塊執行結束之後就會被釋放,但是Block不一定在此時釋放。所以就會出現變數超過生命週期的現象,此時對區域性變數進行截獲,即使區域性變數被釋放,但是Block同樣還是可以正常使用的。因為全域性變數和靜態變數的釋放時間肯定不會在Block之前,所以不必對他們進行截獲。

全域性變數和靜態變數儲存在全域性資料區;區域性變數儲存在棧中

__block變數儲存域探究

不知道大家有沒有想過一個問題,為什麼需要__block呢?如果沒有__block難道就修改不了變數了嗎?

我們先看一下加了__block編譯器給我們做了啥

struct __Block_byref_tmpBlock_0 {
  void *__isa;
__Block_byref_tmpBlock_0 *__forwarding;
 int __flags;
 int __size;
 int tmpBlock;
};

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __Block_byref_tmpBlock_0 *tmpBlock; // by ref
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_tmpBlock_0 *_tmpBlock, int flags=0) : tmpBlock(_tmpBlock->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, int count) {
  __Block_byref_tmpBlock_0 *tmpBlock = __cself->tmpBlock; // bound by ref

        (tmpBlock->__forwarding->tmpBlock) = 100;
        return count+(tmpBlock->__forwarding->tmpBlock);
    }
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->tmpBlock, (void*)src->tmpBlock, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->tmpBlock, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
  void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0), __ViewController__viewDidLoad_block_copy_0, __ViewController__viewDidLoad_block_dispose_0};

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    __attribute__((__blocks__(byref))) __Block_byref_tmpBlock_0 tmpBlock = {(void*)0,(__Block_byref_tmpBlock_0 *)&tmpBlock, 0, sizeof(__Block_byref_tmpBlock_0), 2};
    int (*blk) (int) = ((int (*)(int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, (__Block_byref_tmpBlock_0 *)&tmpBlock, 570425344));
    int result = ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 2);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6n_mdf6rn0d5f5cw6r1h2620h3w0000gn_T_ViewController_a1c583_mi_0,result);
}

複製程式碼

這時候大家可以與前邊沒有加__block的程式碼進行比較,兩者的差別在哪裡。 其實大家應該很容易發現加了__block之後,變數形成了一個struct,這個struct中儲存了變數的值,同時還有一個__forwarding。這個__forwarding其實就是為什麼需要__block的關鍵。

Block從棧複製到堆的時候,__block變數也會受到影響。如下:

__block變數的配置儲存域 Block從棧到堆時的影響
從棧複製到堆並被Block持有
被Block持有

ARC下,Block如果是棧的話,預設會copy到堆上。此時所使用的__block變數同時也會從棧被複制到堆上如下圖

儲存

那如果Block在堆上了,我們在Block中修改了變數,怎麼讓棧上的變數也同時能正確訪問呢?這其實就是__forwarding功勞了。

__block變數初始化的時候__forwarding是指向本身自己的。當__block變數從棧複製到堆上的時候,此時會將__forwarding的值替換為複製到目標堆上的__block變數用結構體例項的地址。如下圖:

Blocks深入理解和詳解

通過此功能,無論是在Block語法中、Block語法外使用__block變數,還是__block變數配置在棧上或堆上,都可以順利地訪問一個__block變數。 到這裡大家應該明白了"__block加了之後,是把變數的地址傳入Block"的說法是很片面的吧啦

我的部落格

FlyOceanFish

相關文章