Block---注意事項

參考:

http://www.devtang.com/blog/2013/07/28/a-look-inside-blocks/

http://blog.csdn.net/wildfireli/article/details/21979955#t2

http://www.tuicool.com/articles/i6Bfee

http://blog.csdn.net/fengsh998/article/details/38090205

http://www.tekuba.net/program/349/


Block學習

可以先看看 cocoachina提供的block測試題你真的知道blocks在Objective-C中是怎麼工作的嗎


Block在內存中的位置


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

  • NSGlobalBlock:類似函數,位於text段;
  • NSStackBlock:位於棧內存,函數返回後Block將無效;
  • NSMallocBlock:位於堆內存
BlkSum blk1 = ^ long (int a, int b) {  
    return a + b;
};
NSLog(@"blk1 = %@", blk1);// blk1 = <__NSGlobalBlock__: 0x47d0>
 
int base = 100;
BlkSum blk2 = ^ long (int a, int b) {  
    return base + a + b;
};
NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>
 
BlkSum blk3 = [[blk2 copy] autorelease];
NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>

爲什麼blk1類型是NSGlobalBlock,而blk2類型是NSStackBlock?blk1和blk2的區別在於,blk1沒有使用Block以外的任何外部變量,Block不需要建立局部變量值的快照,這使blk1與函數沒有任何區別,從blk1所在內存地址0x47d0猜測編譯器把blk1放到了text代碼段。blk2與blk1唯一不同是的使用了局部變量base,在定義(注意是定義,不是運行)blk2時,局部變量base當前值被copy到棧上,作爲常量供Block使用。執行下面代碼,結果是203,而不是204。

int base = 100;  
base += 100;  
BlkSum sum = ^ long (int a, int b) {    
    return base + a + b;  
};  
base++;  
printf("%ld",sum(1,2));
在Block內變量base是隻讀的,如果想在Block內改變base的值,在定義base時要用 __block修飾:__block int base = 100;。
__block int base = 100;  
base += 100;  
BlkSum sum = ^ long (int a, int b) {    
    base += 10;    
    return base + a + b;  
};  
base++;  
printf("%ld\n",sum(1,2));  
printf("%d\n",base);
輸出將是214,211。Block中使用__block修飾的變量時,將取變量此刻運行時的值,而不是定義時的快照。這個例子中,執行sum(1,2)時,base將取base++之後的值,也就是201,再執行Blockbase+=10; base+a+b,運行結果是214。執行完Block時,base已經變成211了。

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],在函數出棧後,從mutableAarry中取到的stackBlock已經被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然後加入數組:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之後生成新的NSMallocBlock類型對象。

● NSMallocBlock支持retain、release,雖然retainCount始終是1,但內存管理器中仍然會增加、減少計數。copy之後不會生成新的對象,只是增加了一次引用,類似retain;

● 儘量不要對Block使用retain操作。


Block對不同類型的變量的存取

基本類型

局部自動變量,

在Block中只讀。Block定義時copy變量的值,在Block中作爲常量使用,所以即使變量的值在Block外改變,也不影響他在Block中的值。

<span style="font-family:SimSun;">int base = 100;
BlkSum sum = ^ long (int a, int b) {  
    // base++; 編譯錯誤,只讀  
    return base + a + b;
};
base = 0;
printf("%ld\n",sum(1,2)); // 這裏輸出是103,而不是3</span>

 static變量、全局變量

如果把上個例子的base改成全局的、或static。Block就可以對他進行讀寫了。因爲全局變量或靜態變量在內存中的地址是固定的,Block在讀取該變量值的時候是直接從其所在內存讀出,獲取到的是最新值,而不是在定義時copy的常量。

static int base = 100;
BlkSum sum = ^ long (int a, int b) {  
    base++;  
    return base + a + b;
};
base = 0;
printf("%d\n", base);
printf("%ld\n",sum(1,2)); // 這裏輸出是3,而不是103
printf("%d\n", base);
輸出結果是0 4 1,表明Block外部對base的更新會影響Block中的base的取值,同樣Block對base的更新也會影響Block外部的base值。


