前言
要探究block前先說一下我對block的了解,我把它了解為;能夠捕獲他所有函數内部的變量的函數指針,匿名函數或者閉包。
block的使用
iOS4.0開始進入block特性。也叫做閉包。是一個函數(或指向函數的指針),再加上該函數執行的外部的上下文變量(有時候也稱作自由變量)。
-
block的聲明:
void (^blockName)(int arg1, int arg2);
中文翻譯:傳回值(^block變量名)(block的參數)
-
block的定義:
^void(int arg1, int arg2) {};
中文翻譯:^傳回類型(block的參數)
-
block隻有在調用的時候才能執行裡面的函數内容
- (void)viewDidLoad {
void (^blockName)(int, int) = ^(int arg1, int arg2) {
NSLog(@"arg1 + arg2 = %d", arg1 + arg2);
};
blockName(1,2);
//2、沒有參數
void (^blockName2)() = ^() {
NSLog(@"block2");
};
blockName2();
//3、block有傳回值
int (^blockName3)(int) = ^(int n) {
return n * 2;
};
//4、block作為方法的參數
[self testBlock2:10];
}
- (void)testBlock2:(int(^)(int))myBlock {
NSLog(@"調用了")
}
block調用外部變量
block隻能讀取,不能修改局部變量。這個時候是值傳遞。
如果想修改局部變量,要用__block來修飾。這個時候是引用傳遞。下面會聊下block的實作原理。
//1、調用局部變量,不用__block
NSInteger testNum2 = 10;
void (^block2)() = ^() {
//testNum = 1000; 這樣是編譯不通過的
NSLog(@"修改局部變量: %ld", testNum2); //列印:10
};
testNum2 = 20;
block2();
NSLog(@"testNum最後的值: %ld", testNum2);//列印:20
//2、修改局部變量,要用__block
__block NSInteger testNum3 = 10;
void (^block3)() = ^() {
NSLog(@"讀取局部變量: %ld", testNum3); //列印:20
testNum3 = 1000;
NSLog(@"修改局部變量: %ld", testNum3); //列印:1000
};
testNum3 = 20;
block3();
testNum3 = 30;
NSLog(@"testNum最後的值: %ld", testNum3);//列印:30
block代碼分析
網上很多通過Clang(LLVM編譯器)将OC的代碼轉換成C++源碼,來進行分析的。但是這些轉換的代碼并不是block的源代碼,隻是用來了解用的過程代碼。
1、block不包含任何變量
建立一個testBlock.m檔案。檔案中代碼為:
執行clang指令:
clang -rewrite-objc testBlock.m
生成.cpp的核心代碼主要在.cpp檔案的底部,大家可以看下圖:
我加了比較詳細的注釋,具體的看圖檔就好。這裡重點強調下關鍵的東西:
- 1.1、其中block的結構體:
struct __block_impl
{
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
isa:isa指針,在Objective-C中,任何對象都有isa指針。block 有三種類型:
_NSConcreteGlobalBlock:全局的靜态 block,類似函數。如果block裡不擷取任何外部變量。或者的變量是全局作用域時,如成員變量、屬性等; 這個時候就是Global類型
_NSConcreteStackBlock:儲存在棧中的 block,棧都是由系統管理記憶體,當函數傳回時會被銷毀。__block類型的變量也同樣被銷毀。為了不被銷毀,block會将block和__block變量從棧拷貝到堆。
_NSConcreteMallocBlock:儲存在堆中的 block,堆記憶體可以由開發人員來控制。當引用計數為 0 時會被銷毀。
代碼執行的時候,block的isa有上面3中值。後面還會進行詳細的說明。
- 1.2、__main_block_func_0 是block要執行的函數:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("列印block函數");
}
- 1.3、__main_block_desc_0 是block的描述資訊 的結構體
- 1.4、block的類型。
在上圖中可以看到:
impl.isa = &_NSConcreteStackBlock;
這裡 impl.isa 的類型為_NSConcreteStackBlock,由于 clang 改寫的具體實作方式和 LLVM 不太一樣,是以這裡 isa 指向的還是
_NSConcreteStackBlock
。但在 LLVM 的實作中,開啟 ARC 時,block 應該是 _NSConcreteGlobalBlock 類型。
是以 block是什麼類型 在 clang代碼裡是看不出來的。
如果要檢視block的類型還是要通過Xcode進行列印:
列印的結果:
clangBlk = <__NSGlobalBlock__: 0x100054240>
列印block函數
上面block代碼,沒有擷取任何外部變量,應該是 _NSConcreteGlobalBlock類型的。該類型的block和函數一樣 存放在 代碼段 記憶體段。記憶體管理簡單。
2、block 通路 局部變量
建立testBlock2.m檔案,代碼如下:
通過clang指令生成 的核心代碼如下,和剛才clang的代碼 不同的地方 已經加了注釋:
- 2.1、可以看到 __main_block_impl_0 中添加了 一個int num的變量。在 __main_block_func_0中使用了該變量。
從這裡可以看出來 這裡是 值拷貝,不能修改,隻能通路。
- 2.2、用Xcode列印上面block代碼,得到的類型為:__NSMallocBlock。
在說_NSConcreteMallocBlock類型前,我們先說下_NSConcreteStackBlock類型。
_NSConcreteStackBlock類型的block存在棧區,當變量作用域結束的時候,這個block和block上的__block變量就會被銷毀。
這樣當block擷取了局部變量,在其他地方通路的時候就會崩潰。block通過copy來解決了這個問題,可以将block從棧拷貝到堆。這樣當棧上的作用域結束後,仍然可以通路block和block中的外部變量。
我們現在看下本文開頭的問題1:
為什麼局部變量muArray出了作用域 還能存在?
captureBlk為預設的__strong類型,當block被指派給__strong類型的對象或者block的成員變量時,編譯器會自動調用block的copy方法。
執行copy方法,block拷貝到堆上,mutArray變量指派給block的成員變量。是以列印的結果就為1,2,3。
如果把上面代碼中的mutArray改為weak類型,那麼列印的就都是0了。因為當出去作用域的時候,mutArray就已經被釋放了。
同時,因為NSMutableArray *mutArray 是引用類型,用clang指令執行後,發現:
struct __main_block_impl_0
{
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
id __strong mutArray;
.....
}
mutArray在block中是id類型,因為是指針 是以在block中mutArray是可以修改的,而int類型的不能修改。當然如果用__block也能修改int類型的外部變量,下面我們會詳說。
下面這個列印的結果是1,也是這個道理:
同時通路外部變量是block進行的值傳遞,是以列印的還是1,不是2。
- 2.3、什麼情況下block會進行copy操作。
用代碼顯示的調用copy操作:
[captureBlk2 copy];
在MRC下block定義的屬性都要加上copy,ARC的時候block定義copy或strong都是可以的,因為ARC下strong類型的block會自動完成copy的操作。
@property (nonatomic, strong) captureObjBlk2 captureBlk21;
當 block 作為函數傳回值傳回時。
當 block 被指派給 __strong id 類型的對象或 block 的成員變量時。
當 block 作為參數被傳入方法名帶有
usingBlock
的 Cocoa Framework 方法或 GCD 的 API 時。
3、__block在block中的作用。
建立testBlock3.m,代碼如下:
用clang生成的代碼如下,進行了詳細的注釋:
block通路的外部變量,在block中就是一個結構體:__Block_byref_num_0:
// 一、用于封裝 __block 修飾的外部變量
struct __Block_byref_num_0 {
void *__isa; // 對象指針
__Block_byref_num_0 *__forwarding; // 指向 拷貝到堆上的 指針
int __flags; // 标志位變量
int __size; // 結構體大小
int num; // 外部變量
};
其中 int num 為外部變量名。
__Block_byref_num_0 *__forwarding; 這個是指向自己堆上的指針,這個後面會詳細說明。
為了對__Block_byref_num_0結構體進行記憶體管理。新加了copy和dispose函數:
//四、對__Block_byref_num_0結構體進行記憶體管理。新加了copy和dispose函數。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
// _Block_object_assign 函數:當 block 從棧拷貝到堆時,調用此函數。
_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
}
// 當 block 從堆記憶體釋放時,調用此函數:__main_block_dispose_0
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
__main_block_impl_0 中增加了 __Block_byref_num_0類型的指針變量。是以__block的變量之是以可以修改 是因為 指針傳遞。是以block内部修改了值,外部也會改變:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_num_0 *num; // 二、__block int num 變成了 __Block_byref_num_0指針變量。也就是 __block的變量通過指針傳遞給block
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在block要執行的函數 __main_block_func_0中,我們通過__Block_byref_num_0的__forwarding指針來修改的 外部變量,即:(num->__forwarding->num) = 10;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_num_0 *num = __cself->num; // bound by ref
(num->__forwarding->num) = 10; //三、這裡修改的是__forwarding 指向的記憶體的值
printf("num = %d", (num->__forwarding->num));
}
這是為什麼呢?
我們先來看下文章開頭的第二個問題:
當外部的局部變量testNum3 改變後,block内的testNum3變量也變了。
在block中修改的testNum3值,在block外部testNum3也改變了。
我們看下剛才clang生成的main方法,上面有截圖:
類似的邏輯:
用__block修改後,testNum3變量轉換為__Block_byref_num_0 的結構體。
上面說過copy操作會将block從棧拷貝到堆上, 會把 testNum3轉成的__Block_byref_num_0 結構體 指派給block的變量。
同時 會把 __Block_byref_num_0 的結構體中的 __forwarding指針指向拷貝到堆上 結構體。
就是棧上和拷貝到堆上的 的__Block_byref_num_0都用__forwarding指向堆上的自己。
這樣在棧上修改 testNum3變量的時候,實際修改的是堆上值,是以block内外的值是互相影響。
本文中的所有代碼還有clang生成的.cpp檔案,都放到了github上。
參考部落格位址