block本質探尋四之copy
說明:
<1>閱讀本文,最好閱讀之前的block文章加以理解;
<2>本文內容:三種block型別的copy情況(MRC)、是否深拷貝、錯誤copy;
一、MRC模式下,三種block型別的copy情況
//程式碼
void test1() { int age = 10; void(^block1)(void) = ^{ NSLog(@"-----"); }; void(^block2)(void) = ^{ NSLog(@"-----%d", age); };id block3 = [block2 copy]; NSLog(@"%@ %@ %@", [block1 class], [block2 class], [block3 class]); NSLog(@"%@ %@ %@", [[block1 copy] class], [[block2 copy] class], [[block3 copy] class]); }
//列印
2019-01-11 14:14:06.902974+0800 MJ_TEST[2183:154918] __NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__2019-01-11 14:14:06.903260+0800 MJ_TEST[2183:154918] __NSGlobalBlock__ __NSMallocBlock__ __NSMallocBlock__ Program ended with exit code: 0
分析:
<1>只有stack型別block例項物件copy後的型別變為malloc,這個前面文章已經討論過,沒有問題;
<2>global型別例項物件儲存在資料區,copy操作其實什麼也沒做;malloc在堆區,copy之後肯定還是在堆區,但不會開闢新的記憶體,只是引用計數加1——此處分析,可以通過clang和地址、引用計數列印來檢視,此處不再贅述;
結論:
補充:上述copy的操作是針對block例項物件,那麼類物件是存在哪個區呢?往下看
//程式碼
int a = 20; void test2() { int b = 10; void(^block1)(void) = ^{ NSLog(@"-----"); }; void(^block2)(void) = ^{ NSLog(@"-----%d", b); }; id block3 = [block2 copy]; id block1Cls = object_getClass(block1); id block2Cls = object_getClass(block2); id block3Cls = object_getClass(block3); NSLog(@"a--global--%p", &a); NSLog(@"b--auto place--%p", &b); NSLog(@"alloc----%p", [[NSObject alloc] init]); NSLog(@"Person----%p", [Person class]); NSLog(@"------block---instance---"); NSLog(@"block1----%@ %p", [block1 class], block1); NSLog(@"block2----%@ %p", [block2 class], block2); NSLog(@"block3----%@ %p", [block3 class], block3); NSLog(@"------block---Class---"); NSLog(@"block1Cls----%@ %p", block1Cls, block1Cls); NSLog(@"block2Cls----%@ %p", block2Cls, block2Cls); NSLog(@"block3Cls----%@ %p", block3Cls, block3Cls); }
//列印
2019-01-11 14:58:29.922125+0800 MJ_TEST[2443:177646] a--global--0x100002520 2019-01-11 14:58:29.922498+0800 MJ_TEST[2443:177646] b--auto place--0x7ffeefbff59c 2019-01-11 14:58:29.922525+0800 MJ_TEST[2443:177646] alloc----0x100526420 2019-01-11 14:58:29.922561+0800 MJ_TEST[2443:177646] Person----0x1000024f8 2019-01-11 14:58:29.922585+0800 MJ_TEST[2443:177646] ------block---instance--- 2019-01-11 14:58:29.922639+0800 MJ_TEST[2443:177646] block1----__NSGlobalBlock__ 0x1000020c0 2019-01-11 14:58:29.922666+0800 MJ_TEST[2443:177646] block2----__NSStackBlock__ 0x7ffeefbff560 2019-01-11 14:58:29.922699+0800 MJ_TEST[2443:177646] block3----__NSMallocBlock__ 0x102812000 2019-01-11 14:58:29.922717+0800 MJ_TEST[2443:177646] ------block---Class--- 2019-01-11 14:58:29.922736+0800 MJ_TEST[2443:177646] block1Cls----__NSGlobalBlock__ 0x7fffb33c3460 2019-01-11 14:58:29.922756+0800 MJ_TEST[2443:177646] block2Cls----__NSStackBlock__ 0x7fffb33c3060 2019-01-11 14:58:29.922777+0800 MJ_TEST[2443:177646] block3Cls----__NSMallocBlock__ 0x7fffb33c3160 Program ended with exit code: 0
分析:
<1>Person類物件:打印出的類物件Person的地址跟全域性變數a和global型別block例項物件的地址類似度極高(都以"0x100002"開頭),我們知道全域性變數a和global型別block例項變數都是存放在資料區(全域性區),那麼可以肯定類物件也是存放在資料區中;
<2>block類物件:通過runtime的API我們拿到了三種類型block類物件,發現類物件的地址並不以"0x100002"開頭———其中的原因我就懵逼了(記憶體地址不是很瞭解),但是可以推斷應該也是在資料區,為什麼呢?往下看
//程式碼
typedef void(^Block)(void); Block block1; void test3() { int b = 10; block1 = ^{ NSLog(@"-----%d", b); }; NSLog(@"%p %p", block1, object_getClass(block1)); }
//設定全域性斷點
//列印
2019-01-11 16:34:06.281146+0800 MJ_TEST[3354:234187] 0x7ffeefbff538 0x7fffb33c3060 2019-01-11 16:34:06.281455+0800 MJ_TEST[3354:234187] ------272632520 2019-01-11 16:34:06.281477+0800 MJ_TEST[3354:234187] 0x7ffeefbff538 0xf9552b000e0 2019-01-11 16:34:06.281496+0800 MJ_TEST[3354:234187] 0x7ffeefbff588 0x7fffb33c3060
分析:
1)作為auto型別的區域性變數,age的作用域僅限於test3()函式內,所以在main函式中再去回撥block時,age已經被自動釋放(所佔記憶體被回收),所以age的值顯示亂碼;而同時block1其實也被銷燬了,為什麼?往下看
<1>object_getClass(block1)每次返回的值都不同,而其他只都保持不變(已經反覆run了多次);
<2>當我們第二次去回撥block1時,如上報出一個很經典的錯誤——野指標呼叫,即指標所指向的記憶體空間已經被回收(即被釋放),但是此時並沒有對該指標賦值一個新的記憶體地址或者nil值,該指標變成了一個野指標,指向不明確;
補充:記憶體洩露:是指指標一直指向某一片記憶體空間,但是程式已經不需要再用該記憶體空間了,但其他的程式又無法呼叫該記憶體空間(只能開闢新的記憶體空間),這樣很容易導致記憶體爆增;所以記憶體洩露跟野指標呼叫是完全相反的;
記憶體溢位:是指系統分配給程式的記憶體空間不夠用,這樣也很容易導致野指標呼叫的問題;
<3>對block1進行copy的情形:
//程式碼
void test3() { int b = 10; block1 = [^{ NSLog(@"-----%d", b); } copy]; NSLog(@"%p %p", block1, object_getClass(block1)); } int main(int argc, const char * argv[]) { @autoreleasepool { // test1(); // test2(); test3(); block1(); NSLog(@"%p %p", block1, object_getClass(block1)); int age = 10; Block bl = ^{ NSLog(@"%d", age); }; NSLog(@"%p %p", bl, object_getClass(bl)); block1(); block1(); block1(); // test4(); // block(); } return 0; }
//列印
2019-01-11 16:40:36.144529+0800 MJ_TEST[3397:237736] 0x100526670 0x7fffb33c3160 2019-01-11 16:40:36.144849+0800 MJ_TEST[3397:237736] -----10 2019-01-11 16:40:36.144864+0800 MJ_TEST[3397:237736] 0x100526670 0x7fffb33c3160 2019-01-11 16:40:36.144893+0800 MJ_TEST[3397:237736] 0x7ffeefbff588 0x7fffb33c3060 2019-01-11 16:40:36.144916+0800 MJ_TEST[3397:237736] -----10 2019-01-11 16:40:36.144934+0800 MJ_TEST[3397:237736] -----10 2019-01-11 16:40:36.144950+0800 MJ_TEST[3397:237736] -----10 Program ended with exit code: 0
分析:copy之後,block1一直沒有被釋放(堆區需要手動管理),即block1一直指向了合法的記憶體空間,因此不會出現野指標呼叫的bug;
綜上:block1是一個指標變數,其指向等號右邊的程式碼塊本質是一個oc物件,存放在棧區中,當回撥該程式碼塊時,其已經被自動釋放,但是block1因為沒有重新賦值而變成了野指標,所以block1指向的程式碼塊是已經被銷燬了的;
2)block1銷燬後,新建立的bl打印出的類物件的地址跟block1銷燬前打印出的地址都是0x7fffb33c3060,因為類物件在記憶體中只有一份,據此,block1的類物件並沒有隨著block1的銷燬而銷燬,所以block的類物件不可能存在於棧區,同一個block類物件供所有建立的block例項物件的isa指標訪問並且類物件是系統自動建立並管理的,因此也不可能存在於堆區,也不會存在於程式碼區
————結論:block類物件跟其他OC例項物件的類物件一樣,都只存在於資料區!!!
二、block拷貝是否深拷貝
//程式碼
void test4() { int age = 10; int *agePtr = &age; NSLog(@"age---1:\n%d %p %d %p %p", age, &age, *agePtr, agePtr, &agePtr); block1 = [^{ NSLog(@"age----2:\n%d %p %d %p %p", age, &age, *agePtr, agePtr, &agePtr); } copy]; }
//列印
2019-01-14 09:55:33.399468+0800 MJ_TEST[907:35484] age---1: 10 0x7ffeefbff59c 10 0x7ffeefbff59c 0x7ffeefbff590 2019-01-14 09:55:33.399735+0800 MJ_TEST[907:35484] age----2: 10 0x100400238 1 0x7ffeefbff59c 0x100400230 Program ended with exit code: 0
分析:
<1>copy後,age、agePtr自身的地址值都發生了變化,說明兩個變數都從棧區拷貝到了堆區;
<2>指標變數的值不再是10而是1(亂碼),因為指標變數依然指向age拷貝前的記憶體區域,而該記憶體區隨時可能被釋放;
我們再看看對nsstring字串的深拷貝(mutableCopy)和淺拷貝(copy)操作
//程式碼
void test5() { NSString *strSource = @"abc"; NSLog(@"source:\n%@ %p %p", strSource, strSource, &strSource); NSString *str1 = [strSource copy]; NSLog(@"str1:\n%@ %p %p", str1, str1, &str1); NSString *str2 = [strSource mutableCopy]; NSLog(@"str2:\n%@ %p %p", str2, str2, &str2); }
//列印
2019-01-14 10:14:26.299400+0800 MJ_TEST[1066:45457] source: abc 0x1000023a0 0x7ffeefbff598 2019-01-14 10:14:26.299783+0800 MJ_TEST[1066:45457] str1: abc 0x1000023a0 0x7ffeefbff590 2019-01-14 10:14:26.299897+0800 MJ_TEST[1066:45457] str2: abc 0x100507170 0x7ffeefbff588 Program ended with exit code: 0
分析:
<1>很明顯,淺拷貝只拷貝了指標變數str1(從程式碼區(常量區)到堆區),該指標依然指向程式碼區常量abc的記憶體區;
<2>深拷貝不僅指標變數被拷貝到堆區,而且常量abc也被拷貝到了堆區;
說明:深拷貝和淺拷貝區別,見參考連結:https://www.jianshu.com/p/63239d4d65e0;
綜上所述:block的拷貝均拷貝了指標和該指標指向的值到堆區,但是新的指標卻依然指向拷貝前的記憶體區域——因此,block的copy類似於深拷貝,不完全是深拷貝!
三、錯誤copy
//程式碼
void(^block)(void); void test6() { int age = 10; NSLog(@"age----%p", &age); block = ^{ NSLog(@"age----%p", &age); NSLog(@"----%d", age); }; NSLog(@"block--1---%p", block); NSLog(@"block class--1---%p", [block class]); id coBlock = [block copy]; NSLog(@"%@", [coBlock class]); NSLog(@"block--2---%p", coBlock); NSLog(@"block class--2---%p", [coBlock class]); }
//列印
2019-01-14 10:31:32.767665+0800 MJ_TEST[1159:53399] age----0x7ffeefbff59c 2019-01-14 10:31:32.767975+0800 MJ_TEST[1159:53399] block--1---0x7ffeefbff578 2019-01-14 10:31:32.768027+0800 MJ_TEST[1159:53399] block class--1---0x7fff8e0fe060 2019-01-14 10:31:32.768075+0800 MJ_TEST[1159:53399] __NSMallocBlock__ 2019-01-14 10:31:32.768094+0800 MJ_TEST[1159:53399] block--2---0x100729380 2019-01-14 10:31:32.768111+0800 MJ_TEST[1159:53399] block class--2---0x7fff8e0fe160 2019-01-14 10:31:32.768127+0800 MJ_TEST[1159:53399] age----0x7ffeefbff598 2019-01-14 10:31:32.768141+0800 MJ_TEST[1159:53399] -----272632456 Program ended with exit code: 0
分析:
<1>block:copy前後,block的地址發生了變化,因為block從棧區被拷貝到堆區了,這一點沒問題;那麼block的類物件地址也發生了變化,因為copy前block的型別為stack型別,之後是malloc型別(系統會自動建立一個類物件),前者存放在棧區,後者存放在堆區,所以也沒問題;
<2>age:並沒有被copy 到堆區,block回撥時,已經被釋放,其值為亂碼,這點沒問題;但是age的地址值這麼發生變化了?我們再往下看
//程式碼
void test7() { int age = 10; int *agePtr = &age; NSLog(@"1----\n%d %p %d %p %p", age, &age, *agePtr, agePtr, &agePtr); block = ^{ NSLog(@"1----\n%d %p %d %p %p", age, &age, *agePtr, agePtr, &agePtr); }; id coBlock = [block copy]; }
//列印
2019-01-14 10:54:30.119695+0800 MJ_TEST[1281:64385] 1---- 10 0x7ffeefbff59c 10 0x7ffeefbff59c 0x7ffeefbff590 2019-01-14 10:54:30.119992+0800 MJ_TEST[1281:64385] 1---- 10 0x7ffeefbff588 32766 0x7ffeefbff59c 0x7ffeefbff580 Program ended with exit code: 0
分析:
<1>不論是age還是agePtr,block回撥時,本身的地址都會發生變化,因為所佔記憶體都被釋放,記憶體地址不回固定,系統會重新編排(個人YY,具體不清楚);
<2>但是,儘管age的值變成亂碼,而指標變數agePtr的值卻沒變依然是原age的地址值——為什麼指標變數的記憶體值不是亂碼呢?也許是因為程式碼區(常量區)跟棧區、堆區的儲存規則的區別,指標變數本身已經被釋放,其值變與不變好像沒有多大的意義——但是,從程式碼規範角度,被釋放後,應當將指標變數置為nil,防止野指標呼叫!