Block面試題(原理, 屬性修飾詞為什么用copy,使用的時(shí)候有哪些注意點(diǎn))

定義

Block是一個(gè)里面存儲(chǔ)了指向定義block時(shí)的代碼塊的函數(shù)指針,以及block外部上下文變量信息的結(jié)構(gòu)體,簡單說就是:帶有自動(dòng)變量的匿名函數(shù)

Block對(duì)象內(nèi)存相關(guān)

iOS內(nèi)存分布一般為:棧區(qū)、堆區(qū)、全局區(qū)靠胜、常量區(qū)假褪、代碼區(qū).其實(shí)Block也是一個(gè)Objective-C的對(duì)象,常見的有以下三種block

  • NSMallocBlock : 存放在堆區(qū)的Block
  • NSStackBlock : 存放在棧區(qū)的Block
  • NSGlobalBlock: 存放在全局區(qū)的Block

通過代碼實(shí)驗(yàn)(聲明 strong、copy筐带、weak 修飾的 Block彪薛,分別引用全局變量、全局靜態(tài)變量坯汤、局部靜態(tài)變量虐唠、普通外部變量) ,得出初步的結(jié)論:

  1. Block內(nèi)部沒有引用外部變量,Block在全局區(qū),屬于GlobalBlock
  2. Block 內(nèi)部有引用外部變量
    a. 引用全局變量惰聂、全局靜態(tài)變量疆偿、局部靜態(tài)變量 : Block在全局區(qū),屬于GlobalBlock

b. 引用普通的外部變量,用copy、strong修飾的Block就放在堆區(qū),屬于是MallocBlock.用weak修飾的Block存放在棧區(qū).屬于StackBlock

注意:Block引用普通外部變量,都是在棧區(qū)創(chuàng)建的,只是用strong搓幌、copy修飾的Block會(huì)把它從棧區(qū)拷貝到堆區(qū)一份(棧區(qū)太小了2M),爾weak修飾的Block不會(huì).

通過上面的可以知道,在ARC中,用strong杆故、copy修飾的Block,會(huì)從棧區(qū)拷貝到堆區(qū),所以ARC中,用strong、copy修飾Block效果是一樣的.

Block源碼分析

通過clang命令將Objective-C代碼轉(zhuǎn)成C++代碼,可以了解其底層機(jī)制,有助于我們更深刻的認(rèn)識(shí)其實(shí)現(xiàn)原理.下面是clang相關(guān)命令

//1.最簡單的命令:
clang -rewrite-objc mian.m

//2.但是如果遇到 main.m:9:9: fatal error: 'UIKit/UIKit.h' file not found 類似的錯(cuò)誤需要我們指定下框架
xcrun -sdk iphonesimulator11.4 clang -S -rewrite-objc -fobjc-arc -fobjc-runtime=ios-11.4 main.m

//3.展示 SDK 版本命令
xcodebuild -showsdks

1.下載Block源碼:
https://opensource.apple.com/source/libclosure/libclosure-65/

  1. 然后將源碼中缺少的庫添加進(jìn)入工程溉愁,具體操作可以參考這篇 Blog:
    https://blog.csdn.net/WOTors/article/details/54426316
    3.通過上面兩個(gè)步驟处铛,我們就有一個(gè)包含 Block 源碼的工程,然后可以編寫 Block 代碼拐揭,去斷點(diǎn)觀察 Block 具體的執(zhí)行過程撤蟆。
    配置工程還是比較麻煩的,這里我上傳了一份:BlockSourceCode
    https://github.com/pengxuyuan/PXYFMWK/tree/master/BlockSourceCode

簡單分析Block C++源碼

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

static struct __block_desc_0 {
    size_t reserved;
    size_t Block_size;
} _block_desc_0_DATA = { 0, sizeof(struct __block_desc_0)};

struct _block_impl_0 {

    struct __block_impl impl;
    struct __block_desc_0* Desc;
    int i; // 這個(gè)是引用外部變量 i
    _block_impl_0(void *fp, struct __block_desc_0 *desc, int _i, int flags=0) :i(_i){

        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

基本可以得出幾點(diǎn)結(jié)論:
1.結(jié)構(gòu)體中有isa指針,證明Block也是一個(gè)對(duì)象
2.Block底層是用結(jié)構(gòu)體實(shí)現(xiàn)的,結(jié)構(gòu)體 _block_impl_0 包含了 __block_impl 結(jié)構(gòu)體和__block_desc_0結(jié)構(gòu)體(作用后續(xù)補(bǔ)充)
3.__block_impl 結(jié)構(gòu)體中的FuncPtr函數(shù)指針,指向的就是我們的Block的具體實(shí)現(xiàn).真正調(diào)用Block就是利用函數(shù)指針去調(diào)用的.
4.為什么能訪問到外部變量就是因?yàn)閷⑼獠孔兞繌?fù)制到了結(jié)構(gòu)體中(int _i 就是外部變量),即自動(dòng)變量回作為成員變量追加到Block結(jié)構(gòu)體中.

分析具有__block修飾外部變量的Block源碼

我們知道Block截獲外部變量是將外部變量作為成員變量追加到Block結(jié)構(gòu)體中國,但是匿名函數(shù)存在作用域的問題,這個(gè)就是為什么我們不能再Block內(nèi)部去修改普通外部變量的原因.所以就出現(xiàn)__block修飾符來解決這個(gè)問題.

下面我們看下__block修飾的變量轉(zhuǎn)換成C++代碼的樣子

//Objective-C 代碼
 - (void)blockDataBlockFunction {
 __block int a = 100;  ///在棧區(qū)
 void (^blockDataBlock)(void) = ^{
 a = 1000;
 NSLog(@"%d", a);
 };  ///在堆區(qū)
 blockDataBlock();
 }

//C++ 代碼
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __BlockStructureViewController__blockDataBlockFunction_block_impl_0 {
  struct __block_impl impl;
  struct __BlockStructureViewController__blockDataBlockFunction_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
};

具有__block修飾的變量,會(huì)生成一個(gè) Block_byref_a_0結(jié)構(gòu)體來表示外部變量,然后再追加到Block的結(jié)構(gòu)體中,這里生成Block_byref_a_0這個(gè)結(jié)構(gòu)體的原因有兩個(gè):一個(gè)是抽象出一個(gè)結(jié)構(gòu)體,可以讓多個(gè)Block同事引用這個(gè)外部變量:兩一個(gè)是好管理:,因?yàn)锽lock_byref_a_0中有個(gè)非常重要的成員變量forwarding指針,這個(gè)指針非常重要(指向Block_byref_a_0結(jié)構(gòu)體),這里是保證當(dāng)我們將Block從棧區(qū)拷貝到堆區(qū)中,修改的變量是同一份.

Block是如何解決存儲(chǔ)域的問題

首先我們知道Block底層是結(jié)構(gòu)體,Block會(huì)轉(zhuǎn)換成block結(jié)構(gòu)體,__block會(huì)轉(zhuǎn)換成__blcok結(jié)構(gòu)體
然后block沒有截獲外部變量堂污、截獲全局變量的都屬于是全局區(qū)的block,即GlobalBlock:其余的都是棧區(qū)的Block.
為了解決作用域的問題,Block提供了copy函數(shù),將Block從棧復(fù)制到堆上,在MRC環(huán)境下需要我們自己調(diào)用Block_copy函數(shù),這里就是為什么MRC下,我們?yōu)槭裁匆胏opy來修飾Block的原因.
在ARC環(huán)境下,編譯器會(huì)盡可能的給我們自動(dòng)添加copy的操作,這里為什么說盡量呢,因?yàn)橛行┣闆r編譯器無法判斷的時(shí)候,就不會(huì)給我們添加copy操作,這里就需要我們自己主動(dòng)調(diào)用copy方法.

__block 變量的存儲(chǔ)域

