探索iOS中Block的實現原理

STzen發表於2019-04-19

Block作為Objective-C中閉包的實現在iOS開發中佔有非常重要的地位,尤其是作為回撥(callback)使用。這篇文章主要記錄Block的實現,關於Block的語法可以參考這裡:How Do I Declare A Block in Objective-C

Block的實質

Block被稱為帶有自動變數(區域性變數)的匿名函式,Block語法去和C語言的函式非常相似。實際上Block的底層就是作為C語言原始碼來處理的,支援Block的編譯器會將含有Block語法的原始碼轉換為C語言編譯器能處理的原始碼,當作C語言原始碼來編譯。

通過LLVM編譯器clang可以將含有Block的語法轉換為C++原始碼:

clang -rewrite-objc fileName
複製程式碼

比如一段非常簡單的含有Block的程式碼:

#include <stdio.h>

int main() {
	void (^blk)(void) = ^{
		printf("Hello Block!\n");
	};

	blk();
	return 0;
}
複製程式碼

使用clang將其轉換為C++原始碼後,其核心內容如下:

struct __block_impl {
  	void *isa;
  	int Flags;
  	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;
  	}
};

// block中的方法
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  	printf("Hello Block!\n");
}

// block的資料描述
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() {
	// 呼叫__main_block_impl_0的建構函式
	void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); 

	// blk()呼叫
	((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

	return 0;
}
複製程式碼

這段原始碼主要包含了3個struct和兩個函式,實際上就是C語言原始碼:

  • struct __block_impl
  • struct __main_block_impl_0
  • struct __main_block_desc_0
  • *static void __main_block_func_0(struct __main_block_impl_0 __cself)
  • int main()

很容易看出mian()函式就是最初程式碼中的mian函式,__main_block_func_0函式就是最初程式碼中的Block語法:

^{
	printf("Hello Block!\n");
};
複製程式碼

由此得出:

  • 通過Block使用的匿名函式實際上被當作簡單的C語言函式來處理
  • 根據Block語法所屬的函式名(此處為mian)和該Block語法在該函式出現的順序值(此處為0)來經clang給函式命名,即(__main_block_func_0)。
  • 函式的引數**__cself為指向Block值的變數**,就相當於Objective-C中的self。

接下來重點看看__main_block_impl_0結構體

__main_block_func_0函式的引數__cself型別宣告為struct __main_block_impl_0。__main_block_impl_0就是該Block的資料結構定義,其中包含了成員變數為impl和Desc指標,impl的__block_impl結構體宣告中包含了某些標誌、今後版本升級所需的區域以及函式指標

struct __block_impl {
  	void *isa; 
  	int Flags; // 某些標誌
  	int Reserved; // 今後版本升級所需的區域
  	void *FuncPtr; // 函式指標
};
複製程式碼

Desc指標的中包含了Block的大小

static struct __main_block_desc_0 {
  	size_t reserved; // 今後版本升級所需的區域
  	size_t Block_size; // Block的大小
};
複製程式碼

在__main_block_impl_0的建構函式中呼叫了impl和Desc的成員變數,這個建構函式在mian函式中被呼叫,為了便於閱讀,將其中的轉換去掉:

// 呼叫__main_block_impl_0的建構函式
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

// 去掉轉換之後
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_func_0 *blk = &tmp;
複製程式碼

建構函式中使用的實參為函式指標__main_block_func_0和靜態全域性變數初始化的__main_block_desc_0結構體例項指標__main_block_desc_0_DATA:

static struct __main_block_desc_0 __main_block_desc_0_DATA = { 
    0, 
    sizeof(struct __main_block_impl_0)
};
複製程式碼

通過這些呼叫可以總結出下面兩條:

  • 原始碼將__main_block_impl_0結構體型別的自動變數,即棧上生成的結構體例項指標,賦值給__main_block_impl_0結構體指標型別的變數blk
  • 原始碼使用Block,即__main_block_impl_0結構體例項的大小,進行初始化

將__main_block_impl_0結構體展開:

struct __main_block_impl_0 {
  	void *isa;
  	int Flags;
  	int Reserved;
  	void *FuncPtr;
  	struct __main_block_desc_0* Desc;
};
複製程式碼

在該程式中建構函式的初始化資料如下:

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
複製程式碼

可以看出FuncPtr = __main_block_func_0就是簡單的使用函式指標FuncPtr呼叫函式__main_block_func_0列印Hello Block!語句,這就是最初的原始碼中對於block呼叫的實現:

blk();
複製程式碼

其對應的原始碼去掉轉換之後就很清晰:

// blk()呼叫
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 去掉轉換之後:
(*blk->impl.FuncPtr)(blk);
複製程式碼

到此,對於Block的建立和使用就可以這樣理解:

  • 建立Block時,實際上就是宣告一個struct,並且初始化該struct的成員變數。
  • 執行Block時,就是通過函式指標呼叫函式。

在__main_block_impl_0的建構函式中有一個_NSConcreteStackBlock:

impl.isa = &_NSConcreteStackBlock;
複製程式碼

這個isa指標很容易想到Objective-C中的isa指標。在Objective-C類和物件中,每個物件都有一個isa指標,Objective中的類最終轉換為struct,類中的成員變數會被宣告為結構體成員,各類的結構體是基於objc_class結構體的class_t結構體。

typedef struct Objc_object {
	Class isa;
} *id;

typedef struct obje_class *Class;

struct objc_class {
	Class isa;
};

struct class_t {
	struct class_t *isa;
	strcut class_t *superclass;
	Cache cache;
	IMP *vtable;
	uintptr_t data_NEVER_USE;
};
複製程式碼

實際上Objective-C中由類生成物件就是像結構體這樣生成該類生成的物件的結構體例項。生成的各個物件(即由該生成的物件的各個結構體例項),通過成員變數isa保持該類的結構體例項指標。

比如一個具有成員變數valueA和valueB的TestObject類:

@interface TestObject : NSObject {
    int valueA;
    int valueB;
}
@end
複製程式碼

其類的物件的結構體如下:

struct TestObject{
	Class isa;
	int valueA;
	int valueB;
};
複製程式碼

探索iOS中Block的實現原理

**在Objective-C中,每個類(比如NSObject、NSMutableArray)均生成並保持各個類的class_t結構體例項。**該例項持有宣告的成員變數、方法名稱、方法的實現(即函式指標)、屬性以及父類的指標,並被Objective-C執行時庫所使用。

再看__main_block_impl_0結構體就相當於基於Objc_object的結構體的Objective-C類物件的結構體,其中的成員變數isa初始化為isa = &_NSConcreteStackBlock;_NSConcreteStackBlock就相當於calss_t結構體例項,在將Block作為Objective-C物件處理時,關於該類的資訊放置於_NSConcreteStackBlock中。

實際上Block的實質Block就是Objective-C物件

Block捕獲自動變數

Blocks如何捕獲自動變數

Block作為傳統回撥函式的替代方法的其中一個原因是:block允許訪問區域性變數,能捕獲所使用的變數的值,即儲存該自動變數的瞬間值,比如下面這段程式碼,Block中儲存了區域性變數mul的瞬間值7,所以後面對於mul的更改不影響Block中儲存的mul值:

int mul = 7;
int (^blk)(int) = ^(int num) {
    return mul * num;
};

// change mul
mul = 10;

int res = blk(3);
NSLog(@"res:%d", res); // res:21 not 30
複製程式碼

通過clang來看看Block捕獲自動變數之後Block的結構有什麼變化:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int mul; // Block語法表示式中使用的自動變數被當作成員變數追加到了__main_block_impl_0結構體中
  // 初始化結構體例項時,根據傳遞建構函式的引數對由自動變變數追加的成員變數進行初始化
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _mul, int flags=0) : mul(_mul) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp; 
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int mul = __cself->mul; // bound by copy

  printf("mul is:%d\n", mul);
}

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 mul = 7;
  void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mul));

  ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

 return 0;
}
複製程式碼

