2011年8月29日 星期一

探討Objective-C Block (part 3)

前面講了簡單的block用法跟block variable,
但是block難的地方應該就是記憶體管理的部份,
如果你不是很瞭解block內部的記憶體管理,
很容易一個不小心就導致circular reference而導致memory leakage..

前篇有提到,我們可以把block當作參數傳給function或是message,
但是傳進去後,有可能這個function會想把你的block pointer留下來
最常見的就是做event handling的例子,
我們把一個事件觸發的block當作參數丟進來,
但是是件觸發可能是數秒之後的事情,
而此時註event handler的function/message已經return了,
那此block所reference的local變數可能已經invalid了,
這時候block要怎麼處理這樣的情形呢?

首先,在block的定義中,此block還停留在call stack之中,
也就是他的生命週期會隨著定義此block的function return之後,其生命週期就會結束
除非我們呼叫block_copy或是[myblock copy]
此時block才會從stack變到heap中。
之後我們才可以把參數傳過來的block指給instance variable或是global variable,
而block中所用到的物件在此同時reference count也會+1。
但reference count +1這件事情卻在每一種case不一樣
因為block內部可以使用環境中看的到local, block, instance, local static, global variable
那copy這個動作會發生什麼事情呢?
我們先寫一個範例code

//MyBlockTest.h
#import 

typedef void (^myBlockTest_type)(void);

@interface MyBlockTest : NSObject {
    NSObject*           instanceRef;
    myBlockTest_type    myBlock;
}

- (void) test;

@end


//MyBlockTest.m
#import "MyBlockTest.h"
@implementation MyBlockTest

static NSObject* globalRef;

+(void) initialize
{
    globalRef = [NSObject new];
}

- (id)init
{
    self = [super init];
    if(self)
    {
        instanceRef = [NSObject new];
    }
    return self;
}

- (void) test
{
    // Local variable
    NSObject* localRef = [NSObject new];
   
    // Block variable
    __block NSObject* blockRef = [NSObject new];
   
    // Local static variable
    static NSObject* localStaticRef;   
    if(!localStaticRef) localStaticRef = [NSObject new];
   
    // create a block
    myBlockTest_type aBlock =
    ^{
        NSLog(@"%@", localRef);
        NSLog(@"%@", blockRef);
        NSLog(@"%@", instanceRef);
        NSLog(@"%@", globalRef);
        NSLog(@"%@", localStaticRef);
    };
   
    //copy the block
    myBlock  = [aBlock copy];
   
    NSLog(@"%d", [localRef retainCount]);
    NSLog(@"%d", [blockRef retainCount]);
    NSLog(@"%d", [instanceRef retainCount]);
    NSLog(@"%d", [globalRef retainCount]);
    NSLog(@"%d", [localStaticRef retainCount]);
   
    [localRef release];
}

@end


大家可以先想想看,當呼叫test的時候,會印出什麼樣的結果?
正確的答案是
2 1 1 1 1
不知道你答對了沒?

第一個localRef應該最能夠理解,基本上就是+1,這個就是這樣設計。

第二個 blockRef,由前面一張對block variable的解釋,
我們可以知道block variable是一個closure用一份。
因此此block variable並沒有額外的retain的動作。
所以被block variable指到的物件也不會有reference count +1的情況。

第三個instanceRef為什麼沒有+1呢?
事實上這個問題也是挺有陷阱題的味道。
對block來講,他看到的是self這個變數,而非instanceRef。
所以ref. count +1的不是instanceRef而是self。
如果在block copy的前後各把self的ref count印出來你就可以佐證這個事實了。

第四個globalRef跟第五個localStaticRef本質上很像,所以兩個可以一起討論。
由於這兩個變數在runtime中的位置是固定而且唯一的,
所以基本上在block內用上面兩個變數跟block沒有什麼兩樣。
因此block copy並不會也不需要增加ref. count的數目。

瞭解之後,那什麼時候可能會出現circular reference呢?
其實跟我們之前聊到的ios delegate你必須知道的事情所說的內容很像。
只是這次主角從delegate換成block。
試想,如果有3個view controller,分別是VC1, VC2, VC3
如果VC1產生並retain VC2
VC2也產生VC3
而且VC2可能跟VC3註冊了一個event handler並且參數是用一個block。
在這個block中可能長這樣。
[vc3 setOnClose:^{
        [self dismissModalViewControllerAnimated:YES];
    }];
那這樣會發生什麼是情呢?
答案是當VC1 release VC2的時候,
VC2因為自己有參照VC3,所以VC3的retain count還是1
VC3因為他的instance variable有retain這個block
而這個block因為用到block中的self
這個self就是VC2,
那這樣可糟了個糕,circular的悲劇就產生了。

目前官方文件告訴我們要這樣做
__block VC2* tempVC2 = self;

    [vc3 setOnClose:^{
        [tempVC2 dismissModalViewControllerAnimated:YES];
    }];
我們透過block variable不會retain的特性,
來把self丟給tempVC2,
如此在block在被copy的時候不會增加retain count。
我只能說太不friendly了,
不過目前好像也只有這樣解,而且到了ARC之後這個問題還是存在。
所以大家一定要改清楚block的memory management,
才不會不知道為什麼,reference count永遠不會歸零的狀況。

探討Objective-C Block (part 1) - block的使用
探討Objective-C Block (part 2) - block變數
探討Objective-C Block (part 3) - block的記憶體管理

1 則留言:

  1. 你能讲讲block的嵌套应用时变量的引用情况么, 以及下面的这种情况
    [vc3 setOnClose:^{
    [tempVC2 dismissModalViewControllerAnimated:YES];
    }];
    这种情况经常看到不对传入的block进行copy操作, 而且也能执行回调, 不会出错, 我看到一些第三方开源库(AFNetworking等)中没有copy, 当然也有部分copy了, 所以有些不解

    我的邮件:willonboyzhang$gmail.com

    回覆刪除