1. 程式人生 > >iOS Block深層次總結和一些經典的面試題

iOS Block深層次總結和一些經典的面試題

上面幾個是之前看書記錄的知識點,可以回顧下,下面用人話概括下自己的理解,方便以後參考,先記住一個概念,Block就是一個物件

OC Block—> C++轉換

1.最普通的轉換

int a = 100;  
int b = 200;  
const charchar *ch = "b = %d\n";  
void (^block)(void) = ^{  
printf(ch,b);  
};  

struct __main_block_impl_0 {  
  struct __block_impl impl;  
  struct __main_block_desc_0* Desc; 
  // 上面也別管了
// 這就是最簡單的值捕獲,外部型別是什麼就是什麼 // 可以理解為copy了一個副本進入這個block,外部怎麼變都和裡面無關 const charchar *ch; int b; // 下面先別看了 __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, const charchar *_ch, int _b, int flags=0) : ch(_ch), b(_b) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl
.FuncPtr = fp; Desc = desc; } };



2.__block修飾的轉換

__block int a = 0;
void (^block)(void) = ^{a = 1};

struct __main_block_impl_0 {  
  struct __block_impl impl;  
  struct __main_block_desc_0* Desc;
  // 忽略上面  
  // 這就是重點,當你加了__block 內部轉換就變成了結構體指標
  // 你可以理解為加了修飾符,就變成了結構體,那這裡就是拿到了引用
  // 外部和內部引用的是同一個,外部修改就能改變內部
__Block_byref_a_0 *a; // by ref // 忽略下面 __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };

介紹下幾個例子

這裡詳細介紹可以參考最頂部的幾個連結,下面分析下幾個典型的例子

基本案例1

    __block int a = 10;
    void (^test)(void) = ^{
        printf("%d",a); // 20
    };
    a = 20;
    test();



    int b = 100;
    void (^block1)(void) = ^{
        printf("%d",b); // 100
    };
    b = 200;
    block1();
    return 0;

根據這個結果和上面給出的轉換後的結構體,咱們可以理解為
1.沒有加__block修飾符,Block截獲的時候只是把外部這個int值賦值到Block內部的一個int變數,那麼這種copy的方法,外部無論怎麼改變都不會影響內部值,因此,列印100
2.加了__block,可以根據上面轉換後的程式碼,我個人把它理解為,加了修飾符,相當於用物件(結構體)包裹起來,而這個變數就是該結構體的某一個屬性,那麼Block截獲的就是這個包裹的結構體,之後你再操作傳進去結構體的地址,就是通過包裹的結構體間接操作包裹結構體內部的變數,因此外部的改變,其實就是通過包裹的結構體在改變變數的值
3.其實剛開始接觸理解起來怪怪的,總之,Block就是值的Copy,那麼直接截獲到的值是無法通過外部的改變而影響內部copy出來的值的,因此,系統通過修飾符幫我們再用結構體包了一層,只是copy的是這個重新建立結構體的指標,你依然無法給這個指標在Block裡面重新複製,思路和沒有加修飾符一樣,況且你能賦值,你也拿不到啊,所以Block內還是無法直接修改截獲的值,你只是操作已經被修飾符包裝一層的物件或者本身就是物件而已,雖然你程式碼是這麼寫,看上去直接修改了截獲的值,可那只是假象而已

這裡寫圖片描述

驚不驚喜,意不意外???
想要知道如何變成C++程式碼,可以看頂部第一篇文章介紹,其實Block就只是copy了一個你的外部變數而已,只是有修飾符,你的變數已經被一個結構體所包含,而沒有修飾符,你的變數被copy了一份進入結構體,如果基礎型別,那麼就是簡單的值複製,如果是指標,那麼就是copy了一個新的指標,可以理解為指向的物件引用計數+1,這裡涉及到迴圈引用,可以參考頭部的文章分析,你外部指標變數改變指標地址,不會影響block內部,很簡單,你要在block裡面修改變數的直接值,你必須加__block修飾符,通過新的結構體物件間接修改,訪問的時候其實也就通過結構體訪問最原始的或者已經修改的值

這裡寫圖片描述

基本案例2


NSMutableString *mutable_string = [NSMutableString stringWithString:@"aaa"];
    void(^mutable_append)(void)=^{
        [mutable_string appendString:@"ccc"];
    };
    [mutable_string appendString:@"bbb"];
    mutable_append();
    NSLog(@"\\n %@",mutable_string);  //結果:aaabbbccc
    // 沒有__block,但是也沒有涉及到直接指標的修改,只是操作而已,因此aaabbbccc

