Block基礎和retain cycle(循環引用)
blcok簡介
Block 是c語言的擴展,并不是什么高新技術是從c擴展而來的,和swift語言的閉包需要注意的是由于 Objective-C在iOS中不支持GC機制。錯誤的內存管理 要么導致return cycle內存泄漏要么內存被提前釋放導致crash.Block的使用很像函數指針,不過與函數最大的不同是:Block可以訪問函數以外、詞法作用域以內的外部變量的值。換句話說,Block不僅 實現函數的功能,還能攜帶函數的執行環境。
blcok基本語法
-
1.如何定義block變量
第一個block是一個int類型的返回值,并且有兩個參數 第二個block是沒有返回值,沒有參數的block int (^sumBlock)(int,int); void (^myBlock)()
-
2.如何使用block來封裝代碼
最基本的用法 int (^sumBlock)(int,int) = ^(int a,int b){ return a- b; }; 宏定義一個block typedef int (^MyBlock)(int, int); 利用宏定義來定義變量 MyBlock sumBlock; 定義一個block變量來實現兩個參數相加 sumBlock = ^(int a, int b) { return a + b; }; 定義一個block變量來實現兩個參數相減 MyBlock minusBlock = ^(int a, int b) { return a - b; }; 定義一個block變量來實現兩個參數相乘 MyBlock multiplyBlock = ^(int a, int b) { return a * b; };
-
3如何調用block
NSLog(@"%d - %d - %d", multiplyBlock(2, 4), sumBlock(10 , 9), minusBlock(10, 8)); 這個依次輸出是 8,19,2
-
4.block可以訪問外部變量
int a = 10; 給局部變量加上__block之后就可以改變b局部變量的值,將取變量此刻運行時的值 __block int b = 2; //定義一個block void (^block)(); block = ^{ 默認情況下,block內部不能修改外面的局部變量 a = 20; 給局部變量加上__block關鍵字,這個局部變量就可以在block內部修改 b = 25; }; block(); NSlog("%d,%d",a,b);
block基本理解
- 1.Block執行的代碼其實在編譯的時候就已經準備好了,就等著我們調用
- 2.一個包含Block執行時需要的所有外部變量值的數據結構。 Block將使用到的、作用域附近到的變量的值建立一份快照拷貝到棧上
blcok在內存中的分析
block內存中的三個位置 NSGlobalBlock,NSStackBlock, NSMallocBlock
NSGlobalBlock : 和函數類似,位于text代碼段
NSStackBlock : 棧內存,函數返回后Block將無效
-
NSMallocBlock : 堆內存
宏定義一個block typedef long (^BlockSum)(int, int); BlockSum block1 = ^ long(int a,int b){ return a + b ; }; //<__NSGlobalBlock__: 0x100001060> NSLog(@"%@",block1); int base = 100; BlockSum block2 = ^ long (int a,int b){ return base + a + b; }; //arc和非arc所在的內存位置不同 //mrc // <__NSStackBlock__: 0x7fff5fbff7e8>/ //arc //<__NSMallocBlock__: 0x10010deb0> NSLog(@"%@",block2); BlockSum block3 = [block2 copy]; //<__NSMallocBlock__: 0x10010deb0> NSLog(@"%@",block3);
上述中為什么block1在NSGlobalBlock中,block2在NSStackBlock(mrc),NSMallocBlock(arc)中
因為block用到了外部的變量base,需要建立局部變量的快照,所以在(定義,不是運行)局部變量被拷貝到棧上(mrc),堆(arc)
ObjectC int base = 2;
base + = 2;
BlockSum sum = ^ long (int a,int b){
return base + a + b;
}
base ++ ;
NSLog("%ld",sum(1,2));
``
分析上述代碼,因為有局部變量拷貝到棧里或者堆里,所以不會用運行時的變量base而是拷貝base所以
輸出的結果為 7,不是8
Block的copy,retain,release操作
- 對block retain操作并不會改變引用計數器,retainCount ,始終為1
- NSGlobalBlock:retain、copy、release操作都無效;
- Block_copy與copy等效,Block_release與release等效
- NSStackBlock:retain、release操作無效,必須注意的是,NSStackBlock在函數返回后,Block內存將被回收。即使retain也沒用。容易犯的 錯誤是[[mutableAarry addObject:stackBlock],在函數出棧后,從mutableAarry中取到的stackBlock已經被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然后加入數組:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock類型對象。
- NSMallocBlock支持retain、release,雖然retainCount始終是1,但內存管理器中仍然會增加、減少計數。copy之后不會生成新的對象,只是增加了一次引用,類似retain
- 盡量不要對Block使用retain操作
Block不同類型的變量
-
static 和基本數據類型
static int base = 100; int base = 100; BlockSum sum = ^ long (int a,int b){ return a + b + base; }; base = 0; NSLog(@"%ld\n",sum(1,2));
上述的類型如果是static的時候外部可以改變base變量,因為一直是一個內存地址,并沒有建立局部變量的快照,不是在定義時copy的常量
如果是基本類型的話會建立一個拷貝,不是同一個地址所以值不會改變
所以static輸出的是 3 ,基本數據類型是 103
-
static變量 如果block中也有變量的時候
static int base = 10; BlockSum sum = ^long (int a,int b){ base ++; return base + a + b; } base = 0; NSLog("%d\n,%ld\n,%d\n",base,sum(1,2),base);
這段代碼輸出的結果為,0,4,1,這段代碼說明block內部對外部static修飾的變量可以在內部進行修改,如果不加static或者block的會報錯
Block變量,被__block修飾的變量,稱作block變量, 基本類型的Block變量等效于全局變量、或靜態變量
Block被另一個Block使用時,另一個Block被copy到堆上時,被使用的Block也會被copy。但作為參數的Block是不會發生copy的
arc的block所有的都在堆上邊
-
mrc的看下邊的實例
int main(){ int base = 10; BlockSum block1 = ^ long(int a,int b){ return base + a + b; }; //<__NSStackBlock__: 0x7fff5fbff7f8> NSLog(@"%@",block1); bar(block1); return 0;
}
void bar(BlockSum block2){
// <__NSStackBlock__: 0x7fff5fbff7f8>
NSLog(@"%@",block2);
void (^block3) (BlockSum) = ^(BlockSum sum){
NSLog(@"%@",sum);
NSLog(@"%@",block2);
};
// <__NSStackBlock__: 0x7fff5fbff7f8>
// <__NSStackBlock__: 0x7fff5fbff7f8>
block3(block2);
block3 = [block3 copy];
// <__NSStackBlock__: 0x7fff5fbff7f8>
// <__NSMallocBlock__: 0x100206780>
block3(block2);
}
-
ObjC對象,不同于基本類型,Block會引起對象的引用計數變化
@interface MyClass : NSObject { NSObject* _instanceObj; } @end @implementation MyClass NSObject* __globalObj = nil; - (id) init { if (self = [super init]) { _instanceObj = [[NSObject alloc] init]; } return self; } - (void) test { static NSObject* __staticObj = nil; __globalObj = [[NSObject alloc] init]; __staticObj = [[NSObject alloc] init]; NSObject* localObj = [[NSObject alloc] init]; __block NSObject* blockObj = [[NSObject alloc] init]; typedef void (^MyBlock)(void) ; MyBlock aBlock = ^{ NSLog(@"%@", __globalObj); NSLog(@"%@", __staticObj); NSLog(@"%@", _instanceObj); NSLog(@"%@", localObj); NSLog(@"%@", blockObj); }; aBlock = [[aBlock copy] autorelease]; aBlock(); NSLog(@"%d", [__globalObj retainCount]); NSLog(@"%d", [__staticObj retainCount]); NSLog(@"%d", [_instanceObj retainCount]); NSLog(@"%d", [localObj retainCount]); NSLog(@"%d", [blockObj retainCount]); } @end int main(int argc, char *argv[]) { @autoreleasepool { MyClass* obj = [[[MyClass alloc] init] autorelease]; [obj test]; return 0; } }
執行結果為1 1 1 2 1。
__globalObj和__staticObj在內存中的位置是確定的,所以Block copy時不會retain對象。
_instanceObj在Block copy時也沒有直接retain _instanceObj對象本身,但會retain self。所以在Block中可以直接讀寫_instanceObj變量。
localObj在Block copy時,系統自動retain對象,增加其引用計數。
blockObj在Block copy時也不會retain。
非ObjC對象,如GCD隊列dispatch_queue_t。Block copy時并不會自動增加他的引用計數,這點要非常小心。
-
Block中使用的ObjC對象的行為
@property (nonatomic, copy) void(^myBlock)(void); MyClass* obj = [[[MyClass alloc] init] autorelease]; self.myBlock = ^ { [obj doSomething]; };
對象obj在Block被copy到堆上的時候自動retain了一次。因為Block不知道obj什么時候被釋放,為了不在Block使用obj前被釋放,Block retain了obj一次,在Block被釋放的時候,obj被release一次。
retain cycle(循環引用的問題)
retain cycle問題的根源在于Block和obj可能會互相強引用,互相retain對方,這樣就導致了retain cycle,最后這個Block和obj就變成了孤島,誰也釋放不了誰。比如:
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
NSString* string = [request responseString];
}];
在上邊這個實例中request和Block循環引用,所以我們只需要打斷其中的循環即可,
解決這個問題的辦法是使用弱引用打斷retain cycle:
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
NSString* string = [request responseString];
}];
request被持有者釋放后。request 的retainCount變成0,request被dealloc,request釋放持有的Block,導致Block的retainCount變成0,也被銷毀。這樣這兩個對象內存都被回收
與上面情況類似的是
//self和block循環引用解決辦法同上
self.myBlock = ^{
[self doSomething];
}
@property (nonatomic, retain) NSString* someVar;
self.myBlock = ^ {
NSLog(@"%@", _someVer);
};
NSString* str = _someVer;
self.myBlock = ^ {
NSLog(@"%@", str);
};
上述的循環引用是對象的屬性的話,retain會reatin對象,所以產生self和block的循環引用
-
retain cycle不只發生在兩個對象之間,也可能發生在多個對象之間,這樣問題更復雜,更難發現
ClassA* objA = [[[ClassA alloc] init] autorelease]; objA.myBlock = ^{ [self doSomething]; }; self.objA = objA;
解決辦法同樣是用__block打破循環引用
ClassA* objA = [[[ClassA alloc] init] autorelease];
MyClass* weakSelf = self;
objA.myBlock = ^{
[weakSelf doSomething];
};
self.objA = objA;
對上邊的進行分析 self(retain 1) ----> objA(retain 1) ---->Block (retain 1)---->self 循環引用
- 注意:MRC中__block是不會引起retain;但在ARC中__block則會引起retain。ARC中應該使用__weak或__unsafe_unretained弱引用。__weak只能在iOS5以后使用。
block對象被提前釋放
看下面例子,有這種情況,如果不只是request持有了Block,另一個對象也持有了Block(下邊的等號是一條虛線一條實線,block指向request 的是虛線)
--->request =======>Block<---- ObjA
這時request已被完全釋放,但Block仍被objA持有,沒有釋放,如果這時觸發了Block,在Block中將訪問已經銷毀的request,這將導致程序crash。為了避免這種情況,開發者必須要注意對象和Block的生命周期。
另一個常見錯誤使用是,開發者擔心retain cycle錯誤的使用__block。比如
__block kkProducView* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.xx = xx;
});
將Block作為參數傳給dispatch_async時,系統會將Block拷貝到堆上,如果Block中使用了實例變量,還將retain self,因為dispatch_async并不知道self會在什么時候被釋放,為了確保系統調度執行Block中的任務時self沒有被意外釋放掉,dispatch_async必須自己retain一次self,任務完成后再release self。但這里使用__block,使dispatch_async沒有增加self的引用計數,這使得在系統在調度執行Block之前,self可能已被銷毀,但系統并不知道這個情況,導致Block被調度執行時self已經被釋放導致crash。
// MyClass.m
- (void) test {
__block MyClass* weakSelf = self;
double delayInSeconds = 10.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(@"%@", weakSelf);
});
// other.m
MyClass* obj = [[[MyClass alloc] init] autorelease];
[obj test];
這里用dispatch_after模擬了一個異步任務,10秒后執行Block。但執行Block的時候MyClass* obj已經被釋放了,導致crash。解決辦法是不要使用__block。
大部分都是借鑒轉載自:tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/