Block學(xué)習(xí)總結(jié)(三)

關(guān)于block的存儲域

一、 block變量存儲域

1. ARC和MRC不同的存儲情況

通過對block本質(zhì)的探究,發(fā)現(xiàn)block內(nèi)部也是有一個isa指針指向它所屬的類,所以block其實也是一個對象。

// ARC 環(huán)境下
    void (^blk)() = ^{
    
    };
    NSLog(@"%@",[blk class]);

其打印結(jié)果為 " _ _NSGlobalBlock _ _ ",可想而知block是屬于一個叫NSGlobalBlock類的對象。其實 block的類有三種。分別為 NSGlobalBlock ,NSStackBlock ,NSMallocBlock。從它們的名字能猜測到,這三種類型的block對應(yīng)著block不同的存儲區(qū)域。相繼為 global(全局?jǐn)?shù)據(jù)區(qū)域) stack (棧區(qū)) malloc(堆區(qū))。

分別在ARC和MRC的情況下看看哪些情況下對應(yīng)的block存儲區(qū)域,首先是ARC的情況下

    NSLog(@"未賦值 未引用外部變量 %@", [^void() {
    
    } class]);
    
    void (^blk)() = ^{
    
    };
    NSLog(@"賦值 未引用外部變量 %@",[blk class]);
    
    int a = 1;
    void (^blk1)() = ^{
        a;
    };
    NSLog(@"賦值 引用外部變量 %@",[blk1 class]);
    
    NSLog(@"未賦值 引用外部變量  %@", [^void() {
        a;
    } class]);

最后的輸出結(jié)果為:

2017-01-08 21:49:15.389 block存儲域[10956:1104789] 未賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:49:15.389 block存儲域[10956:1104789] 賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:49:15.389 block存儲域[10956:1104789] 賦值 引用外部變量 __NSMallocBlock__
2017-01-08 21:49:15.390 block存儲域[10956:1104789] 未賦值 引用外部變量  __NSStackBlock__

可以對ARC的情況下做一個總結(jié):

  • NSGlobalBlock === 在block未引用外部變量的時候
  • NSMallocBlock === 在block引用了外部變量的時候,且將block賦值給了block類型的變量
  • NSStackBlock === 在block引用了外部變量的時候, 沒有將block賦值給block類型的變量時。

接下來是對在MRC的情況下,block的不同存儲區(qū)域

    NSLog(@"未賦值 未引用外部變量 %@", [^void() {
    
    } class]);
    
    void (^blk)() = ^{
    
    };
    NSLog(@"賦值 未引用外部變量 %@",[blk class]);
    
    int a = 1;
    void (^blk1)() = ^{
        a;
    };
    NSLog(@"賦值 引用外部變量 %@",[blk1 class]);
    
    NSLog(@"未賦值 引用外部變量  %@", [^void() {
        a;
    } class]);

輸出結(jié)果為:

2017-01-08 21:56:21.789 block存儲域[10994:1108230] 未賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:56:21.790 block存儲域[10994:1108230] 賦值 未引用外部變量 __NSGlobalBlock__
2017-01-08 21:56:21.790 block存儲域[10994:1108230] 賦值 引用外部變量 __NSStackBlock__
2017-01-08 21:56:21.791 block存儲域[10994:1108230] 未賦值 引用外部變量  __NSStackBlock__

我們可以看到無論是否引用外部變量和是否賦值給了block類型的變量,都沒有出現(xiàn)mallocBlock,那么MRC的情況下如何才會生成mallocBlock呢。這時我們需要調(diào)用對象的copy方法。

    void (^blk)() = ^{
    
    };
    NSLog(@"copy 未引用外部變量 %@",[[blk copy] class]);
   
    int a = 1;
    void (^blk1)() = ^{
        a;
    };
    NSLog(@"copy 引用外部變量 %@",[[blk1 copy] class]);

2. ARC情況下什么時候會進(jìn)行copy

