說明: <1>閱讀本文,最好閱讀之前的block文章加以理解; <2>本文內容:三種block類型的copy情況(MRC)、是否深拷貝、錯誤copy; 一、MRC模式下,三種block類型的copy情況 //代碼 //列印 分析: <1>只有stack類型block實例對象copy後的類型變為mal ...
說明:
<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,防止野指針調用!
結論:所謂錯誤的copy只是copy了block指針變數(等號左邊),並非block代碼塊本身(等號右邊)——因此,被引用的外部auto類型的局部變數不會被copy到堆區;