一、oc代碼 提示:看本文章之前,最好按順序來看; //代碼 //列印 分析: 1)三個block的類型分別為:__NSGlobalBlock__、__NSMallocBlock__、__NSStackBlock__,什麼原因,往下看; 2)上述三種類型最終都是繼承自NSBlock,而NSBlock ...
一、oc代碼
提示:看本文章之前,最好按順序來看;
//代碼
void test1() { int age = 10; void(^block1)(void) = ^{ NSLog(@"block1----"); }; void(^block2)(void) = ^{ NSLog(@"block2----%d", age); }; NSLog(@"block1-----\n%@ %@ %@ %@", [block1 class], [[block1 class] superclass], [[[block1 class] superclass] superclass], [[[[block1 class] superclass] superclass] superclass]); NSLog(@"block2-----\n%@ %@ %@ %@", [block2 class], [[block2 class] superclass], [[[block2 class] superclass] superclass], [[[[block2 class] superclass] superclass] superclass]); NSLog(@"block-----\n%@ %@ %@ %@", [^{ NSLog(@"block----%d", age); } class], [[^{ NSLog(@"block----%d", age); } class] superclass], [[[^{ NSLog(@"block----%d", age); } class] superclass] superclass], [[[[^{ NSLog(@"block----%d", age); } class] superclass] superclass] superclass]); }
//列印
2019-01-10 14:36:04.290317+0800 MJ_TEST[3446:174827] block1----- __NSGlobalBlock__ __NSGlobalBlock NSBlock NSObject 2019-01-10 14:36:04.290608+0800 MJ_TEST[3446:174827] block2----- __NSMallocBlock__ __NSMallocBlock NSBlock NSObject 2019-01-10 14:36:04.290652+0800 MJ_TEST[3446:174827] block----- __NSStackBlock__ __NSStackBlock NSBlock NSObject Program ended with exit code: 0
分析:
1)三個block的類型分別為:__NSGlobalBlock__、__NSMallocBlock__、__NSStackBlock__,什麼原因,往下看;
2)上述三種類型最終都是繼承自NSBlock,而NSBlock又是繼承自NSObject:此處又進一步說明block其實就是一個OC對象(前面的文章已經證明過);
說明:上述結果是在ARC模式下列印的結果,現在我們看看MRC的列印情況
//設置
//列印
2019-01-10 15:05:50.667948+0800 MJ_TEST[3576:189745] block1----- __NSGlobalBlock__ __NSGlobalBlock NSBlock NSObject 2019-01-10 15:05:50.668257+0800 MJ_TEST[3576:189745] block2----- __NSStackBlock__ __NSStackBlock NSBlock NSObject 2019-01-10 15:05:50.668279+0800 MJ_TEST[3576:189745] block----- __NSStackBlock__ __NSStackBlock NSBlock NSObject Program ended with exit code: 0
分析:發現MRC模式下,三種block類型:__NSGlobalBlock__、__NSStackBlock__、__NSStackBlock__,為什麼中間的類型由malloc變成了stack?這是因為ARC系統自動幫助我們對block進行了copy操作;
補充一下:clang成C++代碼,我們看下
struct __test1_block_impl_0 { struct __block_impl impl; struct __test1_block_desc_0* Desc; __test1_block_impl_0(void *fp, struct __test1_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct __test1_block_impl_1 { struct __block_impl impl; struct __test1_block_desc_1* Desc; int age; __test1_block_impl_1(void *fp, struct __test1_block_desc_1 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct __test1_block_impl_2 { struct __block_impl impl; struct __test1_block_desc_2* Desc; int age; __test1_block_impl_2(void *fp, struct __test1_block_desc_2 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
分析:發現都是_NSConcreteStackBlock類型,不能正確反應block的實質類型(據說是LLVM編譯器版本的問題,而clang又是LLVM的一部分);
二、原因分析
1)程式記憶體結構
<1>首先,程式的記憶體結構分為:程式區(代碼區)、數據區(全局區)、堆區、棧區;
<2>全局變數、static類型的局部變數存放在數據區,記憶體直到程式結束才自動釋放;auto類型的局部變數、參數(形/實參)存放在棧區,離其最近的大闊結束時自動記憶體自動被釋放;通過malloc\alloc\copy等手動開闢記憶體的,則存放在堆區,需要程式員手動予以釋放;程式區可以看成非變數區,除變數外,程式的一些其他代碼即常量存放在該區;
說明:
<1>除了堆區需要程式員手動管理記憶體外,其他區都由系統自動管理;
<2>等號左右:
{ //等號左邊:auto形局部變數,存放在棧區;等號右邊:常量,存放在數據區; int age = 10; //等號左邊:auto形局部變數(指針),存放在棧區;等號右邊:auto形局部變數地址,存放在棧區; int *agePtr = &age; //等號左邊:auto形局部變數(指針),存放在棧區;等號右邊:alloc開闢的對象,存放在堆區; NSObject *objc = [[NSObject alloc] init]; }
2)block類型
<1>三種block類型(global、malloc、stack),從字面理解,可以推斷依次存放在數據區、堆區、棧區;
<2>我們發現,blcok1沒有訪問任何變數,後兩個block都訪問量變數age,而age是一個auto類型的局部變數;似乎block的類型跟訪問的變數有關係?往下看;
//代碼
int weight = 20; void test2() { static int age = 10; void(^block1)(void) = ^{ NSLog(@"-----%d", age); }; void(^block2)(void) = ^{ NSLog(@"-----%d", weight); }; NSLog(@"%@ %@", [block1 class], [block2 class]); }
//列印
2019-01-10 15:52:56.366509+0800 MJ_TEST[3852:215571] __NSGlobalBlock__ __NSGlobalBlock__ Program ended with exit code: 0
分析:
<1>如果age是static修飾的局部變數,或者訪問全局變數,則block的類型都是__NSGlobalBlock__,那麼我們基本上可以肯定,block的類型取決於其訪問的變數的屬性;
<2>這裡帶來了一個新的問題:
因為auto類型的局部變數是存放在棧區的,而block要訪問該變數,經前述文章分析,block會講該變數捕獲到block結構體內部,即重新開闢記憶體來存放該局部變數(相當於copy操作,但不是copy),那麼此時的block自己是存放在哪個區呢?
前面說了,auto類型的局部變數一定是存放在棧區的,這點毋庸置疑,而block雖然新開闢記憶體來存放該變數,但改變不了該變數是一個auto類型的局部變數的屬性,因此此時的block也只能存放在棧區;
既然存放在棧區,則訪問的變數作用域僅限於離其最近的大括弧範圍內,超出則被自動釋放,我們來驗證下
//代碼
void(^block)(void); void test3() { int age = 10; block = ^{ NSLog(@"----%d", age); }; } int main(int argc, const char * argv[]) { @autoreleasepool { // test1(); // test2(); test3(); block(); } return 0; }
//列印
2019-01-10 16:09:19.489782+0800 MJ_TEST[3939:224126] -----272632456 Program ended with exit code: 0
分析:
<1>age是一個auto類型的局部變數,作用域僅限於test3()函數,該函數一旦調用完畢,age則被自動釋放(變成垃圾記憶體,值不確定);
<2>根據列印結果,age的值不是10而是一堆亂碼,說明age已經被自動釋放,block再次調用時,訪問的是被廢棄的記憶體;
那麼如何才能不被自動釋放?往下看
//代碼
void test4() { int age = 10; block = [^{ NSLog(@"----%d", age); } copy]; } int main(int argc, const char * argv[]) { @autoreleasepool { // test1(); // test2(); // test3(); test4(); block();
NSLog(@"%@", [block class]);
} return 0; }
//列印
2019-01-11 09:37:16.563329+0800 MJ_TEST[776:26631] ----10
2019-01-11 09:37:16.563935+0800 MJ_TEST[776:26631] __NSMallocBlock__
Program ended with exit code: 0
分析:
<1>通過copy操作能達到auto類型的局部變數的值正確,為什麼?因為copy是把age的值直接拷貝到了一塊新的記憶體區域,而我們知道copy操作開闢的記憶體必定是在堆區(同時,block的類型由之前的__NSStackBlock__類型變為__NSMallocBlock__類型);
<2>因此,防止一個auto類型的局部變數自動釋放的方法,就是將其copy到堆區進行手動管理,達到對其生命周期可控的目的(所以記得要釋放[block release])——這是MRC模式下須手動管理記憶體,而在ARC模式下系統會自動管理記憶體(copy和release);
說明:block此處的copy是深拷貝還是淺拷貝,以及深拷貝和淺拷貝的區別——後面文章會寫到!
三、結論
1)blcok的類型取決於其訪問的變數的類型:
【1】global:沒有訪問auto類型局部變數——包括:沒有訪問任何變數、訪問了static類型的局部變數、訪問了全局變數(包括static和auto類型);
【2】stack:訪問了auto類型的局部變數;
【3】malloc:對block進行了copy操作;
2)存儲位置: