2019 iOS面試題-----Block原理、Block變量截獲、Block的三種形式、__block

2019 iOS面試題大全---全方面剖析面試
  • 什么是Block?
  • Block變量截獲
  • Block的幾種形式

一、什么是Block?

  • Block是將函數及其執行上下文封裝起來的對象。

比如:

NSInteger num = 3;
    
    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
        
        return n*num;
    };
    
    block(2);

通過clang -rewrite-objc WYTest.m命令編譯該.m文件,發現該block被編譯成這個形式:

    NSInteger num = 3;

    NSInteger(*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__WYTest__blockTest_block_impl_0((void *)__WYTest__blockTest_block_func_0, &__WYTest__blockTest_block_desc_0_DATA, num));

    ((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);

其中WYTest是文件名,blockTest是方法名,這些可以忽略。
其中__WYTest__blockTest_block_impl_0結構體為

struct __WYTest__blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __WYTest__blockTest_block_desc_0* Desc;
  NSInteger num;
  __WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block_impl結構體為

struct __block_impl {
  void *isa;//isa指針,所以說Block是對象
  int Flags;
  int Reserved;
  void *FuncPtr;//函數指針
};

block內部有isa指針,所以說其本質也是OC對象
block內部則為:

static NSInteger __WYTest__blockTest_block_func_0(struct __WYTest__blockTest_block_impl_0 *__cself, NSInteger n) {
  NSInteger num = __cself->num; // bound by copy


        return n*num;
    }

所以說 Block是將函數及其執行上下文封裝起來的對象
既然block內部封裝了函數,那么它同樣也有參數和返回值。

二、Block變量截獲

1、局部變量截獲 是值截獲。 比如:
    NSInteger num = 3;
    
    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
        
        return n*num;
    };
    
    num = 1;
    
    NSLog(@"%zd",block(2));

這里的輸出是6而不是2,原因就是對局部變量num的截獲是值截獲。
同樣,在block里如果修改變量num,也是無效的,甚至編譯器會報錯。

NSMutableArray * arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];
    
    void(^block)(void) = ^{
        
        NSLog(@"%@",arr);//局部變量
        
        [arr addObject:@"4"];
    };
    
    [arr addObject:@"3"];
    
    arr = nil;
    
    block();

打印為1,2,3
局部對象變量也是一樣,截獲的是值,而不是指針,在外部將其置為nil,對block沒有影響,而該對象調用方法會影響

2、局部靜態變量截獲 是指針截獲。
   static  NSInteger num = 3;
    
    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
        
        return n*num;
    };
    
    num = 1;
    
    NSLog(@"%zd",block(2));

輸出為2,意味著num = 1這里的修改num值是有效的,即是指針截獲。
同樣,在block里去修改變量m,也是有效的。

3、全局變量,靜態全局變量截獲:不截獲,直接取值。

我們同樣用clang編譯看下結果。

static NSInteger num3 = 300;

NSInteger num4 = 3000;

- (void)blockTest
{
    NSInteger num = 30;
    
    static NSInteger num2 = 3;
    
    __block NSInteger num5 = 30000;
    
    void(^block)(void) = ^{
        
        NSLog(@"%zd",num);//局部變量
        
        NSLog(@"%zd",num2);//靜態變量
        
        NSLog(@"%zd",num3);//全局變量
        
        NSLog(@"%zd",num4);//全局靜態變量
        
        NSLog(@"%zd",num5);//__block修飾變量
    };
    
    block();
}

編譯后

struct __WYTest__blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __WYTest__blockTest_block_desc_0* Desc;
  NSInteger num;//局部變量
  NSInteger *num2;//靜態變量
  __Block_byref_num5_0 *num5; // by ref//__block修飾變量
  __WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

( impl.isa = &_NSConcreteStackBlock;這里注意到這一句,即說明該block是棧block)
可以看到局部變量被編譯成值形式,而靜態變量被編成指針形式,全局變量并未截獲。而__block修飾的變量也是以指針形式截獲的,并且生成了一個新的結構體對象

struct __Block_byref_num5_0 {
  void *__isa;
__Block_byref_num5_0 *__forwarding;
 int __flags;
 int __size;
 NSInteger num5;
};

該對象有個屬性:num5,即我們用__block修飾的變量。
這里__forwarding是指向自身的(棧block)。
一般情況下,如果我們要對block截獲的局部變量進行賦值操作需添加__block
修飾符,而對全局變量,靜態變量是不需要添加__block修飾符的。
另外,block里訪問self或成員變量都會去截獲self。

三、Block的幾種形式

  • 分為全局Block(_NSConcreteGlobalBlock)、棧Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三種形式
    其中棧Block存儲在棧(stack)區,堆Block存儲在堆(heap)區,全局Block存儲在已初始化數據(.data)區
1、不使用外部變量的block是全局block

比如:

    NSLog(@"%@",[^{
        NSLog(@"globalBlock");
    } class]);

輸出:

__NSGlobalBlock__
2、使用外部變量并且未進行copy操作的block是棧block

比如:

  NSInteger num = 10;
    NSLog(@"%@",[^{
        NSLog(@"stackBlock:%zd",num);
    } class]);

輸出:

__NSStackBlock__

日常開發常用于這種情況:

[self testWithBlock:^{
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();

    NSLog(@"%@",[block class]);
}
3、對棧block進行copy操作,就是堆block,而對全局block進行copy,仍是全局block
  • 比如堆1中的全局進行copy操作,即賦值:
void (^globalBlock)(void) = ^{
        NSLog(@"globalBlock");
    };

 NSLog(@"%@",[globalBlock class]);

輸出:

__NSGlobalBlock__

仍是全局block

  • 而對2中的棧block進行賦值操作:
NSInteger num = 10;

void (^mallocBlock)(void) = ^{

        NSLog(@"stackBlock:%zd",num);
    };

NSLog(@"%@",[mallocBlock class]);

輸出:

__NSMallocBlock__

對棧blockcopy之后,并不代表著棧block就消失了,左邊的mallock是堆block,右邊被copy的仍是棧block
比如:

[self testWithBlock:^{
    
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block
{
    block();
    
    dispatch_block_t tempBlock = block;
    
    NSLog(@"%@,%@",[block class],[tempBlock class]);
}

輸出:

__NSStackBlock__,__NSMallocBlock__
  • 即如果對棧Block進行copy,將會copy到堆區,對堆Block進行copy,將會增加引用計數,對全局Block進行copy,因為是已經初始化的,所以什么也不做。

另外,__block變量在copy時,由于__forwarding的存在,棧上的__forwarding指針會指向堆上的__forwarding變量,而堆上的__forwarding指針指向其自身,所以,如果對__block的修改,實際上是在修改堆上的__block變量。

即__forwarding指針存在的意義就是,無論在任何內存位置, 都可以順利地訪問同一個__block變量。
  • 另外由于block捕獲的__block修飾的變量會去持有變量,那么如果用__block修飾self,且self持有block,并且block內部使用到__block修飾的self時,就會造成多循環引用,即self持有block,block 持有__block變量,而__block變量持有self,造成內存泄漏。
    比如:
  __block typeof(self) weakSelf = self;
    
    _testBlock = ^{
        
        NSLog(@"%@",weakSelf);
    };
    
    _testBlock();

如果要解決這種循環引用,可以主動斷開__block變量對self的持有,即在block內部使用完weakself后,將其置為nil,但這種方式有個問題,如果block一直不被調用,那么循環引用將一直存在。
所以,我們最好還是用__weak來修飾self

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容