block 的概念
這篇文章我打算來深究一下 OC 中的 block 到底是何方神圣。后面會介紹用可愛的 clang
指令來看看 block 底層的實現。
塊對象 (block Object)是在 mac OS 10.6 及 iOS 4.0 平臺下可以使用的功能,他不是 OC 而是 C 語言的功能實現。蘋果公司的文檔中將其稱為塊對象或 Block,在其他編程語言中,他與閉包(closure)的功能基本相同。
從 C 語言 block 說起
先從一個 C 函數說起
#include <stdio.h>
void myfunc(int m, void (^b)(void)) {
printf("%d: ", m);
b();
}
int global = 1000; // 外部變量(全局靜態變量)
int main(int argc, const char * argv[]) {
void (^block)(void);
static int s = 20; // 局部靜態變量
int a = 20; // 自動變量(局部變量)
block = ^{ // ============ 1
printf("%d, %d, %d\n", global, s, a);
};
myfunc(1, block);
s = 0;
a = 0;
global = 5000;
myfunc(2, block);
block = ^{ // ============ 2
printf("%d, %d, %d\n", global, s, a);
};
myfunc(3, block);
return 0;
}
仔細讀代碼,想想輸出結果是什么
輸出結果是
1: 1000, 20, 20
2: 5000, 0, 20
3: 5000, 0, 0
上面結果中,第一行沒有問題,第二行是為什么呢?可以發現,變量 global 和 s 的值都改變了,但是局部變量 a 的值沒有改變。第三行顯示的是在代碼 2 處代入塊對象后的變量值,此處的變量 a 的值已經改變了。
綜上,塊對象貌似只在塊句法中保存自動變量的值。(我們所說的自動變量其實就是函數內的局部變量,通常不用 static 關鍵字修飾)
塊對象就是把可以執行的代碼和代碼中可訪問的變量封裝起來,使得之后可以進一步處理的包。
綜上,總結一下
block 內部可以直接訪問全局變量(外部變量)和靜態變量,也可以直接改變其值
-
但是對于局部變量,塊句法會將其從 棧區 copy 一份到 堆區,所以即使最初的變量發生了變化,塊內部在使用的時候也不知道。而且變量的值只可以被讀取不能被改變。自動變量在運行時就相當于 const 修飾的變量。
image
可以通過 __block
來完成在 block 內部對局部變量的修改。
注意:
__block 變量不是靜態變量,它在塊句法每次執行塊句法時獲取變量的內存區域。也就是說,__block 變量在同一個變量作用域中被多個 塊對象 訪問的時候,其實訪問的是同一塊內存區域。
OC 中 block 的注意點解析
塊句法中使用其他任意實例對象
前面已經講了塊句法中有外部變量或自動變量時這些變量的行為,現在我們來介紹一下塊句法內使用對象時的行為,特別是引用計數器的處理。
void (^cp)(void); // 可以保存塊的靜態變量
- (void)someMethod {
id obj = ...; // 引用任意實例對象
int n = 10;
void (^block)(void) = ^{
[obj calc: n];
};
// ...
cp = [block copy];
}
如上代碼,塊對象在棧上生成,變量 obj 引用任何實例變量時,塊對象內使用的變量 obj 也會訪問同一個對象,這時實例變量的引用計數不會發生改變。接著塊對象復制到堆區,實例對象的引用計數加 1,由于方法執行結束后自動變量 obj 也會消失,因此這時塊對象就成為了所有者。注意實例對象是被共享的,不是復制的。所以不只是從塊對象,從哪里都可以發送消息。
塊句法中使用同一類的實例變量
先上代碼
void (^cp)(void); // 可以保存塊的靜態變量
- (void)someMethod {
int n = 10;
void (^block)(void) = ^{
[ivar calc: n]; // 注:ivar 為該類實例變量
};
// ...
cp = [block copy];
}
這種情況下,當對象唄復制時,self 的引用計數會加 1,而非 ivar。注意,塊句法中的實例變量為整數或實數時也是一樣的(這點容易搞錯)。
綜上總結
- 方法定義內的塊句法中存在實例變量時,可以直接訪問實例變量,也可以修改其值。(因為是指向同一塊內存區域)
- 方法定義內的塊句法中存在實例變量時,如果被 copy 到堆區,self 引用計數會加 1。實例變量不一定是對象。
- 塊句法中存在非實例變量的實例對象時,被 copy 后,這個對象的引用計數會加 1。
- 已經復制后,堆區中某個塊對象即使再次收到 copy 方法,結果也只是塊對象自身的引用計數 1。包含的對象的引用計數不變。
- 復制的塊對象在被釋放時,也會向包含的對象發送 release。
OC 中的 block 到底是什么呢?
本著刨根問底的精神,就來一探究竟,block 到底是何方神圣。
我們創建一個純凈的 Command Line Tool
項目,在 main.m
中書寫一下簡單的代碼:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)() = ^{
NSLog(@"======>%d", age);
};
age = 20;
block();
}
return 0;
}
然后打開終端,cd 該目錄下,鍵入
ZK$ clang -rewrite-objc main.m
然后在該路徑下生成 main.cpp 文件,打開后驚奇發現短短幾句 OC 代碼,竟然生成了 九萬多行 C++ 代碼,別怕,我們寫的核心 block 代碼其實也沒多少行。拉到最下面,就是我們重寫出來的 block C++ 代碼,為了閱讀方便,我對這些代碼進行了稍微處理,比如去掉類型強轉等干擾性代碼,就得到了下面這一片精美的 C++ 代碼,我還貼心地加了一些注釋。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 下面這些代碼值這個結構體的構造函數
// `int flags=0` 是默認值
// `: age(_age)` C++ 語法,將 _age 傳給 age 屬性,可知在沒有 __block 情況下,從外部傳進來的 age 直接就賦值給這個結構體的 age。所以相當于寫死了,不能修改。外部改變了也無法獲知。
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; // block 生成的函數被保存在這個屬性中
Desc = desc;
}
};
// 下面這個函數就是 block 最終生成的一個函數體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_0f9b1b_mi_0, age);
}
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 argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
// 從下面這句代碼得知,block 就是指向一個結構體的指針。
// 參1:block 生成的函數
// 參2:`__main_block_desc_0_DATA` 結構體的指針
// 參3:將上面的自動變量直接傳遞進去
void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age)); // 在這里是直接將 10 傳遞進去
age = 20; // 該處的 age 賦值在 block 里面根本無法感知
// 調用 block,`(block)->FuncPtr` 就是 `__main_block_impl_0` 函數
((block)->FuncPtr)(block);
}
return 0;
}
必要的說明已經在上面代碼的注釋中說的很明白,我來總結一下,定義 block 的時候,首先會生成一個結構體 __main_block_impl_0
,他有三個參數,參1是 block 生成的函數__main_block_func_0
,參2是結構體 __main_block_desc_0_DATA
的地址。參3 就是我們直接傳遞進去的自動變量。三個參數傳遞進去 __main_block_impl_0
后會直接出發其構造函數,上面注釋說明很明確。
那么,目光轉回 __main_block_func_0
函數,int age = __cself->age;
這句代碼是將 age 屬性直接取出來,而這個 age 就是我們剛一開始上面提到的參3傳遞進去的自動變量的值 10,固然打印出來的是 10,不是 20。
還不過癮?那么我們 __block
修飾一下自動變量,看看有什么神奇的地方
注意啦,OC 代碼改成如下
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
void (^block)() = ^{
NSLog(@"======>%d", age);
};
age = 20;
block();
}
return 0;
}
運行 clang
指令,讓我們看看有哪些變化。
// 這個結構體用來修飾 __block 的自動變量,竟然發現了我們熟悉的老面孔 `isa`!說明他也是一個對象。
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_cff06d_mi_0, (age->__forwarding->age));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 變化1:age 不是用 int 修飾了,而是增加一個名為 `__Block_byref_age_0` 的結構體,詳見上面這個結構體的定義有注釋。
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {0, &age, 0, sizeof(__Block_byref_age_0), 10};
// 變化2:注意,下面的參3的 age 多了個 `&` 符號取地址,說明 `__main_block_impl_0` 引用的是結構體 `__Block_byref_age_0`的指針,不向之前直接將自動變量的值傳遞進去了,這也就是為什么 定義 block 后外部自動變量修改了,block 內部依然可以讀到最新值。同時,這樣我們也可以在 block 內部修改外部自動變量的值。
void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age, 570425344));
(age.__forwarding->age) = 20;
((block)->FuncPtr)(block);
}
return 0;
}
上面的主要變化已經在注釋說明了,我再總結一下重要的變化:
- 變化1:age 不是用 int 修飾了,而是增加一個名為
__Block_byref_age_0
的結構體,這個結構體用來修飾 __block 的自動變量,竟然發現了我們熟悉的老面孔isa
!說明他也是一個對象。 - 變化2:
__main_block_impl_0
的參3的 age 多了個&
符號取地址,說明__main_block_impl_0
引用的是結構體__Block_byref_age_0
的指針,不向之前直接將自動變量的值傳遞進去了,這也就是為什么 定義 block 后外部自動變量修改了,block 內部依然可以讀到最新值。同時,這樣我們也可以在 block 內部修改外部自動變量的值。 - 變化3:還有像添加了
__main_block_copy_0
,__main_block_dispose_0
結構體等變化