Block變量,被__block修飾的變量稱作Block變量。 基本類型的Block變量等效於全局變量、或靜態變量。

Block被另一個Block使用時,另一個Block被copy到堆上時,被使用的Block也會被copy。但作爲參數的Block是不會發生copy的。

void foo() {  
    int base = 100;  
    BlkSum blk = ^ long (int a, int b) {    
        return  base + a + b;  
    };  
    NSLog(@"%@", blk); // <__NSStackBlock__: 0xbfffdb40>  
    bar(blk);
}
 
void bar(BlkSum sum_blk) {  
    NSLog(@"%@",sum_blk); // 與上面一樣,說明作爲參數傳遞時,並不會發生copy  
    void (^blk) (BlkSum) = ^ (BlkSum sum) {    
        NSLog(@"%@",sum);     // 無論blk在堆上還是棧上,作爲參數的Block不會發生copy。    
        NSLog(@"%@",sum_blk); // 當blk copy到堆上時,sum_blk也被copy了一分到堆上上。  
    };  
    blk(sum_blk); // blk在棧上  
     
    blk = [[blk copy] autorelease];  
    blk(sum_blk); // blk在堆上
}

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對象的行爲


1
2
3
4
5
6
@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一次。

Block在ARC和非ARC的區別

配置在棧上的Block也就是NSConcreteStackBlock類型的Block,如果其所屬的變量作用域結束該Block就會廢棄
。這個時候如果繼續使用該Block,就應該使用copy方法,將NSConcreteStackBlock拷貝爲_NSConcreteMallo
cBlock。當_NSConcreteMallocBlock的引用計數變爲0,該_NSConcreteMallocBlock就會被釋放。
如果是非ARC環境,需要顯式的執行copy或者antorelease方法。
而當ARC有效的時候,實際上大部分情況下編譯器已經爲我們做好了,自動的將Block從棧上覆制到堆上。包括以
下幾個情況:
1,Block作爲返回值時
類似在非ARC的時候,對返回值Block執行[[returnedBlock copy] autorelease];
將Block賦值給附有__strong修飾符的id類型的類或者Blcok類型成員變量時 
3,Cocoa框架中方法名中還有useringBlock等時
4,GCD相關的一系列API傳遞Block時。

5. 被執行copy方法
比如:[mutableAarry addObject:stackBlock];這段代碼在非ARC環境下肯定有問題,而在ARC環境下方法參數
中傳遞NSConcreteStackBlock會自動執行copy。

例子說明

    void exampleB_addBlockToArray(NSMutableArray *array) {
        char b = 'B';
        [array addObject:^{
            printf("%c\n", b);
        }];
    }
    void exampleB() {
        NSMutableArray *array = [NSMutableArray array];
        exampleB_addBlockToArray(array);
        void (^block)() = [array objectAtIndex:0];
        block();
    }
    //Test2
    void exampleC_addBlockToArray(NSMutableArray *array) {
        [array addObject:^{
            printf("C\n");
        }];
    }
    void exampleC() {
        NSMutableArray *array = [NSMutableArray array];
        exampleC_addBlockToArray(array);
        void (^block)() = [array objectAtIndex:0];
        block();
    }
    //Test3
    typedef void (^dBlock)();
    dBlock exampleD_getBlock() {
        char d = 'D';
        return ^{
            printf("%c\n", d);
        };
    }
    void exampleD() {
        exampleD_getBlock()();
    }

Test1:
exampleB_addBlockToArray添加的Block爲NSConcreteStackBlock,在非ARC環境下,執行該Block時,棧
上的數據已經被清除了,錯誤。而在ARC環境下,作爲參數的Block會自動copy,不會出現問題;對應是五(2)情
況。
Test2:
exampleC_addBlockToArray添加的Block爲_NSConcreteGlobalBlock,存儲在程序的數據區,相當於一個不
使用外部環境的函數,沒有用到棧上的數據,所有無論是ARC還是非ARC都能正常執行。
Test3
ARC中作爲返回值的NSConcreteStackBlock會被執行copy操作。所有ARC運行正常,非ARC錯誤。對應是五(1)
情況。

Block的循環引用

retain cycle問題的根源在於Block和obj可能會互相強引用,互相retain對方,這樣就導致了retain cycle,最後這個Block和obj就變成了孤島,誰也釋放不了誰。比如:

ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{  
    NSString* string = [request responseString];
}];

解決這個問題的辦法是使用弱引用打斷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.myBlock = ^ {  
    [self doSomething];
};

這裏self和myBlock循環引用,解決辦法同上:

__block MyClass* weakSelf = self;
self.myBlock = ^ {  
    [weakSelf doSomething];
};
@property (nonatomic, retain) NSString* someVar;
 
self.myBlock = ^ {  
    NSLog(@"%@", _someVer);
};

這裏在Block中雖然沒直接使用self,但使用了成員變量。在Block中使用成員變量,retain的不是這個變量,而會retain self。解決辦法也和上面一樣。

@property (nonatomic, retain) NSString* someVar;
 
__block MyClass* weakSelf = self;
self.myBlock = ^ {  
    NSLog(@"%@", self.someVer);
};

或者

NSString* str = _someVer;
self.myBlock = ^ {  
    NSLog(@"%@", str);
};
retain cycle不只發生在兩個對象之間,也可能發生在多個對象之間,這樣問題更復雜,更難發現

ClassA* objA = [[[ClassA alloc] init] autorelease];  
objA.myBlock = ^{    
    [self doSomething];  
};  
self.objA = objA;

解決辦法同樣是用__block打破循環引用

ClassA* objA = [[[ClassA alloc] init] autorelease];
<span style="color: rgb(20, 25, 30); font-family: SimSun; font-size: 14px; line-height: 28px;">__block </span>MyClass* weakSelf = self;
objA.myBlock = ^{  
    [weakSelf doSomething];
};
self.objA = objA;
注意:MRC中__block是不會引起retain;但在ARC中__block則會引起retain。ARC中應該使用__weak或__unsafe_unretained弱引用。__weak只能在iOS5以後使用。


下面演示一下用weak的

像上面的寫法其實在ARC中會產生(Capturing 'demo' strongly in this block is likely to lead to a retain cycle)告警。如下圖:


在ARC中,編譯器智能化了,直接提示這樣寫會產生循環引用。因此很多愛去除告警的朋友就會想法去掉,好,咱再來看去掉時需注意的問題。

情況一:

- (IBAction)onTest:(id)sender
{
    __weak BlockDemo *demo = [[BlockDemo alloc]init];
    [demo setExecuteFinished:^{
        if (demo.resultCode == 200) {
            NSLog(@"call back ok.");
        }
    }];
    [demo executeTest];
}

直接在前面加一個__weak,但這樣真的沒有告警了嗎?如果有,哪麼恭喜歡你,說明編譯器還幫你大忙。見下圖



這時還會告警,說這是一個WEAK變量,就馬上會被release。因此就不會執行block中的內容。大家可以運行一下看

輸出結果爲:

2014-07-24 19:38:02.453 blockDemo[25305:60b] Object Constructor!
2014-07-24 19:38:02.454 blockDemo[25305:60b] Object Destoryed!

很顯然,馬上被release了,所以block 中的代碼根本就不執行。

謝天謝地,幸好編譯器提前告訴了我們有這個隱性危險。相信大家爲解決告警,又會得到一個比較圓滿的解決方案,見下:

- (IBAction)onTest:(id)sender
{
    BlockDemo *demo = [[BlockDemo alloc]init];
    __weak typeof(BlockDemo) *weakDemo = demo;
    [demo setExecuteFinished:^{
        if (weakDemo.resultCode == 200) {
            NSLog(@"call back ok.");
        }
    }];
    [demo executeTest];
}

這樣寫,即去除了告警又保證了block的運行。這纔是我們最終想要的結果。
輸出爲:

  1. 2014-07-24 19:40:33.204 blockDemo[25328:60b] Object Constructor!  
  2. 2014-07-24 19:40:38.206 blockDemo[25328:60b] call back ok.  
  3. 2014-07-24 19:40:38.207 blockDemo[25328:60b] Object Destoryed!  