Block從棧復(fù)制到堆上,__block修飾的變量也會(huì)從棧復(fù)制到堆上;為了結(jié)構(gòu)體__block變量無論在棧上還是在堆上,都可以正確的訪問變量,我們需要forwarding指針
在Block從棧復(fù)制到堆的時(shí)候,原來?xiàng)I辖Y(jié)構(gòu)體的forwarding指針,會(huì)改變指向,直接指向堆上的結(jié)構(gòu)體,這樣就可以保證之后我們都是訪問同一個(gè)結(jié)構(gòu)體中的變量,這里就是問什么__block修飾的變量,在block內(nèi)部中可以修飾的原因了.

Block截獲對(duì)象需要管理對(duì)象的生命周期

我們知道Block引用外部變量會(huì)將其追加到結(jié)構(gòu)體中,但是編譯器是無法判斷C語言結(jié)構(gòu)體的初始化和廢棄的,因此__block-desc_0會(huì)增加成員變量copy和dispose;以及block_copy家肯、block_dispose函數(shù).用來Block從棧復(fù)制到堆、堆上的Block廢棄的時(shí)候分別調(diào)用.

Block會(huì)出現(xiàn)循環(huán)引用

對(duì)于Block循環(huán)引用算是經(jīng)典問題了,當(dāng)A持有B,B持有A的時(shí)候就會(huì)出現(xiàn)循環(huán)引用.Block對(duì)于外部比那兩都會(huì)追加到結(jié)構(gòu)體中,所以在實(shí)現(xiàn)Block時(shí)候需要注意這個(gè)問題.
ARC環(huán)境一般我們用__weak來打破,MRC環(huán)境下的話,我們可以使用__block來打破循環(huán)引用.

Block面試題

  • 下面代碼在ARC和MRC環(huán)境下運(yùn)行情況
void exampleA() {
  char a = 'A';
  ^{
    printf("%cn", a);
  }();
}
exampleA();

答: 首先這個(gè)Block引用了普通的外部變量,所以這個(gè)Block是在棧上創(chuàng)建的.Block是在exampleA()函數(shù)內(nèi)創(chuàng)建的.然后創(chuàng)建完馬上調(diào)用了,這個(gè)時(shí)候 exampleA() 并沒有執(zhí)行完,所以這個(gè)棧Block 是存在的,不會(huì)被 pop 出戰(zhàn).所以在MRC和ARC 環(huán)境下都能正確編譯運(yùn)行.

  • 下面代碼在MRC環(huán)境和ARC環(huán)境下運(yùn)行的情況
void exampleB_addBlockToArray(NSMutableArray *array) {
  char b = 'B';
  [array addObject:^{
    printf("%cn", b);
  }];
}

void exampleB() {
  NSMutableArray *array = [NSMutableArray array];
  exampleB_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}
exampleB();

答: 這個(gè)跟第一題的區(qū)別就是將Block的創(chuàng)建放到一個(gè)函數(shù)中去.同理分析exampleB_addBlockToArray中創(chuàng)建的Block也是引用了普通的外部變量,Bloock創(chuàng)建在棧上.
MRC 環(huán)境上,調(diào)用exampleB_addBlockToArray 函數(shù),會(huì)創(chuàng)建一個(gè)棧 Block 存放到數(shù)組中去,然后 exampleB_addBlockToArray 函數(shù)結(jié)束, Block 被pop 出棧. 這個(gè)時(shí)候再去調(diào)用Block , Block 已經(jīng)被釋放了,所以出現(xiàn)異常,不能正確執(zhí)行.
ARC 環(huán)境下,在 NSMutableArray 的 addObject 方法中,編譯器會(huì)自動(dòng)執(zhí)行 Copy 的操作,將Block 從椕瞬拷貝到堆, 所以ARC 環(huán)境沒問題.
修改方案

// 主動(dòng)調(diào)用 copy 方法讨衣,將 Block 從棧拷貝到堆中扒披,Block_copy(<#...#>)
[array addObject:[^{
    printf("%cn", b);
} copy]];
  • 下面代碼在MRC 和 ARC 環(huán)境下會(huì)出現(xiàn)什么問題
void exampleC_addBlockToArray(NSMutableArray *array) {
  [array addObject:^{
    printf("Cn");
  }];
}

void exampleC() {
  NSMutableArray *array = [NSMutableArray array];
  exampleC_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}
exampleC();

