Block 底層原理

Block語法?

Block可以認為是一個匿名函數。語法聲明如下:

return_type (^block_name)(parameters)?

例如:

double (^multiplyTwoValues)(double, double);?

Block字面值的寫法:

^ (double firstValue, double secondValue) {?

return firstValue *secondValue;?

}?

上面寫法省略了返回值的類型,可以顯示的指出返回值類型。

typedef double (^MultiplyTwoVlaues)(double, double);?

MultiplyTwoVlaues mtv = ^(double firstValue, double secondValue){?

return firstValue * secondValue;?

};?

NSLog(@"%f", mtv(3, 4));?

Block也是一個Objective-C對象,可以用于賦值,當參數傳遞,也可以放在集合中。

數據結構定義?

block的數據結構定義:


block?

對應的結構體定義如下:

struct Block_descriptor {?

unsigned long int reserved;?

unsigned long int size;?

void (*copy)(void *dst, void *src);?

void (*dispose)(void *);?

};?

struct Block_layout {?

void *isa;?

int flags;?

int reserved;?

void (*invoke)(void *, ...);?

struct Block_descriptor *descriptor;?

/* Imported variables. */?

};?

block實際上由6部分組成:

isa指針,所有對象都有該指針,用來指向對象相關實現。?

flags,用于按位表示一些block附加信息。?

reserved, 保留變量。?

invoke, 函數指針,指向具體的block實現的函數調用地址。?

descriptor, 表示該block的附加描述信息,主要是size的大小,以及copy和dispose函數指針。?

variables,capture過來的變量,block能夠訪問它外部的局部變量,就是因為將這些變量復制到了結構體中。?

Block分類?

在Objective-C中,一共有3種類型的Block:

_NSConcreteGlobalBlock全局的靜態block,不會訪問任何外部變量。?

_NSConcreteStackBlock 保存在棧中的block,當函數返回時會被銷毀。?

_NSConcreateMallocBlock 保存在堆中的block,當引用計數為0時會被銷毀。?

Clang?

為了研究編譯器是如何實現block的,需要使用clang,clang命令,可以將Objective-C的源碼改寫成C語言,命令如下:

clang -rewrite-objc xxx.c?

Block實現?

全局的靜態Block實現?

新建一個globalBlock.c的源文件:

#include ?

int main()?

{?

^{?

printf("Hello, World!/n");?

} ();?

return 0;?

}?

在文件所在的文件夾,使用命令:clang -rewrite-objc globalBlock.c,在目錄中得到一個名為globalBlock.c的文件。其中關鍵代碼:

struct __block_impl {?

void *isa;?

int Flags;?

int Reserved;?

void *FuncPtr;?

};?

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) {?

printf("Hello, World!/n");?

}?

static struct __main_block_desc_0 {?

size_t reserved;?

size_t Block_size;?

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};?

int main()?

{?

((void (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA)) ();?

return 0;?

}?

下面具體看一下是如何實現的。__main_block_impl_0就是block的實現,從中可以看出:

一個block實際上就是一個對象,主要由 isa、 impl和 descriptor組成。?

在LLVM實現中,開啟ARC時。block的 isa應該是 _NSConcreteGlobalBlock類型。因為使用clang命令并沒有開啟ARC,所以還是 _NSConreteStackBlock類型。?

impl是實際的函數指針。它指向 __main_block_func_0。其實, impl就是 invoke變量。?

descriptor 是描述當前這個block的附加信息,包括大小,需要capture和dispose的變量列表等。?

靜態Block實現?

新建一個名為 stackBlock.c的文件:

#include ?

int main()?

{?

int a = 100;?

void (^stackBlock)(void) = ^{?

printf("%d/n", a);?

};?

stackBlock();?

return 0;?

}?

使用clang命令:

struct __main_block_impl_0 {?

struct __block_impl impl;?

struct __main_block_desc_0* Desc;?

int a;?

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {?

impl.isa = &;_NSConcreteStackBlock;?

impl.Flags = flags;?

impl.FuncPtr = fp;?

Desc = desc;?

}?

};?

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {?

int a = __cself->a; // bound by copy?

printf("%d/n", a);?

}?

static struct __main_block_desc_0 {?

size_t reserved;?

size_t Block_size;?

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};?

int main()?

{?

int a = 100;?

void (*stackBlock)(void) = ((void (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA, a));?

((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);?

return 0;?

}?

本例中,

isa指向_NSConcreteStackBlock,這是分配在棧上的實例。?

__main_block_impl_0中增加了一個變量a, 在block中引用的變量a實際是在聲明block時,被復制到 __main_block_impl_0結構體中的那個變量a。所以,在block內部修改變量a的內容, 不會影響外部變量a。?

__main_block_impl_0增加了變量a, 所有結構體大小變了,該結構體被寫在 __main_block_desc_0中。?

修改上面的代碼,在變量前面添加__block關鍵字:

#include ?

int main()?

{?

__block int i = 100;?

void (^stackBlock)(void) = ^{?

printf("%d/n", i);?

i = 10;?

};?

stackBlock();?

return 0;?

}?

轉換代碼:

struct __Block_byref_i_0 {?

void *__isa;?

__Block_byref_i_0 *__forwarding;?

int __flags;?

int __size;?

int i;?

};?

struct __main_block_impl_0 {?

struct __block_impl impl;?

struct __main_block_desc_0* Desc;?

__Block_byref_i_0 *i; // by ref?

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {?

impl.isa = &;_NSConcreteStackBlock;?

impl.Flags = flags;?

impl.FuncPtr = fp;?

Desc = desc;?

}?

};?

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {?

__Block_byref_i_0 *i = __cself->i; // bound by ref?

printf("%d/n", (i->__forwarding->i));?

(i->__forwarding->i) = 10;?

}?

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&;dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}?

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}?

static struct __main_block_desc_0 {?

size_t reserved;?

size_t Block_size;?

void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);?

void (*dispose)(struct __main_block_impl_0*);?

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};?

int main()?

{?

__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&;i, 0, sizeof(__Block_byref_i_0), 100};?

void (*stackBlock)(void) = ((void (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA, (__Block_byref_i_0 *)&;i, 570425344));?

((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);?

return 0;?

}?

代碼可以看到:

源碼中添加了一個名為 __Block_byref_i_0的結構體,用來保存需要capture并且修改的變量i。?

在 __main_block_impl_0中引用的 __Block_byref_i_0的結構體指針,這樣就可以達到修改外部變量的作用。?

__Block_byref_i_0結構體中帶有isa,說明也是一個對象。?

我們需要負責 __Block_byref_i_0結構體相關的內存管理,所以 __main_block_desc_0中增加了copy和dispose函數指針,對于在調用前后修改相應的引用計數。?

堆Block實現?

NSConcreteMallocBlock類型的block通常不會再源碼中直接出現,默認當block被copy的時候,才會將block復制到堆中。以下是block被copy時的代碼(來自這里),在第8步,目標block類型被修改為_NSConcreteMallocBlock。

static void *_Block_copy_internal(const void *arg, const int flags) {?

struct Block_layout *aBlock;?

const bool wantsOne = (WANTS_ONE &; flags) == WANTS_ONE;?

// 1?

if (!arg) return NULL;?

// 2?

aBlock = (struct Block_layout *)arg;?

// 3?

if (aBlock->flags &; BLOCK_NEEDS_FREE) {?

// latches on high?

latching_incr_int(&;aBlock->flags);?

return aBlock;?

}?

// 4?

else if (aBlock->flags &; BLOCK_IS_GLOBAL) {?

return aBlock;?

}?

// 5?

struct Block_layout *result = malloc(aBlock->descriptor->size);?

if (!result) return (void *)0;?

// 6?

memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first?

// 7?

result->flags &;= ~(BLOCK_REFCOUNT_MASK); // XXX not needed?

result->flags |= BLOCK_NEEDS_FREE | 1;?

// 8?

result->isa = _NSConcreteMallocBlock;?

// 9?

if (result->flags &; BLOCK_HAS_COPY_DISPOSE) {?

(*aBlock->descriptor->copy)(result, aBlock); // do fixup?

}?

return result;?

}?

變量的復制?

對于block外部變量的引用,block默認是將其復制到其數據結構中來實現訪問的。?

對于__block修飾的外部變量引用,block是復制其引用地址來實現訪問的。?

另外,可以參考《招聘一個靠譜的iOS (下)13、14題》。

ARC,MRC 對Block類型的影響?

在ARC開啟的情況下,將只會有NSConcreteGlobalBlock和NSConcreteMallocBlock類型的block。原本NSConreteStackBlock被NSConcreteMallocBlock類型替代。

在Block中,如果只使用全局或靜態變量或者不是用外部變量,那么Block塊的代碼會存儲在全局區。

在ARC中

如果使用外部變量,Block塊的代碼會存儲在堆區。?

在MRC中

如果使用外部變量,Block塊的代碼會存儲在棧區。?

Block默認情況下不能修改外部變量,只能讀取外部變量。

在ARC中

外部變量在堆中,這個變量在Block塊內與在Block塊外地址相同。?

外部變量在棧中,這個變量會被copy到為Block代碼塊所分配的堆中。?

在MRC中

外部變量在堆中,這個變量在Block塊內與在Block塊外地址相同。?

外部變量在棧中,這個變量會被copy到為Block代碼塊所分配的棧中。?

如果需要修改外部變量,需要在外部變量前聲明__block。

在ARC中

外部變量存在堆中,這個變量在Block塊內與Block塊外地址相同。?

外部變量存在棧中,這個變量會被轉移到堆中,不是復制。?

在MRC中

外部變量存在堆中,這個變量在Block塊內與Block塊外地址相同。?

外部變量存在棧中,這個變量在Block塊內與Block塊外地址相同。?

關于Block的面試題?

使用block時什么情況會發生引用循環,如何解決??

在block內如何修改block外部變量??

使用系統的某些block api(如UIView的block版本寫動畫時),是否也考慮引用循環問題??

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容