1. 程式人生 > >block本質探尋四之copy

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,防止野指標呼叫!

 

GitHub