但大家別得意。有提示,相信大家都能處理,並得到個好的解決方法。哪麼下面大來再來看一下這個寫法,讓你真心甘拜下風。。。。。

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. - (IBAction)onTest:(id)sender  
  2. {  
  3.     __weak BlockDemo *demo = [BlockDemo blockdemo]; //這裏纔是重點,前面是[[BlockDemo alloc]init];會有告警。  
  4.       
  5.     [demo setExecuteFinished:^{  
  6.         if (demo.resultCode == 200) {  
  7.             NSLog(@"call back ok.");  
  8.         }  
  9.     }];  
  10.     [demo executeTest];  
  11. }  


其實只是把init放到了類方法中進行書寫而已,但會有什麼不同。
[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. + (BlockDemo *)blockdemo  
  2. {  
  3.     return OBJC_AUTORELEASE([[BlockDemo alloc]init]);  
  4. }  
不同點見下圖:真心看不到作何告警,是不是。但這存在什麼風險,風險就是運行的時候,block根本就沒有run。因爲對象早就釋放了。


直接輸出:

  1. 2014-07-24 19:47:53.033 blockDemo[25395:60b] Object Constructor!  
  2. 2014-07-24 19:47:53.035 blockDemo[25395:60b] Object Destoryed!  

因此,寫這個主要用來告戒一些喜歡用BLOCK但又想當然的朋友,有一些朋友喜歡去除告警,但只是盲目的加上__weak 或__block關鍵語,往往可能存在一些重大的安全隱患。就像演示中block根本不走。如果到了發佈時,爲了去告警而這樣簡單的處理了,並沒有進行測試就打包。哪麼將死得很慘。。。。。


block會不會引行死循環,我說不會的理由。

見碼:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. - (IBAction)onTest:(id)sender  
  2. {  
  3.     BlockDemo *demo = [BlockDemo blockdemo];//[[BlockDemo alloc]init];  
  4.       
  5.     [demo setExecuteFinishedParam:^(BlockDemo * ademo) {  
  6.         if (ademo.resultCode == 200) {  
  7.             NSLog(@"call back ok.");  
  8.         }  
  9.     }];  
  10.       
  11.     [demo executeTest];  
  12. }  

不管是在外面init,還是在裏面,且沒有加__block 及__weak。爲什麼,因爲我個人常常在使用自己寫的block時,如果是回調,比較喜歡把自身當作參數傳到block中。這樣期實是編譯器給我們做了弱引用。因此不會產生循環引用。


Block使用對象被提前釋放

看下面例子,有這種情況,如果不只是request持有了Block,另一個對象也持有了Block。

這時如果request 被持有者釋放。

這時request已被完全釋放,但Block仍被objA持有,沒有釋放,如果這時觸發了Block,在Block中將訪問已經銷燬的request,這將導致程序crash。爲了避免這種情況,開發者必須要注意對象和Block的生命週期。

// 有待考證

另一個常見錯誤使用是,開發者擔心retain cycle錯誤的使用__block。比如

<del>__block kkProducView* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{  
    weakSelf.xx = xx;
});</del>

將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。

<del>// 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];</del>

這裏用dispatch_after模擬了一個異步任務,10秒後執行Block。但執行Block的時候MyClass* obj已經被釋放了,導致crash。解決辦法是不要使用__block。


__block、__weak等修飾符

AIn manual reference counting mode, __block id x; has the effect of not retaining x. In ARC mode, __block id x; defaults to retaining x (just like all other values). To get the manual reference counting mode behavior under ARC, you could use __unsafe_unretained __block id x;. As the name __unsafe_unretained implies, however, having a non-retained variable is dangerous (because it can dangle) and is therefore discouraged. Two better options are to either use __weak (if you don’t need to support iOS 4 or OS X v10.6), or set the __block value to nil to break the retain cycle.

在MRC下,我們通常使用__block ,而在ARC下我們通常使用__weak , 或者__unsafe_unretaine(不安全,不建議使用) 來修飾對象防止循環引用而造成的內存泄露

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章