分析__main_block_impl_0和其構造方法可以發現:Block中使用的自動變數mul被當作成員變數追加到了__main_block_impl_0結構體中,並根據傳遞建構函式的引數對該成員變數進行初始化。__main_block_impl_0中結構體例項的初始化如下:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
mul = 7; // 追加的成員變數
複製程式碼

由此可見,**在__main_block_impl_0結構體例項(即Block)中,自動變數值被捕獲。**可以將Block捕獲自動變數總結為如下:Block在執行語法時,Block中所使用的自動變數值被儲存到Block的結構體例項(即Block自身)中。即向結構體__main_block_impl_0中追加成員變數。

__block說明符

雖然Block能捕獲自動變數值,但是卻不能對其進行修改,比如下面程式碼就會報錯:

int main() {
	int val = 10;
	void(^blk)(void) = ^ {
		val = 1; // error Variable is not assignable (missing __block type specifier)
	};
	blk();
	printf("val:%d\n", val);
	
	return 0;
}
複製程式碼

需對val變數使用__block說明符:

int main() {
	__block int val = 10;
	void(^blk)(void) = ^ {
		val = 1; 
	};
	blk();
	printf("val:%d\n", val); // val:1
	
	return 0;
}
複製程式碼

將其用clang轉換之後:

struct __Block_byref_val_0 {
  void *__isa;
  __Block_byref_val_0 *__forwarding;
  int __flags;
  int __size;
  int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref

  (val->__forwarding->val) = 1;
 }

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose((void*)src->val, 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() {
	// __block型別的變數居然變成了結構體 __block int val = 10;
 	__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
 		(void*)0,
 		(__Block_byref_val_0 *)&val, 
 		0,
 		sizeof(__Block_byref_val_0), 
 		10
 	};

 void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

 ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

 printf("val:%d\n", (val.__forwarding->val));

 return 0;
}
複製程式碼

增加了__block變數之後原始碼急劇增多,最明顯的是增加了一個結構體和4個函式:

  • struct __main_block_impl_0
  • static void __main_block_copy_0
  • static void __main_block_dispose_0
  • _Block_object_assign
  • _Block_object_dispose

首先比較一下使用__block和沒有使用__block的__main_block_func_0函式對變化

// 沒有使用__block
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int mul = __cself->mul; // bound by copy

  printf("mul is:%d\n", mul);
}

// 使用__block
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

  (val->__forwarding->val) = 1;
 }
複製程式碼

可以看出在沒有使用__block時,Block僅僅是捕獲自動變數的值,即int mul = __cself->mul;

再看剛才的原始碼,使用__block變數的val居然變成了結構體例項

// __block int val = 10; 轉換之後的原始碼:
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
 	0,
 	&val, 
    0,
 	sizeof(__Block_byref_val_0), 
 	10
 };
複製程式碼

__block變數也同Block一樣變成了__Block_byref_val_0結構體型別的自動變數(棧上生成的__Block_byref_val_0結構體例項),該變數初始化為10,且這個值也出現在結構體例項的初始化中,表示該結構體持有相當於原有自動變數的成員變數(下面__Block_byref_val_0結構體中的成員變數val就是相當於原自動變數的成員變數):

struct __Block_byref_val_0 {
  void *__isa;
  __Block_byref_val_0 *__forwarding;
  int __flags;
  int __size;
  int val; // 相當於原自動變數的成員變數
};
複製程式碼

回過頭去看Block給val變數賦值的程式碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

  (val->__forwarding->val) = 1;
 }
複製程式碼

得出:__Block_byref_val_0結構體例項的成員變數__forwarding持有指向例項自身的指標。通過成員變數__forwarding訪問成員變數val。

探索iOS中Block的實現原理

這裡沒有將__block變數的__Block_byref_val_0結構體直接寫在Block的__main_block_impl_0結構體中是為了能在多個Block中使用同一個__block變數。 比如在兩個Block中使用同一個__block變數:

__block int val = 10;
void (^blk1)(void) = ^ {
	val = 1; 
};

void (^blk2)(void) = ^ {
	val = 2;
};
複製程式碼

轉換之後:

__Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(    __Block_byref_val_0), 10};

blk1 = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 570425344);

blk2 = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &val, 570425344);
複製程式碼

雖然到這裡已經大致知道為什麼Block能捕獲自動變數了,但是這裡還遺留幾個問題:

  1. __Block_byref_val_0中為什麼需要成員變數__forwarding?
  2. __main_block_copy_0和__main_block_dispose_0函式的作用是什麼?

Block的型別

綜上可知:

  • Block轉換為Block結構體型別的自動變數
  • __block變數轉換為__block變數的結構體型別的自動變數

結構體型別的自動變數即棧上說生成的該結構體的例項

既然Block是Objective-C物件,那麼它具體是哪種物件?在Block中的isa指標指向的就是該Block的Class,目前所見都是_NSConcreteStackBlock型別,而在block的runtime中實際定義了6中型別的Block,其中我們主要接觸到的是這三種:

  • _NSConcreteStackBlock:建立在棧上的Block
  • _NSConcreteGlobalBlock:作為全域性變數的Block
  • _NSConcreteMallocBlock:堆上建立的Block

它們對應在程式中的記憶體分配:

探索iOS中Block的實現原理

那麼Block在什麼情況下時在堆上的?什麼時候時棧上的?什麼時候又是全域性的?

_NSConcreteGlobalBlock

_NSConcreteGlobalBlock很好理解,將Block當作全域性變數使用的時候,生成的Block就是_NSConcreteGlobalBlock類物件。比如:

#include <stdio.h>

void (^blk)(void) = ^{
	printf("Gloabl Block\n");
};

int main() {
	return 0;
}
複製程式碼

用clang轉換之後為該Block用結構體__block_impl的成員變數初始化為_NSConcreteGlobalBlock,即Block用結構體例項設定在程式記憶體的資料區:

isa = &_NSConcreteGlobalBlock;
複製程式碼

將全域性Block存放在資料區的原為:**使用全域性變數的地方不能使用自動變數,所以不存在對自動變數的捕獲。因此Block用結構體例項的內容不依賴於執行時的狀態,所以整個程式中只需要一個例項。**只有在捕獲自動變數時,Block用結構體例項捕獲的值才會根據執行時的狀態變化。因此總結Block為_NSConcreteGlobalBlock類物件的情況如下:

  • Block當作全域性變數使用時
  • Block語法表示式中不使用應捕獲的自動變數時

_NSConcreteStackBlock

除了上述兩中情況下Block配置在程式的資料區中以外,Block語法生成的Block為_NSConcreteStackBlock類物件,且設定在棧上。 配置在棧上的Block,如果其所屬的變數作用域結束,該Block就被自動廢棄。

_NSConcreteMallocBlock

那麼配置在堆上的_NSConcreteMallocBlock類在何時使用?

配置在全域性變數上的Block,從變數作用域外也可以通過指標訪問。但是設定在棧上的Block,如果其所屬的作用域結束,該Block就被廢棄;並且__block變數的也是配置在棧上的,如果其所屬的變數作用域結束,則該__block變數也會被廢棄。那麼這時需要將Block和__block變數複製到堆上,才能讓其不受變數域作用結束的影響。

Block提供了將Block和__block變數從棧上覆制到堆上的方法。複製到堆上的Block將_NSConcreteMallocBlock類物件寫入Block用結構體例項的成員變數isa:

isa = &_NSConcreteMallocBlock;
複製程式碼

對於堆上的__block的訪問,就是通過__forwarding實現的:**__block變數用結構體成員變數__forwarding實現無論__block變數配置在棧還是在堆上都能正確的訪問__block變數。**當__block變數配置在堆上時,只要棧上的結構體成員變數__forwarding指向堆上的結構體例項,那麼不管是從棧上還是從堆上的__block變數都能正確訪問。

探索iOS中Block的實現原理

並且在ARC時期,大多數情況下編譯器知道在合適自動將Block從棧上覆制到堆上,比如將Block作為返回值時。而當向方法或函式的引數中傳遞Block時,編譯器不能判斷,需要手動呼叫copy方法將棧上的Block複製到堆上,但是apple提供的一些方法已經在內部恰當的地方複製了傳遞過來的引數,這種情況就不需要再手動複製:

  • Cocoa框架中的方法且方法名中含有usingBlock等時
  • CGD的API

並且,不管Block配置在何存,用copy方法複製都不會出現問題。但是將Block從棧上覆制到堆上時相當消耗CPU的。對於已經在堆上的Block呼叫copy方法,會增加其引用計數。

並且對使用__block變數的Block從棧複製到堆上時,__block變數也會收到影響:如果在1個Block中使用__block變數,當該Block從棧複製到堆時,這些__block變數也全部被從棧複製到堆上。並且此時Block持有__block變數。如果有個Block使用__block變數,在任何一個Block從棧複製到堆時,__block變數都會一併複製到堆上並被該Block持有;當剩下的Block從棧複製到堆時,被複制的Block持有__block變數,並增加其引用計數。如果配置在堆上的Block被廢棄,它說使用的__block變數也就被釋放。這種思考方式同Objective-C記憶體管理方式相同。即使用__block變數的Block持有該__block變數,當Block被廢棄時,它所持有的__block變數也被廢棄

Block捕獲物件

在這裡Block捕獲的是__block型別的變數val,如果捕獲的是Objective-C物件會有什麼區別?

int main() {
    id arr = [[NSMutableArray alloc] init]; 
    void (^blk)(id) = [^(id obj) {
    	[arr addObject:obj];
	NSLog(@"arr count: %ld", [arr count]);
    } copy];

    blk(@"Objective-C");
    blk(@"Switf");
    blk(@"C++");

    return 0;
}
複製程式碼

值得注意的是:Block捕獲的是objective-C物件,並且呼叫變更該物件的方法addObject:,所以這裡不會產生編譯錯誤。這是因為block捕獲的變數值是一個NSMutableArray類的物件,用C語言描述就是捕獲NSMutableArray類物件用的結構體例項指標。addObject方法是使用block截獲的自動變數arr的值,所以不會有任何問題,但是如果在Block內部去給捕獲的arr物件賦值就會出錯:

int main() {
    id arr = [[NSMutableArray alloc] init]; 
    void (^blk)(id) = [^(id obj) {
        arr = [NSMutableArray arrayWithObjects:obj, nil]; // error Variable is not assignable (missing __block type specifier)
        
        NSLog(@"arr count: %ld", [arr count]);
    } copy];

    blk(@"Objective-C");
    return 0;
}

複製程式碼

之前的程式碼轉換之後的部分原始碼為:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
	_Block_object_assign((void*)&dst->arr, (void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose((void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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

再回頭看看之前__block int val = 10;轉換之後的原始碼中的部分內容:

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose((void*)src->val, 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*);
}
複製程式碼

OBjective-C物件和__block變數對比,發現在Block用的結構體部分基本相同,不同之處在於:Objective-C物件用BLOCK_FIELD_IS_OBJECT標識,__block變數是用BLOCK_FIELD_IS_BYREF標識。即通過BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF引數區分copy函式和dispose函式的物件型別是物件還是__block變數。

該原始碼中在__main_block_desc_0 結構體中增加了成員變數copy和dispose,以及作為指標賦值給該成員變數的__main_block_copy_0函式和__main_block_dispose_0函式,這兩個函式的作用:

  • __main_block_copy_0函式中所使用的_Block_object_assign函式將物件型別物件複製給Block用結構體的成員變數arr並持有該物件,呼叫_Block_object_assign函式相當於retain函式,將物件賦值在物件型別的結構體成員變數中

  • __main_block_dispose_0函式中使用_Block_object_dispose函式釋放賦值在Block用結構體成員變數arr中的物件。呼叫_Block_object_dispose函式相當於呼叫release函式,釋放賦值在物件型別結構體中的物件。

這兩個函式在Block從棧複製到堆和已經堆上的Block被廢棄時呼叫:

  • Block棧上覆制到堆上會呼叫copy函式
  • 堆上的Block被廢棄時會呼叫dispose函式

Block使用注意事項

當Block從棧複製到堆上時,Block會持有捕獲的物件,這樣就容易產生迴圈引用。比如在self中引用了Block,Block優捕獲了self,就會引起迴圈引用,編譯器通常能檢測出這種迴圈引用:

@interface TestObject : NSObject
@property(nonatomic, copy) void (^blk)(void);
@end

@implementation TestObject
- (instancetype)init {
    self = [super init];
    if (self) {
        self.blk = ^{
            NSLog(@"%@", self); // warning:Capturing 'self' strongly in this block is likely to lead to a retain cycle
        };
    }
    return self;
}
複製程式碼

同樣,如果捕獲到的是當前物件的成員變數物件,同樣也會造成對self的引用,比如下面的程式碼,Block使用了self物件的的成員變數name,實際上就是捕獲了self,對於編譯器來說name只不過時物件用結構體的成員變數:

@interface TestObject : NSObject
@property(nonatomic, copy) void (^blk)(void);
@property(nonatomic, copy) NSString *name;
@end

@implementation TestObject
- (instancetype)init {
    self = [super init];
    if (self) {
        self.blk = ^{
            NSLog(@"%@", self.name);
        };
    }
    return self;
}
@end
複製程式碼

解決迴圈引用的方法有兩種:

  1. 使用__weak來宣告self

    - (instancetype)init {
        self = [super init];
        if (self) {
            __weak typeof(self) weakSelf = self;
            self.blk = ^{
                NSLog(@"%@", weakSelf.name);
            };
        }
        return self;
    }
    複製程式碼
  2. 使用臨時變數來避免引用self

    - (instancetype)init {
        self = [super init];
        if (self) {
            id tmp = self.name;
            self.blk = ^{
                NSLog(@"%@", tmp);
            };
        }
        return self;
    }
    複製程式碼

使用__weak修飾符修飾物件之後,在Block中對物件就是弱引用:

探索iOS中Block的實現原理

參考

相關文章