block引用了外部變量就會存儲在棧區(qū),在ARC環(huán)境下大多數(shù)情況會自動copy操作后復(fù)制到堆區(qū)。但是有些情況下編譯器不會自動進(jìn)行copy操作:

  • block作為函數(shù)參數(shù)的時候不會自動進(jìn)行copy操作,除了以下情況之外:

    (1) Cocoa框架的方法切方法名中又usingBlock。

    (2) GCD的API。

通過以下代碼驗證:

  - (void)viewDidLoad {
    [super viewDidLoad];
    NSArray *tempArray = [self getBlockArray];
    blk_t blk = tempArray[0];
    blk();
}

- (id)getBlockArray {
    int a = 10;
    return [[NSArray alloc] initWithObjects:^{
        NSLog(@"blk1 %d",a);
    },^{
        NSLog(@"blk2 %d",a);
            }, nil];
}

因為作為參數(shù)傳遞的block的存儲區(qū)域為棧區(qū),而出了 getBlockArray 函數(shù)后,block的作用域結(jié)束了,當(dāng)調(diào)用函數(shù)取得數(shù)組時,訪問已經(jīng)釋放了的block,就會造成了crash。要是想正確訪問block ,可以做以下修改,對作為參數(shù)傳遞的block進(jìn)行copy操作。

- (id)getBlockArray {
    int a = 10;
    return [[NSArray alloc] initWithObjects:[^{
        NSLog(@"blk1 %d",a);
    } copy],[^{
        NSLog(@"blk2 %d",a);
            }copy], nil];
}

二、 __block變量存儲域

我們知道對于block內(nèi)部使用的外部變量,不允許在block內(nèi)部修改外部變量的值。以下代碼是編譯不過的。

    int a = 10;
    void (^blk)() = ^(){
    a = 20;
    } ;
    blk();

因為在block進(jìn)行copy的同時,會將block內(nèi)使用的外部變量也進(jìn)行一次copy操作,如果這個變量是在棧區(qū)的,則會被拷貝到堆區(qū),并被block持有。而如果之前這個變量是在堆區(qū)的則不會有影響,只是被block持有。

    int a = 10;
    NSLog(@"定義block前 %p",&a);
    void (^blk)() = ^(){
        NSLog(@"block內(nèi)部 %p",&a);
    } ;
    blk();
    NSLog(@"定義block后 %p",&a);

打印結(jié)果為:

2017-02-08 14:20:06.716 block存儲域_test[23117:147534] 定義block前 0x7fff5b635a4c
2017-02-08 14:20:06.717 block存儲域_test[23117:147534] block內(nèi)部 0x60800005a1b0
2017-02-08 14:20:06.717 block存儲域_test[23117:147534] 定義block后 0x7fff5b635a4c

16進(jìn)制轉(zhuǎn)換為10進(jìn)制,block外部為 140734726625868 ,block內(nèi)部使用的時候為 106102872449456,2者相差1532868764個字節(jié),轉(zhuǎn)換為1461 mb。因為堆地址遠(yuǎn)小于棧中的地址,又因為iOS中一個進(jìn)程的棧區(qū)內(nèi)存只有 1mb,所以在block內(nèi)部 的變量已經(jīng)在堆區(qū)內(nèi)了。

__block 變量

對于block內(nèi)部使用 _ _block變量時,和普通的變量一樣,如果這個變量是在棧區(qū)的,則會被拷貝到堆區(qū),并被block持有。而如果之前這個變量是在堆區(qū)的則不會有影響,只是被block持有。但是為什么能在block內(nèi)部修改 _ _block修飾的變量。

    __block int a = 10;
    NSLog(@"定義block前 %p",&a);
    void (^blk)() = ^(){
        a++;
        NSLog(@"block內(nèi)部 %p",&a);
    } ;
    NSLog(@"定義block后 %p",&a);
    blk();

打印的結(jié)果為:

2017-02-08 14:44:01.933 block存儲域_test[26207:167214] 定義block前 0x7fff569eca48
2017-02-08 14:44:01.934 block存儲域_test[26207:167214] 定義block后 0x60800003eb38
2017-02-08 14:44:01.934 block存儲域_test[26207:167214] block內(nèi)部 0x60800003eb38

