[深入淺出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 變量在堆上還沒有對應的拷貝的話,否則就增加堆上對應的拷貝的引用記數。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM