關(guān)于block的語法,請使勁戳這里→fuckingblocksyntax.com
這篇文章只記錄一下block的實現(xiàn),和block使用的注意事項。
正文:
1.block的數(shù)據(jù)結(jié)構(gòu)
首先,關(guān)于block的數(shù)據(jù)結(jié)構(gòu)和runtime是開源的,可以在llvm項目看到,或者下載蘋果的libclosure庫的源碼來看。蘋果也提供了在線的代碼查看方式,其中包含了很多示例和文檔說明。
這兩個地方的定義是相同的:
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
在objc中,根據(jù)對象的定義,凡是首地址是*isa的結(jié)構(gòu)體指針,都可以認為是對象(id)。這樣在objc中,block實際上就算是對象。
為了查看編譯器具體的工作,這里可以用clang重寫一段代碼試試看:
void foo_(){
int i = 2;
NSNumber *num = @3;
long (^myBlock)(void) = ^long() {
return i * num.intValue;
};
long r = myBlock();
}
上面這是一個很簡單的block,捕獲了兩個變量:一個int,一個NSNumber。
用clang翻譯成C++后變出了一大坨代碼,看著別扭不貼上來了。為了方便理解,這里稍微簡化和調(diào)整一下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __foo_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __foo_block_impl_0*, struct __foo_block_impl_0*);
void (*dispose)(struct __foo_block_impl_0*);
};
//myBlock的數(shù)據(jù)結(jié)構(gòu)定義
struct __foo_block_impl_0 {
struct __block_impl impl;
struct __foo_block_desc_0* Desc;
int i;
NSNumber *num;
};
//block數(shù)據(jù)的描述
static struct __foo_block_desc_0 __foo_block_desc_0_DATA = {
0,
sizeof(struct __foo_block_impl_0),
__foo_block_copy_0,
__foo_block_dispose_0
};
//block中的方法
static long __foo_block_func_0(struct __foo_block_impl_0 *__cself) {
int i = __cself->i; // bound by copy
NSNumber *num = __cself->num; // bound by copy
return i * num.intValue;
}
void foo(){
int i = 2;
NSNumber *num = @3;
struct __foo_block_impl_0 myBlockT;
struct __foo_block_impl_0 *myBlock = &myBlockT;
myBlock->impl.isa = &_NSConcreteStackBlock;
myBlock->impl.Flags = 570425344;
myBlock->impl.FuncPtr = __foo_block_func_0;
myBlock->Desc = &__foo_block_desc_0_DATA;
myBlock->i = i;
myBlock->num = num;
long r = myBlock->impl.FuncPtr(myBlock);
}
編譯器會根據(jù)block捕獲的變量,生成具體的結(jié)構(gòu)體定義。block內(nèi)部的代碼將會提取出來,成為一個單獨的C函數(shù)。創(chuàng)建block時,實際就是在方法中聲明一個struct,并且初始化該struct的成員。而執(zhí)行block時,就是調(diào)用那個單獨的C函數(shù),并把該struct指針傳遞過去。
block中包含了被引用的自由變量(由struct持有),也包含了控制成分的代碼塊(由函數(shù)指針持有),符合閉包(closure)的概念。
2.block的Copy
block中的isa指向的是該block的Class。在block runtime中,定義了6種類:
_NSConcreteStackBlock 棧上創(chuàng)建的block
_NSConcreteMallocBlock 堆上創(chuàng)建的block
_NSConcreteGlobalBlock 作為全局變量的block
_NSConcreteWeakBlockVariable
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
其中我們能接觸到的主要是前3種,后三種用于GC不再討論..
上面代碼可以看到,當struct第一次被創(chuàng)建時,它是存在于該函數(shù)的棧幀上的,其Class是固定的_NSConcreteStackBlock。其捕獲的變量是會賦值到結(jié)構(gòu)體的成員上,所以當block初始化完成后,捕獲到的變量不能更改。
當函數(shù)返回時,函數(shù)的棧幀被銷毀,這個block的內(nèi)存也會被清除。所以在函數(shù)結(jié)束后仍然需要這個block時,就必須用Block_copy()方法將它拷貝到堆上。這個方法的核心動作很簡單:申請內(nèi)存,將棧數(shù)據(jù)復制過去,將Class改一下,最后向捕獲到的對象發(fā)送retain,增加block的引用計數(shù)。詳細代碼可以直接點這里查看。
struct Block_layout *result = malloc(aBlock->descriptor->size);
memmove(result, aBlock, aBlock->descriptor->size);
result->isa = _NSConcreteMallocBlock;
_Block_call_copy_helper(result, aBlock);
return result;
3.__block類型的變量
默認block捕獲到的變量,都是賦值給block的結(jié)構(gòu)體的,相當于const不可改。為了讓block能訪問并修改外部變量,需要加上__block修飾詞。
舉個例子:
void foo(){
__block int i = 3;
void(^myBlock)(void) = ^{
i *= 2;
};
myBlock();
}
讓clang重寫一下:
struct Block_byref { //Block_private.h中的定義
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
//__block count的實現(xiàn)
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
void foo_(){
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 1};
void(*myBlock)(void) = (void (*)())&__foo__block_impl_0((void *)__foo__block_func_0, &__foo__block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
嘩~一下子變出來一坨東西。就因為加了個__block,原本的int值的位置變成了一個struct(struct __Block_byref)。這個struct的首地址為同樣為*isa。
正是如此,這個值才能被block共享、并且不受棧幀生命周期的限制、在block被copy后,能夠隨著block復制到堆上。
4.使用注意事項
block對變量的捕獲規(guī)則:
靜態(tài)存儲區(qū)的變量:例如全局變量、方法中的static變量
引用,可修改。block接受的參數(shù)
傳值,可修改,和一般函數(shù)的參數(shù)相同。棧變量 (被捕獲的上下文變量)
const,不可修改。 當block被copy后,block會對 id類型的變量產(chǎn)生強引用。
每次執(zhí)行block時,捕獲到的變量都是最初的值。棧變量 (有__block前綴)
引用,可以修改。如果時id類型則不會被block retain,必須手動處理其內(nèi)存管理。
如果該類型是C類型變量,block被copy到heap后,該值也會被挪動到heap
注意1.內(nèi)存
Block_copy()和Block_release()必須一一匹配,否則會內(nèi)存泄漏或crash。
__block這個修飾詞會將原本的簡單類型轉(zhuǎn)化為較大的struct,這會給內(nèi)存、調(diào)用帶來額外的開銷,使用時需要注意。
注意2.ARC
在開啟ARC后,block的內(nèi)存會比較微妙。ARC會自動處理block的內(nèi)存,不用手動copy/release。
但是,和非ARC的情況有所不同:
void (^aBlock)(void);
aBlock = ^{ printf("ok"); };
block是對象,所以這個aBlock默認是有__strong修飾符的,即aBlock對該block有strong references。即aBlock在被賦值的那一刻,這個block會被copy。所以,ARC開啟后,所能接觸到的block基本都是在堆上的。。
void (^aBlock)(void) = nil;
if (!aBlock) {
aBlock = ^{ printf("hehe"); };
}
//block此時block已經(jīng)被釋放,該處留下了一個dangling pointer
aBlock();
上面這個例子,如果是非ARC時,block還在棧幀上,所以沒問題。但開啟ARC后,block會被先copy到堆上,然后再被釋放,這里就會crash了(經(jīng)測試現(xiàn)在不會crash)。所以這時就必須手動調(diào)用Block_copy了。蘋果建議盡量避免這種情況。
注意3.循環(huán)引用
當block被copy之后(如開啟了ARC、或把block放入dispatch queue),該block對它捕獲的對象產(chǎn)生strong references (非ARC下是retain),
所以有時需要避免block copy后產(chǎn)生的循環(huán)引用。
如果用self引用了block,block又捕獲了self,這樣就會有循環(huán)引用。
因此,需要用weak來聲明self
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
self.block = ^{
[weakSelf doSomething]; //捕獲到的是弱引用
}
}
如果捕獲到的是當前對象的成員變量對象,同樣也會造成對self的引用,同樣也要避免。
- (void)configureBlock {
id tmpIvar = _ivar; //臨時變量,避免了self引用
self.block = ^{
[tmpIvar msg];
}
}
為了避免循環(huán)引用,可以這樣理解block:block就是一個對象,它捕獲到的值就是這個對象的@property(strong)。這樣在遇到問題時,就能迅速確定是否有循環(huán)引用了。Xcode5已經(jīng)能自動發(fā)現(xiàn)這種問題了,不錯~
PS:Pro Multithreading and Memory Management for iOS and OS X 這是一本好書,強烈推薦。
PSS:后來才發(fā)現(xiàn)原來這是本日文原版書,并且有中文版翻譯。名字叫做"Objective-C高級編程:iOS與OS X多線程和內(nèi)存管理"。名字差那么多?。。“Α?。買到中文版才發(fā)現(xiàn)之前看過。。
本文轉(zhuǎn)自objc 中的 block