block本質探尋二之變數捕獲
一、程式碼
說明:本文章須結合文章《block本質探尋一之記憶體結構》和《class和object_getClass方法區別》加以理解;
//main.m
#import <Foundation/Foundation.h> int a = 10; static int b = 10; int main(int argc, const char * argv[]) { @autoreleasepool { auto int c = 20; static int d = 20;void (^block)(void) = ^{ NSLog(@"a=%d, b=%d, c=%d, d=%d", a, b, c, d); }; a = 30; b = 35; c = 40; d = 45; block(); } return 0; }
//列印
2019-01-09 15:42:16.246684+0800 MJ_TEST[4180:224738] a=30, b=35, c=20, d=45Program ended with exit code: 0
分析:很顯然,只有c的值沒有改變,其它變數的值都改變了——為什麼,看下底層程式碼實現;
二、main.cpp
int a = 10; static int b = 10; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int c; int *d; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int_c, int *_d, int flags=0) : c(_c), d(_d) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int c = __cself->c; // bound by copy int *d = __cself->d; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_main_1f1f41_mi_0, a, b, c, (*d)); } 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; auto int c = 20; static int d = 20; void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, c, &d)); a = 30; b = 35; c = 40; d = 45; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; }
分析:
1)C語言語法
<1>int c被轉換成auto int c:我們知道c、d為區域性變數,而a、b為全域性變數,C語言中,所有沒有修飾符的區域性變數預設的修飾符為auto,static修飾的變數為靜態變數,還有一個register註冊類的在此不再贅述(自己有興趣上網查下);
<2>auto型別的區域性變數的生命週期為離其最近的大括號內,超出該大括號,該變數被自動銷燬;
<3>static型別的變數(不論是全域性還是區域性),其值一直保留在記憶體中,不受大括號的限制,程式結束時才被銷燬;
2)變數捕獲概念
我們發現在block結構體中,存在c、d而不存在a、b變數,在此,我們把存在於block結構體中的外部變數稱為變數捕獲,不存在的則沒有被捕獲,所以a、b變數沒有被block捕獲;
3)變數呼叫流程
<1>在block結構體中,c保持不變依然為int型變數,而d被轉換成int型指標變數,因此在main函式中通過__main_block_impl_0方法傳遞實參c本身的值和d指向的記憶體的值&d;
而在block的建構函式中,c(_c), d(_d)為C++語法<=>c = _c,d = _d,那麼main函式中的實參c、&d最終傳遞給了block結構體中的變數c和指標變數d;
<2>最後在__main_block_func_0方法中,對c、d而言,須先獲取block內部的成員變數再輸出;而對於a、b,因為是全域性變數,所以可以直接引用;
綜上所述:
auto區域性變數因為作用域(或生命週期)有限,隨時會銷燬,故block在引用時系統會自動將其值儲存在block結構體中(即捕獲);而全域性變數和static修飾的變數(區域性或全域性),並不會隨時被銷燬,其值一直會在記憶體中保持不變,知道整個程式結束時才銷燬
1)另外從另一個角度理解,全域性變數其作用域為從其定義的地方開始到該檔案結束止都是有效的,所以main函式中可以用,__main_block_func_0函式中也可以用,不需要再將其儲存到block自身的結構體中;
2)static修飾的區域性變數會被轉化成指標變數,而儲存到block結構體中也是指標,因為指標本身的值為另一個變數的地址,所以block對該指標的操作始終是對另一個變數的地址的操作,而非另一個變數值的本身,當對d重新賦值時,block中的指標變數指向的變數的值也就隨之改變,對*d輸出當然被改變(*d即取出指向的記憶體地址存放的值);
3)auto區域性變數被捕獲,即是在記憶體中重新開闢了記憶體來存放該變數的值(即copy),只不過是在block結構體對應的記憶體中;
三、結論
1.
2.auto修飾的區域性變數在block定義後的修改,不影響block內部對該變數的使用;後兩者,有影響;
四、擴充套件——OC物件捕獲問題
1)Person.m——注:此處我將.h檔案也貼過來了,為了很好的閱讀
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject @property (nonatomic, copy) NSString *name; - (instancetype)initWithName:(NSString *)name; @end NS_ASSUME_NONNULL_END #import "Person.h" int weight_ = 10; @implementation Person - (void)test { void(^block)(void) = ^{ NSLog(@"-----%p", self); NSLog(@"-----%@", _name); NSLog(@"-----%@", self.name); NSLog(@"-----%d", weight_); }; } - (instancetype)initWithName:(NSString *)name { self = [super init]; if (self) { } return self; } @end
2)Person.cpp——注:此處只對.m檔案進行轉化
問題一:引數
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)); } static instancetype _Nonnull _I_Person_initWithName_(Person * self, SEL _cmd, NSString * _Nonnull name) { self = ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init")); if (self) { } return self; }
分析:
<1>在.m檔案內部有兩個方法test和initWithName,前者不帶引數,後者帶一個引數name,但是轉成C++後,發現,兩個方法前面均自動加上了兩個引數:Person * self, SEL _cmd;這是每個方法必備的兩個引數,前者是呼叫物件本身self,後者是方法名;
<2>此處的self為例項物件而非類物件(驗證方法:在test方法中列印self的地址%p,會發現每次呼叫的值都不一樣,而類物件在記憶體中只有一份;
問題二: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; } };
分析:
<1>self被捕獲到block結構體體中,那麼可以肯定self是auto型別的區域性變數;
<2>從另一個角度理解:self作為實參從main函式中傳遞到block結構體的建構函式__Person__test_block_impl_0的形參_self,再將_slef賦值於self;而引數本身就是一個auto型別的區域性變數,函式結束後就自動被銷燬;
問題三:block程式碼塊執行
int weight_ = 10; static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) { Person *self = __cself->self; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_0, self); NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_1, (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_name))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_2, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name"))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_3, weight_); }
分析:
<1>self作為auto型別的區域性變數,輸出前需先從block結構體中取出該成員變數;
<2>weight作為全域性變數,直接引用,無須捕獲;
<3>以_name來引用物件屬性,其本質是block的成員變數,存放在類物件的結構體記憶體中,而結構體指標變數須通過"->"來引用該結構體成員變數,但是self作為例項物件與Person類物件不是同一個,問什麼能通過"->"來引用?
————原因:self的第一個成員變數為isa,而isa是指向類物件的指標,即類物件的首地址跟例項物件的首地址是同一個地址,而結構體成員變數在記憶體中的地址是連續的,因此self可以通過"->"形式來找到_name成員變數;
<4>self.name即通過getter方法來訪問name值,轉換成C++為objc_msgSend方法,即通過訊息轉發機制來訪問name值,而訊息轉發機制的本質是通過isa來找到類物件,進而訪問該類物件中的name成員變數;
3)結論
【1】oc例項物件self會被捕獲到block結構體中;
【2】@property宣告的屬性的引用,須先執行【1】步驟,因此嚴格上講也是被捕獲到block中;
【3】.m檔案中宣告的全域性變數,不受self影響,依然不會被捕獲到block中,;