iOS探索:Block解析淺談
什麼是Block
- Block是將函式及其執行上下文封裝起來的物件
接下來讓我們通過原始碼來看一看Block的本質
- 我們在一個方法中寫了三行程式碼,第一行是定義了一個區域性變數,第二行是一個Block,第三行是這個Block的呼叫
這裡我們通過一個clang的編譯命令clang -rewrite-objc xxx.m來看一下原始碼的實現
-
我們的那段程式碼通過編譯器編寫後,首先第一行I代表的是一個例項方法後面的是物件和方法名,傳了兩個引數一個是self,一個是選擇器因子
-
然後我們方法中的第一行程式碼在編譯後沒有發生改變,我們著重看一下Block方法編譯後的改變
-
首先我們可以看到__BlockOneObj__testMethod_block_impl_0這樣一個結構體,在這個結構體中傳遞了幾個引數,第一個引數(void*)__BlockOneObj__testMethod_block_func_0我們通過名字可以知道這是一個無型別的函式指標,第二個引數&__BlockOneObj__testMethod_block_desc_0_DATA是一個Block相關描述的結構體然後取地址符,第三個引數muIntNum就是我們定義的區域性變數。最後取這個結構體地址強制轉換賦值給我們定義的這個Block
然後我們來看看__BlockOneObj__testMethod_block_impl_0這個結構體中有什麼具體操作,如下圖
其中第一個結構體裡面又是什麼資料結構呢,請看下圖
在我們上面介紹的結構體下面還有一個函式,具體解釋請看下圖
那麼什麼是Block的呼叫呢
Block的呼叫其實就是函式的呼叫,從原始碼中我們可以看出來
-
首先先對這個Block進行一個強制型別轉換(__block_impl *)Block
-
之後又取出它之中的成員變數FuncPtr(函式指標),找到我們上面解析的結構體和函式,在其中拿到對應的函式呼叫,然後把其中的引數傳遞進去,一個引數是我們這個Block本身,一個是我們傳遞的2,然後就回去呼叫__BlockOneObj__testMethod_block_func_0函式,最終進行呼叫
Block截獲變數
首先我們先來看一段程式碼
- (void)testMethod {
int muIntNum = 6;
int(^Block)(int) = ^int(int num){
return num *muIntNum;
};
muIntNum = 4;
Block(2);
}
複製程式碼
這段程式碼執行完Block(2)返回的值是多少呢?-------答案是12 接下來我們看一下為什麼是12以及Block截獲變數的本質是什麼
-
對於基本資料型別的區域性變數截獲其值
-
對於物件型別的區域性變數連同其所有權修飾符一起截獲
-
對於區域性靜態變數是以指標形式去截獲
-
對於全域性變數和靜態全域性變數不截獲
下面直接上程式碼
#import "BlockTwoObj.h"
//全域性變數
int global_var = 4;
//全域性靜態變數
static int static_global_var = 5;
@implementation BlockTwoObj
- (void)testMethodTwo {
//基本資料型別的區域性變數
int var = 1;
//物件型別的的區域性變數
__unsafe_unretained id unsafe_obj = nil;
__strong id strong_obj = nil;
//區域性靜態變數
static int static_var = 3;
void(^Block)(void) = ^{
NSLog(@"基本資料型別區域性變數:%d", var);
NSLog(@"物件型別區域性變數(__unsafe_unretained修飾):%@", unsafe_obj);
NSLog(@"物件型別區域性變數(__strong修飾):%@", strong_obj);
NSLog(@"區域性靜態變數:%d", static_var);
NSLog(@"全域性變數:%d", global_var);
NSLog(@"全域性靜態變數:%d", static_global_var);
};
Block();
}
@end
複製程式碼
接下來我們通過clang命令clang -rewrite-objc -fobjc-arc xxx.m來看一下原始碼
- 在這張圖中可以很清晰的看到Block中的變數截獲,其中需要注意的是對於區域性的靜態變數截獲的是指標,也就是說如果後面這個區域性靜態變數發生了修改,那麼Block中使用的是最新的值
__block修飾符
我們在什麼情況下使用__block修飾符呢? 一般情況下,對被截獲變數進行賦值操作需要新增__block修飾符,這裡需要注意的是賦值不等於是使用,切記!!!
例如在下面的程式碼中是否需要__block修飾符來修飾
NSMutableArray *muArr = [[NSMutableArray alloc] init];
void(^Block)(void) = ^{
//這裡只是做了新增操作,並非賦值,所以不需要用__block進行修飾
[muArr addObject:@"111"];
};
Block();
複製程式碼
那麼在下面的程式碼段當中呢?
__block NSMutableArray *muArrOther = nil;
void(^BlockOther)(void) = ^{
//這裡做了賦值操作,所以需要用__block進行修飾,否則會出現編譯報錯
muArrOther = [NSMutableArray array];
};
BlockOther();
複製程式碼
對變數進行賦值時
-
需要__block修飾符修飾的是區域性變數(包括基本資料型別和物件型別)
-
不需要__block修飾符修飾的是靜態區域性變數、全域性變數和靜態全域性變數,因為對於全域性變數和靜態全域性變數不涉及到變數的截獲,而對於靜態區域性變數呢,是通過使用指標來操作對應的變數的,所以也不需要修飾
下面請看一段程式碼,還是我們上面的那個例子
- (void)testMethod {
__block int muIntNum = 6;
int(^Block)(int) = ^int(int num){
return num *muIntNum;
};
muIntNum = 4;
Block(2);
}
複製程式碼
此時Block返回的是8,這裡是為什麼呢,我們只是用了__block來修飾
- 因為在這裡會發生一個非常奇妙的變化,__block修飾的變數變成了物件
請看下面的流程圖
-
首先__block int muIntNum會被轉化成第一個這樣一個結構體,其中具有isa指標,我們也可以理解成一個物件
-
從這個角度來看muIntNum經過編譯後就會變成一個物件,通過__forwarding指標去找到對應的物件,然後進行賦值
-
剛才我們看到的程式碼段是在棧上,在__block變數中有一個__forwarding指標,而這個指標指向的是自己,這裡要注意的是前提是在棧上,如果在堆上,這個__forwarding指標指向的就不是自己了,在下面會講到
-
所以在棧上我們修改這個變數的值,就會通過__forwarding指標找到自己本省去修改這個變數的值
那麼這裡有一個問題就是我們在棧上這個__forwarding指向的是自己到底有什麼用呢?我們完全可以通過訪問成員變數來修改,為什麼還需要這個指標呢,請繼續往下看
Block的記憶體管理
Block有三種類型
-
_NSConcreteGlobalBlock 全域性Block
-
_NSConcreteStackBlock 棧Block
-
_NSConcreteMallocBlock 堆Block
Block的Copy操作
-
當我們棧上的Block通過copy在堆上產生一個一樣的Block,有相同的Block和__block變數,當變數作用於結束後,棧上的Block物件就會被銷燬,而堆上的block依舊存在,所有如果棧上Block不用copy拷貝到堆上,在作用於銷燬後會因為找不到Block物件而崩潰
-
當然我們在這裡有一個問題,假如說在MRC環境下,如果在棧上進行了copy操作,會不會產生記憶體洩漏,答案是肯定的,相當於一個物件alloc出來,但是並沒有對應的relese操作一樣
- 當我們棧上的Block經過copy操作後,在堆上會產生一個一樣的Block,在棧中的Block中的__forwarding指標指向的事堆上Block的__block變數,並且在堆上Block的__forwarding指標也是指向的它自己的__block變數
參考書籍
Objective - C 高階程式設計:iOS與OS X多執行緒和記憶體管理