    NSString *string = @"aaa";
    NSString*(^append)(void)=^{
        return [string stringByAppendingString:@"ccc"];
    };
    string = @"bbb";
    NSLog(@"\\n %@",append());  //結果:aaaccc
    // 沒有__block,copy值,截獲之後和外部都指向@"aaa",但是外部string修改了指向為@"bbb",內部指標還是指向@"aaa",所以aaaccc

    __block NSString *block_string = @"aaa";
    NSString*(^block_append)(void)=^{
        return [block_string stringByAppendingString:@"ccc"];
    };
    block_string = @"bbb";
    NSLog(@"\\n %@",block_append()); //結果: bbbccc
    // 有__block,自動轉換成新的結構體,string變成其內部屬性,block截獲的是新結構體的地址,外部block_string重新賦值,也不是簡單的賦值,內部轉換成`a.__forwarding.a`的程式碼,可以理解為通過新的結構體改變指標所指向的值,通過__block所形成的新結構體作為載體,之後所有的操作都是操作同一個物件,理解為指標操作,因此,形成一致,列印bbbccc

    __block NSString *name = [NSString stringWithFormat:@"%@",@"mikejing"];

    NSString *(^addaa)(void) = ^{
        return [name stringByAppendingString:@"cjj"];
    };
    name = @"MKJ";
    NSLog(@"\\n %@",addaa()); // \n MKJcj
    // 同上

    char *ch = "b =\n";
    void (^block)(void) = ^{
        printf("%s",ch); // b =
    };

    ch = "value had changed.b =\n";
    block();
    // 無法修改,上面已經介紹

總結

1.Block為什麼不能修改外部變數(這裡如何全域性和static變數,頭部文章有介紹)?因為你Block是copy值的型別進入Struch結構體儲存,如果外部變數修改指向,影響不了內部copy的值,好比兩個指標都指向字串@”a”,當一個指標指向了@”b”,但是另一個指標還是指向@”a”,除非儲存@”a”的地址下的值發生了變化
2.如何在Block內部和外部變數統一,或者如何Block內部修改值?用__block,該修飾符的意思,包裹成新的物件,變數就成了這個物件的屬性,Block捕獲的就是這個新物件的地址,之後這個變量出現的上下文,都是通過這個新物件的地址間接訪問,Block內也一樣訪問這個物件,這樣就保持了一致性,都訪問同一個,無論你在哪修改,都能讓值產生變化
3.迴圈引用的產生,既然是copy,那指標copy就會導致retain count + 1,就必須用弱引用來消除,這裡就不展開了

一道大廠的面試題

@autoreleasepool{
        NSString *test = @"test1111";
        TestBlock block = ^(void){
            dispatch_sync(dispatch_queue_create("jd.test", DISPATCH_QUEUE_SERIAL), ^{
                NSLog(@"%@",test);
            });
        };
        test = @"test2222";
        block();
    }
    // 輸出什麼,在哪個執行緒,為什麼?
    // <NSThread: 0x60c00007cec0>{number = 1, name = main}
    // test1111

我感覺我功力不夠,看不出這題目的玄機,感覺就考了一個block而已啊???!!!根據上面分析,沒有__block,因此只是值copy,列印test1111,block裡面搞了個同步執行緒,由於本身就在主執行緒,因此沒有開新執行緒,還是主執行緒列印。
文章和題目都是個人的理解而已,如果有不同意見和簡介,希望各位留言糾正,好記性不如爛筆頭,多記錄點知識點

我這算是比較通俗的寫了點見解,頂部還有幾個比較書面化的知識點分析,喜歡看C原始碼的可以看看內部,這裡有一份寫的很不錯的文章分析
Block深入分析

2018年更新

更新源自於看到了論壇上的一篇文章 想要看原文章的可以點選 Block面試帖子

首先Block的文章頂部一定介紹了一些了,可以自己翻閱,這裡我覺得最重要需要明白的一點就是
MRC下面 block在建立的時候,它的記憶體是分配在棧(stack)上,可能被隨時回收,而不是在堆(heap)上。他本身的作於域是屬於建立時候的作用域,一旦在建立時候的作用域外面呼叫block將導致程式崩潰。通過copy可以把block拷貝(copy)到堆,保證block的宣告域外使用。
ARC下面預設建立就在堆上面了,可以直接供外部調

一些比較老的書上會描述Block有三種類型,分別是

NSGlobalBlock:全域性Block,程式被載入後被分配在程序資料段上,也就是常量,靜態建立的Block。

NSMallocBlock:在程序堆上分配的Block,動態建立的Block。

NSStackBlock:程序棧上分配的Block,動態建立的Block。

 void(^blockA)(void) = ^{
        NSLog(@"just a block");
    };
    NSLog(@"%@", blockA);

    int value = 10;
    void(^blockB)(void) = ^{

        NSLog(@"just a block === %d", value);
    };
    NSLog(@"%@", blockB);

    void(^ __weak blockC)(void) = ^{
        NSLog(@"just a block === %d", value);
    };

    NSLog(@"%@", blockC);

    void(^ __weak blockD)(void) = ^{
        NSLog(@"just a block");
    };

    NSLog(@"%@", blockD);
2018-02-24 14:30:27.929070+0800 mianshi[1067:37772] <__NSGlobalBlock__: 0x102f680c8>
2018-02-24 14:30:27.929179+0800 mianshi[1067:37772] <__NSMallocBlock__: 0x60800025d940>
2018-02-24 14:30:27.929282+0800 mianshi[1067:37772] <__NSStackBlock__: 0x7ffeecc96b20>
2018-02-24 14:30:27.929360+0800 mianshi[1067:37772] <__NSGlobalBlock__: 0x102f68148>



注意看它們的地址,NSGlobalBlock的地址明顯要短,因為它是在程序資料段上的。一般來講StackBlock在ARC下基本不可見了,但是通過修飾符也可以出現
blockC則是強行用__weak宣告讓其分配在棧上,這裡會看到一個黃色的警告(Assigning block literal to a weak variable; object will be released after assignment),大意就是指分配後就會被釋放。就是說viewDidLoad這個方法return後這個block就會被釋放。那麼 weak修飾也分兩種情況,一般Block沒有捕獲變數的情況下都是GlobalBlock型別的,捕獲之後就是StackBlock型別了。

動態分配和靜態分配的區分是在哪裡?觀察一下就發現NSGlobalBlock型別是沒有捕獲區域性變數的,它只是列印一一個字串。通過NSString literal建立的字串是放在常量區的,也就是資料段上。全域性的block裡沒有引用任何堆或棧上的資料。另外如果將上面的例子中的int value = 10;改為const int value = 10;那麼blockB將變成NSGlobalBlock,這是因為const修飾下value裡的值會儲存在常量區即資料段上,也就是不違反原則,只要block literal裡沒有引用棧或堆上的資料,那麼這個block會自動變為NSGlobalBlock型別,這是編譯器的優化。

在屬性宣告上,我們一般會用copy修飾一個Block屬性。原因是什麼?
在MRC中,block預設是在棧上建立的。如果我們將它賦值給一個成員變數,如果成員變數沒有被copy修飾或在賦值的時候沒有進行copy,也就是區域性變數離開作用域之後會被系統回收,那麼在使用這個block成員變數的時候就會崩潰。

看一段程式碼

@property(nonatomic, weak) void(^block)();

- (void)viewDidLoad {
[superviewDidLoad];

int value = 10;
void(^blockC)() = ^{
NSLog(@"just a block === %d", value);
     };

NSLog(@"%@", blockC);
     _block = blockC;

}

- (IBAction)action:(id)sender {
NSLog(@"%@", _block);
}

1.首先我們看到的屬性修飾符是weak(注意不是Assign)
2.ARC下面能正常執行,是因為ARC下Block預設已經分配到heap上了 blockC建立的時候內部捕獲了變數,而且沒有weak修飾符,因此從globalBlock變成了MallocBlock型別,第一個列印的就是2018-02-24 14:55:30.870410+0800 mianshi[1467:64609] <__NSMallocBlock__: 0x6000000558d0> ,後面用weak修飾的屬性進行賦值,weak修飾物件,不會增加引用計數,因此離開作用域之後,Block被釋放,由於是weak修飾的,那麼weak修飾的指標會在weak hash表中自動置為nil,因此下面事件中打印出來的就(null)
3.MRC下面就會崩潰,由於預設是生成在stack上面的,離開作用域之後被系統回收,再訪問被釋放的物件就會崩潰
4.在我看來,ARC下崩潰不崩潰是修飾符導致的,weak肯定不會崩潰,因為weak 引用的物件在釋放的時候會把指標都置為nil,但是如果你用assign修飾,ARC下面是有很大概率崩潰的,為什麼很大概率是因為這塊用assgin修飾的block地址有沒有被再次使用,你也可以理解為崩潰。因為assgin只是簡單的賦值,不會再物件釋放的時候自動置為nil,這也是weak和assign最大的區別

這裡原文章留了一個小小的題目

@property(nonatomic, weak) void(^block)();


- (void)viewDidLoad {
[superviewDidLoad];

void(^ __weak blockA)() = ^{
NSLog(@"just a block");
    };

    _block = blockA;

}

- (IBAction)action:(id)sender {
    _block();
}

這裡的答案很顯然了,由於根據上面的四種列印,這裡打印出來的就是GlobalBlock,是靜態資料儲存區域的,可以再外部繼續訪問