答:exampleC_addBlockToArray 中的 Block 并沒有引用外部變量值依,所以 Block 是創(chuàng)建在全局區(qū)的,是一個(gè) GlobalBlock碟案,生命周期是跟隨著程序的愿险,故 MRC、ARC 環(huán)境下都可以正確運(yùn)行价说。

  • 下面代碼在MRC 和 ARC 環(huán)境下會(huì)出現(xiàn)什么問題
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
  char d = 'D';
  return ^{
    printf("%cn", d);
  };
}
void exampleD() {
  exampleD_getBlock()();
}
exampleD();

答:這題跟第二題差不多辆亏,區(qū)別在于這里是將 Block 作為函數(shù)返回值了;一樣棧區(qū) Block 在 exampleD_getBlock 函數(shù)執(zhí)行完就會(huì)釋放鳖目,MRC 環(huán)境下會(huì)調(diào)用異常扮叨,但是這里編譯器能檢查到這種情況,這里實(shí)際效果是編譯不通過领迈。
在 ARC 環(huán)境下彻磁,Block 作為函數(shù)返回值碍沐,會(huì)自動(dòng)調(diào)用 Copy 方法,將 Block 從棧復(fù)制到堆上(StackBlock -> MallocBlock)衷蜓,故 ARC 環(huán)境下可以正確運(yùn)行累提。

  • 下面代碼在 MRC 環(huán)境 和 ARC 環(huán)境運(yùn)行的情況
typedef void (^eBlock)();
eBlock exampleE_getBlock() {
  char e = 'E';
  void (^block)() = ^{
    printf("%cn", e);
  };
  return block;
}
void exampleE() {
  eBlock block = exampleE_getBlock();
  block()
}
exampleE();

答:這題跟第四題是一樣的,這里在 MRC 環(huán)境下磁浇,可以編譯通過斋陪,但是調(diào)用異常;ARC 環(huán)境下可以正確執(zhí)行置吓。

  • ARC 環(huán)境下輸出的結(jié)果
__block NSString *key = @"AAA";

    objc_setAssociatedObject(self, &key, @1, OBJC_ASSOCIATION_ASSIGN);
    id a = objc_getAssociatedObject(self, &key);

    void (^block)(void) = ^ {
        objc_setAssociatedObject(self, &key, @2, OBJC_ASSOCIATION_ASSIGN);
    };

    id m = objc_getAssociatedObject(self, &key);
    block();
    id n = objc_getAssociatedObject(self, &key);
    objc_setAssociatedObject(self, &key, @3, OBJC_ASSOCIATION_ASSIGN);
    id p = objc_getAssociatedObject(self, &key);
    NSLog(@"%@ --- %@ --- %@ --- %@",a,m,n,p);

答:輸入結(jié)果:1 --- (null) --- 2 --- 3无虚,代碼執(zhí)行過程如下:

1.__block 修飾的 key,創(chuàng)建在棧區(qū)衍锚,訪問變量 key 為:&(結(jié)構(gòu)體->forwarding->key) 友题,key 在棧區(qū),此時(shí)利用棧區(qū)地址作為 Key 來存值

2.變量 a 使用棧區(qū)地址取值构拳,故 a 的值為 1

3.聲明一個(gè) block咆爽,引用到了外部變量 key,此時(shí)將 block 從椫蒙拷貝堆斗埂,訪問變量 key 為:&(結(jié)構(gòu)體->forwarding->key) ,key 在堆區(qū)

4.變量 m 用堆區(qū)地址來取值凫海,故為 null

5.執(zhí)行 block呛凶,用堆區(qū)地址將 2 存進(jìn)去

6.變量 n 用堆區(qū)地址來取值,故為 2

7.再用堆區(qū)地址將 3 存進(jìn)去

8.變量 p 用堆區(qū)地址來取值行贪,故為 3

  • 有幾種方式去調(diào)用 Block
void (^block)(void) = ^{
 NSLog(@"block get called");
 };

 //1. blcok()
 block();

 //2. 利用其它方法去執(zhí)行 block
 [UIView animateWithDuration:0 animations:block];

 //3.
 [[NSBlockOperation blockOperationWithBlock:block] start];

 //4. NSInvocation
 NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@?"];
 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
 [invocation invokeWithTarget:block];

 //5.DLIntrospection invoke
 [block invoke];

 //6. 指針調(diào)用
 void *pBlock = (__bridge void *)block;
 void (*invoke)(void *, ...) = *((void **)pBlock + 2);
 invoke(pBlock);

 //7. 利用 Clang
 __strong void(^cleaner)(void) __attribute ((cleanup(blockCleanUp),unused)) = block;


 //8. 內(nèi)聯(lián)一個(gè)匯編 完成調(diào)用
 asm("callq *0x10(%rax)");

 static void blockCleanUp (__strong void (^*block)(void)) {
 (*block)();
 }
  • 如何通過 Block 實(shí)現(xiàn)鏈?zhǔn)骄幊田L(fēng)格的代碼
    具體可看實(shí)現(xiàn):Block ChainProgramming
    https://github.com/pengxuyuan/PXYFMWK/blob/master/PXYFMWK/PXYFMWK/PXYFMWK/PXYFMWK/Component/PXYChainProgramming/UIView/UIView%2BPXYChainProgramming.m
    具體參考 Masonry , Snapkit

  • Block 為什么用 Copy 修飾
    對(duì)于這個(gè)問題漾稀,得區(qū)分 MRC 環(huán)境 和 ARC 環(huán)境;首先建瘫,通過上面小節(jié)可知崭捍,Block 引用了普通外部變量,都是創(chuàng)建在棧區(qū)的啰脚;對(duì)于分配在棧區(qū)的對(duì)象殷蛇,我們很容易會(huì)在釋放之后繼續(xù)調(diào)用,導(dǎo)致程序奔潰橄浓,所以我們使用的時(shí)候需要將棧區(qū)的對(duì)象移到堆區(qū)粒梦,來延長該對(duì)象的生命周期。
    對(duì)于 MRC 環(huán)境荸实,使用 Copy 修飾 Block匀们,會(huì)將棧區(qū)的 Block 拷貝到堆區(qū)。
    對(duì)于 ARC 環(huán)境准给,使用 Strong泄朴、Copy 修飾 Block重抖,都會(huì)將棧區(qū)的 Block 拷貝到堆區(qū)。
    所以祖灰,Block 不是一定要用 Copy 來修飾的仇哆,在 ARC 環(huán)境下面 Strong 和 Copy 修飾效果是一樣的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末夫植,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子油讯,更是在濱河造成了極大的恐慌详民,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陌兑,死亡現(xiàn)場離奇詭異沈跨,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)兔综,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門饿凛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人软驰,你說我怎么就攤上這事涧窒。” “怎么了锭亏?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵纠吴,是天一觀的道長。 經(jīng)常有香客問我慧瘤,道長戴已,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任锅减,我火速辦了婚禮糖儡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘怔匣。我一直安慰自己握联,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布劫狠。 她就那樣靜靜地躺著拴疤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪独泞。 梳的紋絲不亂的頭發(fā)上呐矾,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音懦砂,去河邊找鬼蜒犯。 笑死组橄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的罚随。 我是一名探鬼主播玉工,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淘菩!你這毒婦竟也來了遵班?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤潮改,失蹤者是張志新(化名)和其女友劉穎狭郑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汇在,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翰萨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了糕殉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亩鬼。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖阿蝶,靈堂內(nèi)的尸體忽然破棺而出雳锋,到底是詐尸還是另有隱情,我是刑警寧澤羡洁,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布魄缚,位于F島的核電站,受9級(jí)特大地震影響焚廊,放射性物質(zhì)發(fā)生泄漏冶匹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一咆瘟、第九天 我趴在偏房一處隱蔽的房頂上張望嚼隘。 院中可真熱鬧,春花似錦袒餐、人聲如沸飞蛹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卧檐。三九已至,卻和暖如春焰宣,著一層夾襖步出監(jiān)牢的瞬間霉囚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工匕积, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盈罐,地道東北人榜跌。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像盅粪,于是被迫代替她去往敵國和親钓葫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容