你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS內存管理(4)--Block屬性用copy修飾 & 避免循環引用的問題

iOS內存管理(4)--Block屬性用copy修飾 & 避免循環引用的問題

編輯:IOS開發綜合

一、Block的類型

根據Block在內存中的位置分為三種類型NSGlobalBlock,NSStackBlock, NSMallocBlock。

  • NSGlobalBlock:類似函數,位於text段;
  • NSStackBlock:位於棧內存,函數返回後Block將無效;
  • NSMallocBlock:位於堆內存。

 

二、Block的copy、retain、release操作

不同於NSObjec的copy、retain、release操作:

  • Block_copy與copy等效,Block_release與release等效;
  • 對Block不管是retain、copy、release都不會改變引用計數retainCount,retainCount始終是1;
  • NSGlobalBlock:retain、copy、release操作都無效;
  • NSStackBlock:retain、release操作無效,必須注意的是,NSStackBlock在函數返回後,Block內存將被回收。即使retain也沒用。容易犯的錯誤是[[mutableAarry addObject:stackBlock],(補:在arc中不用擔心此問題,因為arc中會默認將實例化的block拷貝到堆上)在函數出棧後,從mutableArray中取到的stackBlock已經被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然後加入數組:[mutableArray addObject:[[stackBlock copy] autorelease]]。支持copy,copy之後生成新的NSMallocBlock類型對象。
  • NSMallocBlock:支持retain、release,雖然retainCount始終是1,但內存管理器中仍然會增加、減少計數。copy之後不會生成新的對象,只是增加了一次引用,類似retain;
  • 盡量不要對Block使用retain操作

三、ARC與非ARC下的block

對於引用了外部變量的Block,如果沒有對他進行copy,他的作用域只會在聲明他的函數棧內(類型是__NSStackBlock__),如果想在非ARC下直接返回此類Block,Xcode會提示編譯錯誤的,如下圖:

屏幕快照 2013-12-13 下午5.15.13

(Xcode提示Returning block that lives on the local stack)

而在ARC環境下,上述代碼會編譯通過,因為ARC會自動加入copy操作。

比如可以在ARC下運行如下代碼:

 

//ARC
MyBlock block = func();
NSLog(@"%d", block());
NSLog(@"%@", [block class]);

 

輸出:

123
__NSMallocBlock__

類型是__NSMallocBlock__,說明Block已經被copy到了堆中了。

即便把Block用strong修飾,系統也會把block copy到堆中。

例如:@property (nonatomic,strong)void (^SubmitBlock)(GoodsModel *goodsModel,int number);

打印一下block的類型為如下圖

\

\

當然其實在非ARC下,也可以使上面有錯誤的函數編譯通過。如下代碼:

 

typedef int(^MyBlock)();
MyBlock func()
{
    int i = 123;
    //非ARC下不要這樣!!!
    MyBlock ret = ^{ return i; };
    return ret;
}

 

 

我們把原來的返回值賦給一個變量,然後再返回這個變量,就可以編譯通過了。不過雖然編譯通過了,這個返回的Block作用域仍是在函數棧中的,因此一旦函數運行完畢後再使用這個Block很可能會引發BAD_ACCESS錯誤。

所以在非ARC下,必須把Block復制到堆中才可以在函數外使用Block,如下正確的代碼:

 

typedef int(^MyBlock)();
MyBlock func()
{
    //非ARC
    int i = 123;
    return [^{ return i; } copy];
}

 

 

 

我們可以直接通過輸出變量的指針,就可以驗證Block被copy後,他所引用的變量被復制到了堆中的情況,如下代碼(非ARC下):

 

//非ARC
void func()
{
    int a = 123;
    __block int b = 123;
    NSLog(@"%@", @"=== block copy前");
    NSLog(@"&a = %p, &b = %p", &a, &b);
    
    void(^block)() = ^{
        NSLog(@"%@", @"=== Block");
        NSLog(@"&a = %p, &b = %p", &a, &b);
        NSLog(@"a = %d, b = %d", a, b = 456);
    };
    block = [block copy];
    block();
    
    NSLog(@"%@", @"=== block copy後");
    NSLog(@"&a = %p, &b = %p", &a, &b);
    NSLog(@"a = %d, b = %d", a, b);
    
    [block release];
}

 

輸出:

=== block copy前
&a = 0x7fff5fbff8bc, &b = 0x7fff5fbff8b0
=== Block
&a = 0x100201048, &b = 0x100201068
a = 123, b = 456
=== block copy後
&a = 0x7fff5fbff8bc, &b = 0x100201068
a = 123, b = 456

可以看到,在Block執行中,他所引用的變量a和b都被復制到了堆上。而被標記__block的變量事實上應該說是被移動到了堆上,因此,當Block執行後,函數棧內訪問b的地址會變成堆中的地址。而變量a,仍會指向函數棧內原有的變量a的空間。

四、So,Block屬性的聲明,首先需要用copy修飾符。Block默認存放在棧中,可能隨時被銷毀,需要作用域在堆中,所以只有copy後的Block才會在堆中,棧中的Block的生命周期是和棧綁定的。

 

五、循環引用的問題

循環引用是另一個使用Block時常見的問題。為什麼會循環引用?因為retain。

 

因為block在拷貝到堆上的時候,會retain其引用的外部變量,那麼如果block中如果引用了他的宿主對象,那很有可能引起循環引用,如:

 

self.myblock = ^{
    
    [self doSomething];
    
};

 

 

下面是對ARC環境做的測試:

 

- (void)dealloc
{
    NSLog(@"no cycle retain");
}



- (id)init
{
    self = [super init];
    
    if (self) {
        
        
#if TestCycleRetainCase1
        
        
        
        //會循環引用
        
        self.myblock = ^{
            
            [self doSomething];
            
        };
        
#elif TestCycleRetainCase2
        
        
        
        //會循環引用
        
        __block TestCycleRetain *weakSelf = self;
        
        self.myblock = ^{
            
            [weakSelf doSomething];
            
        };
        
        
        
#elif TestCycleRetainCase3
        
        
        
        //不會循環引用
        
        __weak TestCycleRetain *weakSelf = self;
        
        self.myblock = ^{
            
            [weakSelf doSomething];
            
        };
        
        
        
#elif TestCycleRetainCase4
        
        //不會循環引用
        
        __unsafe_unretained TestCycleRetain *weakSelf = self;
        
        self.myblock = ^{
            
            [weakSelf doSomething];
            
        };
        
        
#endif
        
        NSLog(@"myblock is %@", self.myblock);
        
    }
    
    return self;
}



- (void)doSomething
{
    NSLog(@"do Something");
}



int main(int argc, char *argv[]) {
    
    @autoreleasepool {
        
        TestCycleRetain* obj = [[TestCycleRetain alloc] init];
        
        obj = nil;
        
        return 0;
    }
}

 

經過上面的測試發現,在加了__weak和__unsafe_unretained的變量引入後,TestCycleRetain方法可以正常執行dealloc方法,而不轉換和用__block轉換的變量都會引起循環引用。
因此防止循環引用的方法如下: __weak TestCycleRetain *weakSelf = self;

 

所以,在ARC下,由於__block抓取的變量一樣會被Block retain,所以必須用弱引用才可以解決循環引用問題,iOS 5之後可以直接使用__weak,之前則只能使用__unsafe_unretained了,__unsafe_unretained缺點是指針釋放後自己不會置空。示例代碼:

 

//iOS 5之前可以用__unsafe_unretained
//__unsafe_unretained typeof(self) weakSelf = self;
__weak typeof(self) weakSelf = self;
self.myBlock = ^(int paramInt)
{
    //使用weakSelf訪問self成員
    [weakSelf anotherFunc];
};

 

 

在非ARC下,顯然無法使用弱引用,這裡就可以直接使用__block來修飾變量,它不會被Block所retain的,參考代碼:

 

//非ARC
__block typeof(self) weakSelf = self;
self.myBlock = ^(int paramInt)
{
    //使用weakSelf訪問self成員
    [weakSelf anotherFunc];
};

 

六、__weak 和 __block的區別

1. 循環引用時:有上文可知,__weak用在ARC環境時;__block僅可用在非ARC環境時(因為ARC環境下仍然會被retain)。

2.修改局部變量時:需要加__block,否則不能在block中修改局部變量。如下:

 

__block int multiplier = 7;
     int (^myBlock)(int) = ^(int num) {
         multiplier ++;//這樣就可以了
         return num * multiplier;
     };

 

 



 

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved