說明: <1>閱讀本文章,請參照前面的block文章加以理解; <2>本文的變數指的是auto類型的局部變數(包括實例對象); <3>ARC和MRC兩種模式均適用; 一、無法修改的原因 //代碼 很明顯,強行給age賦值會報錯; //列印 分析:為什麼在block內部不能改變age的值?往下看 // ...
說明:
<1>閱讀本文章,請參照前面的block文章加以理解;
<2>本文的變數指的是auto類型的局部變數(包括實例對象);
<3>ARC和MRC兩種模式均適用;
一、無法修改的原因
//代碼
很明顯,強行給age賦值會報錯;
void test1() { int age = 10; block = ^{ // age = 20; NSLog(@"%d", age); }; }
//列印
2019-01-15 15:00:43.641417+0800 MJ_TEST[3676:199449] 10 Program ended with exit code: 0
分析:為什麼在block內部不能改變age的值?往下看
//clang
struct __test1_block_impl_0 { struct __block_impl impl; struct __test1_block_desc_0* Desc; int age; __test1_block_impl_0(void *fp, struct __test1_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __test1_block_func_0(struct __test1_block_impl_0 *__cself) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_main_d8e7e4_mi_0, age); } void test1() { int age = 10; block = ((void (*)())&__test1_block_impl_0((void *)__test1_block_func_0, &__test1_block_desc_0_DATA, age)); }
分析:
<1>age被捕捉到block結構體中,根據輸出的結果很明顯是在ARC模式下,因此當被強指針變數block持有時,系統會自動將block對象從棧區拷貝到堆區;而MRC模式下,因為block對象會隨著test1()方法結束,其記憶體地址會被回收,age的值為亂碼
2019-01-15 16:12:43.234301+0800 MJ_TEST[4434:243013] -272632456 Program ended with exit code: 0
<2>block代碼塊是通過__test1_block_func_0函數來實現,而該函數應用的age就是block對象結構體__test1_block_impl_0中的age,這跟test1()方法中的age是兩個不同的age:前者存在於堆區,後者存在於棧區;
<3>想要在__test1_block_impl_0函數中去改變test1()方法中的局部變數,顯然是不成立的,根本就拿不到該局部變數;
結論:block對象本身的代碼塊是存放在一個新的函數中,而block引用的外部auto類型的局部變數存在於block指針變數所在的函數中——所以,兩個不同的函數間彼此不可能改變對方內部的局部變數;
二、修改方法
1)static修飾
//代碼
void test2() { static int age = 10; block = ^{ age = 20; NSLog(@"%d", age); }; }
//列印
2019-01-15 16:31:56.533685+0800 MJ_TEST[4676:255859] 20 Program ended with exit code: 0
//clang
struct __test2_block_impl_0 { struct __block_impl impl; struct __test2_block_desc_0* Desc; int *age; __test2_block_impl_0(void *fp, struct __test2_block_desc_0 *desc, int *_age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __test2_block_func_0(struct __test2_block_impl_0 *__cself) { int *age = __cself->age; // bound by copy (*age) = 20; NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_main_bf7285_mi_1, (*age)); }
分析:
<1>根據前述文章,此時test2()方法中的整型age是以指針的形式被捕捉到block對象結構體中,該指針變數指向值為10的記憶體區域;
<2>通過指針當然可以變數該指針指向的記憶體區域的值(見“__test2_block_func_0”函數),這點沒問題——C語言語法基礎;
結論:通過static修飾auto類型的局部變數來改變值,其本質是通過指針來改變變數的值;
補充:static修飾的弊端
<1>修改了變數的屬性類型——age由整型變數變成整型指針變數;
<2>static修飾的局部變數,是存放在數據區(全局區),直到整個程式結束才會釋放記憶體——不利於記憶體的有效利用;
2)設置為全局變數
此處就不論證,很容易理解,block對象代碼塊是放在另一個函數中,而該函數是可以訪問該全局變數的——這點沒問題;
3)__block修飾
//代碼
void test3() { __block int age = 10; block = ^{ age = 20; NSLog(@"%d", age); }; }
//列印
2019-01-15 16:51:56.337321+0800 MJ_TEST[4909:268533] 20 Program ended with exit code: 0
//clang
struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; }; struct __test3_block_impl_0 { struct __block_impl impl; struct __test3_block_desc_0* Desc; __Block_byref_age_0 *age; // by ref __test3_block_impl_0(void *fp, struct __test3_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 __test3_block_func_0(struct __test3_block_impl_0 *__cself) { __Block_byref_age_0 *age = __cself->age; // bound by ref (age->__forwarding->age) = 20; NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_main_3fa1c2_mi_2, (age->__forwarding->age)); } void test3() { __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10}; block = ((void (*)())&__test3_block_impl_0((void *)__test3_block_func_0, &__test3_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344)); }
分析:
<1>在test3()方法中,被__block修飾的age變數被轉成__Block_byref_age_0類型的變數,而__Block_byref_age_0是一個結構體並且第一個成員變數是isa指針,那麼可以肯定__Block_byref_age_0類型age是一個OC對象——即經__block修飾的auto類型的局部變數會被系統生成一個新的OC對象;
<2>__Block_byref_age_0結構體中:__forwarding是一個指向該結構體本身的指針變數;age就是被捕獲到block結構體中的test3()方法中的age(ARC會被copy到堆區);
<3>在block對象的代碼塊函數__test3_block_func_0中,對整型變數age賦值流程:拿到block對象本身結構體中的成員變數age(__Block_byref_age_0類型指針變數)——>拿到新生成的OC對象結構體__Block_byref_age_0中的成員變數__forwarding——>拿到__Block_byref_age_0中的成員變數age;
補充:age->__forwarding->age <=> age->age,但是為什麼通過__forwarding(要多一道手續)來拿到最終的整型變數age呢?——該問題後面文章會寫到!
結論:通過__block修飾auto類型的局部變數來改變值,本質是系統會創建一個臨時的OC對象,該對象結構體存儲外部變數,而block對象結構體是通過該臨時對象來訪問外部變數;
補充:
<1> ARC模式強指針持有情況下,該OC臨時對象很顯然是存放在棧區——否則,test3()方法結束後block回調時,不能正確對age變數賦值(會崩潰)——此處涉及block的記憶體管理問題,後面文章會寫到!
<2>該方法並不會改變局部變數的類型,age其依然是atuo int類型;
<3>__block不能修飾static變數和全局變數
——因為__block就是為了在block代碼塊中修改外部auto類型的局部變數的值而設計的!
三、結論
【1】在block代碼塊中修改外部auto類型的局部變數的值:用static修飾、設置為全局變數、__block修飾;
【2】static修飾和設置為全局變數弊端:持續占有記憶體,不利於記憶體的高效利用;變數的生命周期不可控——__block反之;
【3】__block不能修飾static變數和全局變數;
註:以上對局部實例對象也適用——此處就不再論證了!
四、拓展——查找age地址值
//代碼
void test4() { __block int age = 10; block = ^{ age = 20; NSLog(@"%d", age); }; NSLog(@"%p", &age); }
//列印
2019-01-15 17:58:15.930081+0800 MJ_TEST[5583:308759] 0x100701f38 2019-01-15 17:58:15.930453+0800 MJ_TEST[5583:308759] 20 Program ended with exit code: 0
分析:列印出的age地址,據上述分析,到底是新生成的oc對象本身的地址,還是該對象結構體內成員變數age的值?
//代碼
struct __Block_byref_age_0 { void *__isa; struct __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __test3_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(void); void (*dispose)(void); }; struct __test4_block_impl_0 { struct __block_impl impl; struct __test3_block_desc_0* Desc; struct __Block_byref_age_0 *age; };
//列印
分析:
<1>上述block的橋接轉換和&(strBlock->age->age),前面的文章已經講過,此處不再贅述;
<2>我們發現,__Block_byref_age_0結構體內的成員變數age的地址和test4()方法中列印出的age的地址是一樣的——也就是說,我們在OC代碼中對age的操作都是對__Block_byref_age_0結構體內的成員變數age的操作,這樣有利於程式員的理解(蘋果公司刻意隱藏底層)!