iOS-block如何捕獲外部變數(基本資料型別變數)
Block如何捕獲外部變數一:基本資料型別
共有三種:auto變數 、static變數、全域性變數
這一篇,我們詳細講解Block捕獲外部變數的機制.我們把block捕獲外部基本資料型別變數的情況分為以下幾種,見下圖:
一:auto變數
auto變數:自動變數,離開作用域就會銷燬,一般我們建立的區域性變數都是auto變數,比如int age = 10
,系統會在預設在前面加上auto int age = 10
首先我們要搞清楚,什麼是捕獲,所謂捕獲外部變數,意思就是在block內部,建立一個變數來存放外部變數,這就叫做捕獲.先做一個小小的Test:
{int age = 10; void (^block)(void) = ^{ NSLog(@"age is %d",age); }; age = 20; block(); }
輸出的age
是10
定義一個age
變數,在block
內部訪問這個age
,在呼叫block
之前,修改這個age
值,那麼最後輸出的age
是多少呢?很簡單,輸出的age
還是10,相信很多人都知道結果,我們從底層來看一下為什麼會這樣.
上面程式碼通過Clang編譯器轉換後如下:
{ int age = 10;//定義block void (*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age ); age= 20; //呼叫block block->FuncPtr(block); return 0; }
我們看到在呼叫block的建構函式時,傳入了三個引數,分別是:__main_block_func_0
,&__main_block_desc_0_DATA
,age
,
我們找到block
的建構函式,看看內部如何處理這個age
:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; // 1: 定義了一個同名的age變數 //block建構函式 __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; Desc = desc; age = _age; //2 :C++的特殊語法,在建構函式內部會預設把_age賦值給age } };
通過檢視block
的內部結構看我們發現,block
內部建立了一個age
變數,並且在block
建構函式中,把傳遞進來的_age
賦值給了這個age
變數.我們看看呼叫block
時,他的底部取的是哪個age
:
//呼叫block的FuncPtr函式,把block當做引數傳遞進去 block->FuncPtr(block); //FuncPtr函式內部 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { //通過傳遞的block,找到block內部的age int age = __cself->age; // bound by copy //列印age NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_3089d7_mi_0,age); }
通過底層程式碼,我們看到,在呼叫block
時,block
會找到自己內部的age
變數, 然後列印數出,所以我們修改age = 20
, 並不會影響block
內部的age
值
二:static變數
我們把上面的程式碼稍作修改:
auto int age = 10; static int height = 20; void (^block)(void) = ^{ NSLog(@"age is %d, height is %d",age,height); }; age = 20; height = 30; block();
列印的結果是age is 10, height is 30
同樣轉換 C++ 程式碼,檢視底層:
{ auto int age = 10; static int height = 20; void (*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age, &height //傳遞指標 ); age = 20; height = 30; (block->FuncPtr(block); }
我們看到,在定義block
時,呼叫block
的建構函式,傳遞引數時,age
傳遞的是值,而height
傳遞的是指標,看看建構函式內部:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age;//定義 age 變數 int *height;//定義一個 指標變數,存放外部變數的指標 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
我們看到,在block
內部,定義了兩個變數age
,height
,不同的是,height
是一個指標指標變數
,用於存放外部變數的指標.我們再來看看執行block
程式碼塊的內部:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy int *height = __cself->height; // bound by copy // *height : 取出指標變數所指向的記憶體的值 NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_bf6cae_mi_0,age,(*height)); }
我們看到,對於age
是捕獲到內部,把外部age
的值存起來,而對於height
,是把外部變數的指標儲存起來,所以, 我們在修改height
時,會影響到block
內部的值
思考:為什麼會出現這兩種情況 ?(block捕獲外部基本資料型別變數: auto變數是值傳遞;static變數是指標傳遞)?
原因很簡單,因為auto
是自動變數,出了作用域後會自動銷燬的,如果我們保留他的指標,就會存在訪問野指標的情況
//定義block型別 void(^block)(void); void test(){ int age = 10; static int height = 20; //在block內部訪問 age , height block = ^{ NSLog(@"age is %d, height is %d",age,height); }; age = 20; height = 30; } //在main函式中呼叫 int main(int argc, const char * argv[]) { test(); //test呼叫後,age變數就會自動銷燬,如果block內部是保留age變數的指標,那麼我們在呼叫block()時,就出現訪問野指標 block(); }
三:全域性變數
全域性變數哪裡都可以訪問,所以block
內部是不會捕獲全域性變數的,直接訪問,這個很好理解,我們直接看程式碼:
全域性變數底層
為什麼全域性變數不需要捕獲?
因為全域性變數無論哪個函式都可以訪問,block
內部當然也可以正常訪問,所以根本無需捕獲
為什麼區域性變數就需要捕獲呢?
因為作用域的問題,我們在一個函式中定義變數,在block
內部訪問,本質上跨函式訪問,所以需要捕獲起來.
Test1:
我們在Person
類中寫一個test()
方法,在test()
方法中定義一個block
並訪問self
, 請問block
會不會捕獲self
.
@implementation Person - (void)test{ void(^block)(void) = ^{ NSLog(@"會不會捕獲self--%@",self); }; block(); } @end
答案是會捕獲self
,我們看看底層程式碼:
struct __Person__test_block_impl_0 { struct __block_impl impl; struct __Person__test_block_desc_0* Desc; Person *self; __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
很顯然block
內部的確聲明瞭一個Person *self
用於儲存self
,既然block
內部捕獲了self
,那就說明self
肯定是一個區域性變數.那問題就來了,
為什麼self
會是一個區域性變數?它應該是一個全域性變數呀?我們看一下轉換後的test()
方法:
static void _I_Person_test(Person * self, SEL _cmd) { void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }
我們 OC 中的test()
方法時沒有引數的,但是轉換成 C++ 後就多了兩個引數self
,_cmd
,其實我們每個 OC 方法都會預設有這兩個引數,這也是為什麼我們在每個方法中都能訪問self
和_cmd
,而引數就是區域性變數,所以block
就自然而然的捕獲了self
.
Test2:
我們對 Test1 稍加修改,增加一個name
屬性,然後在block
中訪問_name
,這時候block
會捕獲self
嗎?
答案是:會.繼續看一下底層:
block
底層仍然捕獲了self
,這是因為,我們去訪問_name
屬性的時候,實際上相當於self -> name
,要想獲取name
,必須要先獲取self
,因為name
是從self
中來的,所以block
內部會對self
進行捕獲.
總結:
一:只要是區域性變數,不管是auto 變數
,還是static 變數
,block
都會捕獲. 不同的是,對於auto 變數
,block
是儲存值,而static 變數
是儲存的指標.
二:如果是全域性變數,根本不需要捕獲,直接訪問
本篇只是講解了block
捕獲基本資料型別的auto
變數,下一篇會講解block捕獲物件型別的auto變數