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的記憶體管理

探討Objective-C Block (part 2)

原本只打算寫兩篇的,但是忽然覺得很多東西可以講。
而且原本在這篇就要講的有關記憶體的問題,
我選擇了往後擱著
我們來先探討block variable,也就是__block這個修飾字。

前一篇有提到,block在別的語言叫做closure,
這個closure的概念很像是把環境閉鎖起來,
而這個環境就是指定義該block的這個call stack frame。
當block被呼叫block_copy的時候,或是[someBlock copy]的時候
這個block就會進入heap,
並且會指到目前的stack frame形成closure。
而block variable會跟這個closure綁住,
所以可能說closure variable會更加的貼切。

我們來看下面一個有趣的例子
typedef NSUInteger (^countdown_type)(void);
countdown_type createCountdown(NSUInteger number)
{
    __block NSUInteger counter = number;
   
    return [[^NSUInteger{
        return counter--;
    } copy] autorelease];
}
這個function產生了一個block,這個block中有一個block variable,
起始值是由傳進來的參數決定。
之後每呼叫一次counter都會減1,並且把原本的值傳回去。
下面使用的範例
countdown = createCountdown(10);
    NSLog(@"%d", countdown()); //10
    NSLog(@"%d", countdown()); //9
    NSLog(@"%d", countdown()); //8

所以我們更能清楚知道local variable跟block variable的差異。
local variable隨著function回傳而該變數的位置就隨著call stack pop掉。
而如果local static variable跟block variable的比較
static local整個app只有一份。
但是block variable是一個closure一份,
所以以這個例子,如果我們用的是local static,則所有的countdown都共用一個counter。
而如果是用block variable,則每個block都各自有自己的一份。

如果你比較瞭解了block variable的定義,
你可以在回頭想想為什麼在block中local variable只能當常數使用,
而block variable可以當變數使用。
相信你心中已經有答案了 :D

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

2011年8月24日 星期三

探討Objective-C Block (part 1)

在ios4推出後,出現了一個新的語言功能block。這個功能其實有接觸多種程式語言的話,會知道其實這個功能不算是Objctive-C特有的
別的語言可能叫做Lambda或是Closure,
這東西主要的特性是他除了是function外,但是更紀錄了此function外部的環境。
也許有點抽象,我先舉個javascript的例子
var i = 2;
    var func = function(){ i = i * i;};
    func();  // than i = 4
    func();  // than i = 16
我們在中間定義一個function並且指派到func這個變數,
而在這邊可以用i這個外面的變數,因為在javascript中這樣的定義的就是一個closure,
他會記住外面有i這個變數,
所以之後我們連續呼叫兩次func,
則可以得到i是16這個結果
這個跟objective-c的block,或是別的語言的lambda,closure都是一樣的東西
相關的資料可以參考Clousre - wikipedia 裡面的介紹。

回到objective-c的block,當然這個好物當然要好好的利用一下,
這個東西最吸引人的地方就是可以帶不定數量的變數到function當中,因為你只要環境中可以取得的都可以在function中使用
並且因為block(或是其他語言的closure)通常使用上都是anonymous function,直接藏身在你的code當中,因此可以讓程式碼更加精簡。
但是我覺得剛開始用block的時候,有時候會有知其然而不知其所以然的情況。
尤其是Objective-c最令人頭疼的記憶體管理的部份在block中更是複雜,
如果你不知道你的物件怎麼在block中去使用那會是一件危險的事情,
所以這個主題算是經驗分享文,來分享我現在對block的認知,
而當然ios有很多官方的API都支援block,最有名的就是Grand Central Dispatch (GCD)
但是這篇不會介紹GCD,我們把內容專注在block這個語言功能。

在講block怎麼用之前,容我先介紹C的function跟function pointer,我認為在學block之前要有這個基本的認識比較好。
一來是block跟funtion有點相似,但是又有點不一樣,知道function再來看block我覺得會比較透徹的瞭解
二來它們兩個定義的部份真的很像,但是實作一個function跟block卻大不相同,仔細想想兩個的差異絕對會對block更有深刻的感覺。
首先function相信大家都會實作function,這再簡單不過了
int plusone(int a)
{
    return a+1;
}


上面這裡定義了一個傳入參數是int回傳是int的function,這應該不用解釋太多了。
那function pointer呢? 可能開始有些人不是那麼熟悉了...
int ooxx()
{
     typedef int (*myfunc_type)(int a);
     myfunc_type myfunc = plusone;
     NSLog(@"return value = %d", myfunc(5));
}

這邊開始就要解釋了
第一行是我定義一個myfunc_type這個function pointer type,而這個function type是傳入是int,回傳是int
而第二行是定義一個myfunc_type的變數,我讓他指到我們剛剛定義的plusone
而第三行我們就是去呼叫這個function pointer,則他就會等同去呼叫plusone,並且帶入5。
如果上面的code你看懂了,恭喜你,block幾乎沒差太多,甚至更好用。
int ooxx()
{
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        return a+1;
     };
     NSLog(@"return value = %d", myblock(5));
}

第一行跟function pointer很像,原本function pointer是用*,但是block是用^
第二行也是定義一個myblock_type的變數,但是不一樣的是他指到的是一個block,
block的實作也是^開頭,緊接著是回傳type,接著是傳入的參數,後面接上{....}就是block實作的內容啦。
就像前面所說的,block本身就是anonymous function的方式去定義,
有些時候anonymous function非常好用,例如定義event handler,
我們可以註冊event handler時,直接在後面寫event handler的邏輯,可以很快的可以看到事件發生之後會要做哪些動作
不必像用function pointer或是delegate的方式需要把邏輯寫在另外一個function,
第三行(應該說第三個statement)就跟function pointer很像了,就是呼叫這個block。

當然block強大的地方就是block所包含的code可以使用外面的變數
例如下面的例子,temp是block外面所定義的,
但是我們可以直接拿來用
int ooxx()
{
     int temp = 5;
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        return temp+1;
     };
     NSLog(@"return value = %d", myblock(5));
}

但是值得注意的是,上面的code,在block中會把temp當作常數看待,也就是說當定義block的時候,temp的值是5
即便我們在後來把temp改成1
但是得到的結果不會變,
這同樣也說明了一個特性,
那就是我們不能在block中,直接的修改(write)一個外部(區域)變數的值,
所以看下段code
int ooxx()
{
     int temp = 5;
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        temp++;
        return temp;
    };
     NSLog(@"return value = %d", myblock(5));
}

這邊compile就會錯了,因為這邊不只是會讀temp的值,還會寫temp的值。
但是如果我們加上__block這個變數修飾..
那此變數就可以在block中修改。
下面就是個例子
int ooxx()
{
     __block int temp = 5;
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        temp++;
        return temp;
    };
     NSLog(@"return value = %d", myblock(5));
}

以上就是常用的block的方法
事實上更常用的用法會是把block當參數傳到function,
這邊舉個GCD的例子,
dispatch_async(queue, ^(void) {
        // some task here
    });
這邊就是把一個block丟到queue中等待被背景執行,
而就是丟進一個傳入是void傳回是void的block
由於傳回是void,所以可以把^void(void){...}簡化成^(void){...}
在block的使用上常常會見到這種把block當參數的寫法,之後相信會漸漸的習慣。

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