[深入淺出Cocoa]Block編程值得注意的那些事兒

[深入淺出Cocoa]Block編程值得注意的那些事兒

羅朝輝 (http://www.cnblogs.com/kesalin/)

一,前言

在前文《深入淺出Cocoa多線程編程之block與dispatch quene》中我介紹了 block 的一些基本語法以及如何和 GCD 結合的使用示例。block 是在 iOS 4 中引入的新特性,它和 C++ 11 中的 lamba 表達式概念相似,有時候也被稱爲閉包。經過一段時間的使用,我發現要用對用好 block 還是有不少需要注意的地方,今天就來八一八這些值得注意的事兒。

 本文源碼下載:點此下載

 

二,block 注意事項

1,block 在實現時就會對它引用到的它所在方法中定義的棧變量進行一次只讀拷貝,然後在 block 塊內使用該只讀拷貝。

如下代碼:

複製代碼
- (void)testAccessVariable
{
    NSInteger outsideVariable = 10;
    //__block NSInteger outsideVariable = 10;
    NSMutableArray * outsideArray = [[NSMutableArray alloc] init];
    
    void (^blockObject)(void) = ^(void){
        NSInteger insideVariable = 20;
        KSLog(@"  > member variable = %d", self.memberVariable);
        KSLog(@"  > outside variable = %d", outsideVariable);
        KSLog(@"  > inside variable = %d", insideVariable);
        
        [outsideArray addObject:@"AddedInsideBlock"];
    };
    
    outsideVariable = 30;
    self.memberVariable = 30;

    blockObject();
    
    KSLog(@"  > %d items in outsideArray", [outsideArray count]);
}
複製代碼

輸出結果爲:

  > member variable = 30
  > outside variable = 10
  > inside variable = 20
  > 1 items in outsideArray

注意到沒?outside 變量的輸出值爲10,雖然outside變量在定義 block 之後在定義 block 所在的方法 testAccessVariable 中被修改爲 20 了。這裏的規則就是:blockObject 在實現時會對 outside 變量進行只讀拷貝,在 block 塊內使用該只讀拷貝。因此這裏輸出的是拷貝時的變量值 10。如果,我們想要讓 blockObject 修改或同步使用 outside 變量就需要用 __block 來修飾 outside 變量。

__block NSInteger outsideVariable = 10;

 注意:

a),在上面的 block 中,我們往 outsideArray 數組中添加了值,但並未修改 outsideArray 自身,這是允許的,因爲拷貝的是 outsideArray 自身。 

b),對於 static 變量,全局變量,在 block 中是有讀寫權限的,因爲在 block 的內部實現中,拷貝的是指向這些變量的指針。

c), __block 變量的內部實現要複雜許多,__block 變量其實是一個結構體對象,拷貝的是指向該結構體對象的指針。

 

2,非內聯(inline) block 不能直接訪問 self,只能通過將 self 當作參數傳遞到 block 中才能使用,並且此時的 self 只能通過 setter 或 getter 方法訪問其屬性,不能使用句點式方法。但內聯 block 不受此限制。

複製代碼
typedef NSString* (^IntToStringConverter)(id self, NSInteger paramInteger);
- (NSString *) convertIntToString:(NSInteger)paramInteger
                 usingBlockObject:(IntToStringConverter)paramBlockObject
{
    return paramBlockObject(self, paramInteger);
}

typedef NSString* (^IntToStringInlineConverter)(NSInteger paramInteger);
- (NSString *) convertIntToStringInline:(NSInteger)paramInteger
                 usingBlockObject:(IntToStringInlineConverter)paramBlockObject
{
    return paramBlockObject(paramInteger);
}

IntToStringConverter independentBlockObject = ^(id self, NSInteger paramInteger) {
    KSLog(@" >> self %@, memberVariable %d", self, [self memberVariable]);
    
    NSString *result = [NSString stringWithFormat:@"%d", paramInteger];
    KSLog(@" >> independentBlockObject %@", result);
    return result;
};

- (void)testAccessSelf
{
    // Independent
    //
    [self convertIntToString:20 usingBlockObject:independentBlockObject];
    
    // Inline
    //
    IntToStringInlineConverter inlineBlockObject = ^(NSInteger paramInteger) {
        KSLog(@" >> self %@, memberVariable %d", self, self.memberVariable);
        
        NSString *result = [NSString stringWithFormat:@"%d", paramInteger];
        KSLog(@" >> inlineBlockObject %@", result);
        return result;
    };
    [self convertIntToStringInline:20 usingBlockObject:inlineBlockObject];
}
複製代碼

 

3,使用 weak–strong dance 技術來避免循環引用

在第二條中,我提到內聯 block 可以直接引用 self,但是要非常小心地在 block 中引用 self。因爲在一些內聯 block 引用 self,可能會導致循環引用。如下例所示:

複製代碼
@interface KSViewController ()
{
    id _observer;
}

@end

@implementation KSViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    KSTester * tester = [[KSTester alloc] init];
    [tester run];
    
    _observer = [[NSNotificationCenter defaultCenter]
                 addObserverForName:@"TestNotificationKey"
                 object:nil queue:nil usingBlock:^(NSNotification *n) {
                     NSLog(@"%@", self);
                 }];
}

- (void)dealloc
{
    if (_observer) {
        [[NSNotificationCenter defaultCenter] removeObserver:_observer];
    }
}
複製代碼

在上面代碼中,我們添加向通知中心註冊了一個觀察者,然後在 dealloc 時解除該註冊,一切看起來正常。但這裏有兩個問題:

a) 在消息通知 block 中引用到了 self,在這裏 self 對象被 block retain,而 _observer 又 retain 該 block的一份拷貝,通知中心又持有 _observer。因此只要 _observer 對象還沒有被解除註冊,block 就會一直被通知中心持有,從而 self 就不會被釋放,其 dealloc 就不會被調用。而我們卻又期望在 dealloc 中通過 removeObserver 來解除註冊以消除通知中心對 _observer/block 的 retain。

b) 同時,_observer 是在 self 所在類中定義賦值,因此是被 self retain 的,這樣就形成了循環引用。

上面的過程 a) 值得深入分析一下:

蘋果官方文檔中對 addObserverForName:object:queue:usingBlock: 中的 block 變量說明如下:

The block is copied by the notification center and (the copy) held until the observer registration is removed.

因此,通知中心會拷貝 block 並持有該拷貝直到解除 _observer 的註冊。在 ARC 中,在被拷貝的 block 中無論是直接引用 self 還是通過引用 self 的成員變量間接引用 self,該 block 都會 retain self。

這兩個問題,可以用 weak–strong dance 技術來解決。該技術在 WWDC 中介紹過:2011 WWDC Session #322 (Objective-C Advancements in Depth)

複製代碼
    __weak KSViewController * wself = self;
    _observer = [[NSNotificationCenter defaultCenter]
                 addObserverForName:@"TestNotificationKey"
                 object:nil queue:nil usingBlock:^(NSNotification *n) {
                     KSViewController * sself = wself;
                     if (sself) {
                         NSLog(@"%@", sself);
                     }
                     else {
                         NSLog(@"<self> dealloc before we could run this code.");
                     }
                 }];
複製代碼

下面來分析爲什麼該手法能夠起作用。

首先,在 block 之前定義對 self 的一個弱引用 wself,因爲是弱引用,所以當 self 被釋放時 wself 會變爲 nil;然後在 block 中引用該弱應用,考慮到多線程情況,通過使用強引用 sself 來引用該弱引用,這時如果 self 不爲 nil 就會 retain self,以防止在後面的使用過程中 self 被釋放;然後在之後的 block 塊中使用該強引用 sself,注意在使用前要對 sself 進行了 nil 檢測,因爲多線程環境下在用弱引用 wself 對強引用 sself 賦值時,弱引用 wself 可能已經爲 nil 了。

通過這種手法,block 就不會持有 self 的引用,從而打破了循環引用。

 

擴展:其他還需要注意避免循環引用的地方

與此類似的情況還有 NSTimer。蘋果官方文檔中提到"Note in particular that run loops retain their timers, so you can release a timer after you have added it to a run loop.",同時在對接口

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

的 target 說明文檔中提到:

The object to which to send the message specified by aSelector when the timer fires. The target object is retained by the timer and released when the timer is invalidated.

結合這兩處文檔說明,我們就知道只要重複性 timer 還沒有被 invalidated,target 對象就會被一直持有而不會被釋放。因此當你使用 self 當作 target 時,你就不能期望在 dealloc 中 invalidate timer,因爲在 timer 沒有被invalidate 之前,dealloc 絕不會被調用。因此,需要找個合適的時機和地方來 invalidate timer,但絕不是在 dealloc 中。 

 

4,block 內存管理分析

block 其實也是一個 NSObject 對象,並且在大多數情況下,block 是分配在棧上面的,只有當 block 被定義爲全局變量或 block 塊中沒有引用任何 automatic 變量時,block 才分配在全局數據段上。 __block 變量也是分配在棧上面的。

在 ARC 下,編譯器會自動檢測爲我們處理了 block 的大部分內存管理,但當將 block 當作方法參數時候,編譯器不會自動檢測,需要我們手動拷貝該 block 對象。幸運的是,Cocoa 庫中的大部分名稱中包含”usingBlock“的接口以及 GCD 接口在其接口內部已經進行了拷貝操作,不需要我們再手動處理了。但除此之外的情況,就需要我們手動干預了。

複製代碼
- (id) getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{ KSLog(@"  > block 0:%d", val); },    // block on the stack
            ^{ KSLog(@"  > block 1:%d", val); },    // block on the stack
            nil];
    
//    return [[NSArray alloc] initWithObjects:
//            [^{ KSLog(@"  > block 0:%d", val); } copy],    // block copy to heap
//            [^{ KSLog(@"  > block 1:%d", val); } copy],    // block copy to heap
//            nil];
}

- (void)testManageBlockMemory
{
    id obj = [self getBlockArray];
    typedef void (^BlockType)(void);
    BlockType blockObject = (BlockType)[obj objectAtIndex:0];
    blockObject();
}
複製代碼

執行上面的代碼中,在調用 testManageBlockMemory 時,程序會 crash 掉。因爲從 getBlockArray 返回的 block 是分配在 stack 上的,但超出了定義 block 所在的作用域,block 就不在了。正確的做法(被屏蔽的那段代碼)是在將 block 添加到 NSArray 中時先 copy 到 heap 上,這樣就可以在之後的使用中正常訪問。

在 ARC 下,對 block 變量進行 copy 始終是安全的,無論它是在棧上,還是全局數據段,還是已經拷貝到堆上。對棧上的 block 進行 copy 是將它拷貝到堆上;對全局數據段中的 block 進行 copy 不會有任何作用;對堆上的 block 進行 copy 只是增加它的引用記數。

如果棧上的 block 中引用了__block 類型的變量,在將該 block 拷貝到堆上時也會將 __block 變量拷貝到堆上如果該 __block 變量在堆上還沒有對應的拷貝的話,否則就增加堆上對應的拷貝的引用記數。

 

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