與普通的外部變量不同的是,定義block后,變量的地址也指向了堆中的那個地址。而不是定義之前的棧中地址。還記得__block內(nèi)部那個指向 變量自身的那個forwading指針嗎,當(dāng) _ _block變量賦值到堆區(qū)時,棧區(qū)的變量的forwading指針也改為了指向堆中的那個變量的地址,所以在block外部使用變量時,它訪問的是堆中的那個變量

a++  <===> a._forwarding ->a)++

三、 截獲的對象存儲域

通常的一個變量,如下,當(dāng)它超過了作用域之后,就會被釋放了。

- (void)test5 {
    NSMutableArray *tempArray = [NSMutableArray array];
}

但是對于block中使用的外部變量,在某些情況下,卻能超過變量的作用域存在。

(該代碼 是在ARC環(huán)境下運(yùn)行)
blk_t globalblk;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    globalblk();
}

- (void)test5 {
    NSMutableArray *tempArray = [NSMutableArray array];
    globalblk = ^{
        id obj = [[NSObject alloc] init];
        [tempArray addObject:obj];
        NSLog(@"count == %ld a == %d", tempArray.count,a);
    };
}

該代碼中,tempArray變量出了 test5這個函數(shù),就超出了作用域,但是該代碼卻能正確的打印出來。說明: 在ARC環(huán)境下,當(dāng)給block賦值給一個block變量時,將會默認(rèn)的對block進(jìn)行一次copy操作。如果在MRC環(huán)境下,也就是沒有對block進(jìn)行手動copy的話,這段代碼就會crash。

四、 __block和對象

關(guān)于__block修飾的對象,被block使用的時候,在ARC和MRC情況下,是有很大的不同的。當(dāng)在ARC環(huán)境下,block內(nèi)部使用 _ _block修飾的對象的時候,它和正常的外部變量一樣,當(dāng)block從棧區(qū)copy到堆區(qū)時,會被block所持有,因此能超出變量總用域存在。而在MRC的情況下,當(dāng) _ _block修飾的變量被block使用時,不會隨著block從棧區(qū)copy到堆區(qū)而被block所持有。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    globalblk();
}

- (void)test5 {
    __block NSMutableArray *tempArray = [NSMutableArray array];
    globalblk = [^{
        id obj = [[NSObject alloc] init];
        [tempArray addObject:obj];
        NSLog(@"count == %ld", [tempArray count]);
    } copy];
}

以上的代碼,在ARC環(huán)境下能正常運(yùn)行,在MRC情況下會因為野指針的訪問而crash。

五、 block的循環(huán)引用

block持有_ _strong修飾的外部變量時,block會持有該對象,而如果該對象又持有block時,就會造成循環(huán)引用的問題。

typedef void (^blk_t)();
@interface Person ()
{
    blk_t blk;
}
@end

@implementation Person
- (instancetype)init {
    if (self = [super init]) {
        blk = [^(){
            NSLog(@"%@",self);
        } copy];
    }
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
}

該段代碼的dealloc一定不會被打印,因為在調(diào)用 Person的init函數(shù)是,成員變量blk使用了 self,使得blk對 self 持有。而blk作為 Person對象的成員變量,Person對象就對 blk持有。造成了這種互相持有的關(guān)系后,該對象就無法釋放了。

循環(huán)引用問題.png

解決方法(1): 通過對block內(nèi)部使用的變量,改為weak修飾。

- (instancetype)init {
    if (self = [super init]) {
        __weak typeof(self) weakSelf = self;
        blk = [^(){
            NSLog(@"%@",weakSelf);
        } copy];
    }
    return self;
}

解決方法(2): 之前說過,在MRC環(huán)境下,用_ _block修飾變量時,block不會對該變量進(jìn)行持有。

//僅對MRC環(huán)境下有效
- (instancetype)init {
    if (self = [super init]) {
        __block tempSelf = self;
        blk = [^(){
            NSLog(@"%@",tempSelf);
        } copy];
    }
    return self;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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