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

定義

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

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

iOS內(nèi)存分布一般為:棧區(qū)、堆區(qū)、全局區(qū)、常量區(qū)、代碼區(qū).其實(shí)Block也是一個(gè)Objective-C的對象,常見的有以下三種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會把它從棧區(qū)拷貝到堆區(qū)一份(棧區(qū)太小了2M),爾weak修飾的Block不會.

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

Block源碼分析

通過clang命令將Objective-C代碼轉(zhuǎn)成C++代碼,可以了解其底層機(jī)制,有助于我們更深刻的認(rèn)識其實(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è)對象
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 就是外部變量),即自動變量回作為成員變量追加到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修飾的變量,會生成一個(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是如何解決存儲域的問題

首先我們知道Block底層是結(jié)構(gòu)體,Block會轉(zhuǎn)換成block結(jié)構(gòu)體,__block會轉(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)境下,編譯器會盡可能的給我們自動添加copy的操作,這里為什么說盡量呢,因?yàn)橛行┣闆r編譯器無法判斷的時(shí)候,就不會給我們添加copy操作,這里就需要我們自己主動調(diào)用copy方法.

__block 變量的存儲域

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

Block截獲對象需要管理對象的生命周期

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

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

對于Block循環(huán)引用算是經(jīng)典問題了,當(dāng)A持有B,B持有A的時(shí)候就會出現(xiàn)循環(huán)引用.Block對于外部比那兩都會追加到結(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 是存在的,不會被 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ù),會創(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 方法中,編譯器會自動執(zhí)行 Copy 的操作,將Block 從??截惖蕉? 所以ARC 環(huán)境沒問題.
修改方案

// 主動調(diào)用 copy 方法,將 Block 從??截惖蕉阎?,Block_copy(<#...#>)
[array addObject:[^{
    printf("%cn", b);
} copy]];
  • 下面代碼在MRC 和 ARC 環(huán)境下會出現(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)境下會出現(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í)行完就會釋放,MRC 環(huán)境下會調(diào)用異常,但是這里編譯器能檢查到這種情況,這里實(shí)際效果是編譯不通過。
在 ARC 環(huán)境下,Block 作為函數(shù)返回值,會自動調(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 從??截惗眩L問變量 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 修飾
    對于這個(gè)問題,得區(qū)分 MRC 環(huán)境 和 ARC 環(huán)境;首先,通過上面小節(jié)可知,Block 引用了普通外部變量,都是創(chuàng)建在棧區(qū)的;對于分配在棧區(qū)的對象,我們很容易會在釋放之后繼續(xù)調(diào)用,導(dǎo)致程序奔潰,所以我們使用的時(shí)候需要將棧區(qū)的對象移到堆區(qū),來延長該對象的生命周期。
    對于 MRC 環(huán)境,使用 Copy 修飾 Block,會將棧區(qū)的 Block 拷貝到堆區(qū)。
    對于 ARC 環(huán)境,使用 Strong、Copy 修飾 Block,都會將棧區(qū)的 Block 拷貝到堆區(qū)。
    所以,Block 不是一定要用 Copy 來修飾的,在 ARC 環(huán)境下面 Strong 和 Copy 修飾效果是一樣的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容