參考文章:深入研究Block捕獲外部變量和__block實現(xiàn)原理
Block是什么匈子?
Blocks是C語言的擴充功能,在OS X v10.6 和iOS 4中被引入讳嘱,并在系統(tǒng)API中被廣泛使用幔嗦。它很像標準的C函數(shù),但是它除了包含可執(zhí)行代碼外沥潭,還包含了執(zhí)行時需要訪問的變量(棧上或堆上)邀泉。
簡而言之,Block是能夠捕獲當前作用域變量的匿名函數(shù)钝鸽。也可以理解為一個可以延遲執(zhí)行的代碼片段汇恤。
如何學(xué)習(xí)Block
Working with Blocks
Blocks Programming Topics
llvm-project開源 - BlocksRuntime
Block的特征
- Block允許你創(chuàng)建一段代碼并能像變量一樣傳參、返回拔恰、存儲因谎,然后在適當?shù)臅r機被執(zhí)行。這為異步編程打下了基礎(chǔ)
- Block還能從當前作用域中捕獲變量仁连,類似于其他語言的“閉包(closure)”蓝角、“ lambdas表達式”等概念阱穗。某些情況下Block還能修改被捕獲的原始變量(比如使用__block)
Block語法
- 聲明Block引用
void (^blockReturningVoidWithVoidArgument)(void);
int (^blockReturningIntWithIntAndCharArguments)(int, char);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
可以使用typedef簡化聲明
typedef float (^MyBlockType)(float, float);
MyBlockType myFirstBlock = // ... ;
- 創(chuàng)建Block
^(float aFloat) {
float result = aFloat - 1.0;
return result;
};
注意創(chuàng)建時^左側(cè)不需要指明返回類型饭冬,且如果沒有參數(shù)的話,^右側(cè)可以把括號省略。
- Block參數(shù)類型
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
Block返回類型
Block屬性
@property (copy) void (^blockProperty)(void);
Block的應(yīng)用
- 將代碼邏輯傳入API揪阶,自定義算法實現(xiàn):
char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
char *left = *(char **)l;
char *right = *(char **)r;
return strncmp(left, right, 1);
});
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }
- 作為回調(diào)參數(shù)傳入API昌抠,用來實現(xiàn)異步調(diào)用
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
- 局部封裝代碼,實現(xiàn)代碼復(fù)用和延遲執(zhí)行
void (^printTip)() = ^{
NSLog(@"hello word");
};
if (needPrintDirectly) {
printTip();
}else{
[self beginTaskWithName:@"MyTask" completion:^{
printTip();
}];
}
- 構(gòu)造可分發(fā)的Task鲁僚,實現(xiàn)并發(fā)編程
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
...
}];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
- 能夠捕獲和修改變量炊苫,從而延長變量的作用域和生命周期
Block是對象嗎裁厅?
Block在OC中的實現(xiàn)如下:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
從結(jié)構(gòu)圖中很容易看到isa,所以O(shè)C處理Block是按照對象來處理的侨艾。在iOS中执虹,isa常見的就是_NSConcreteStackBlock,_NSConcreteMallocBlock唠梨,_NSConcreteGlobalBlock這3種袋励。
即Block是一種OC對象,它也可以存放在NSArray或NSDictionary這樣的集合中当叭。
Block三種類型的區(qū)別
三種類型的Block存儲在哪呢茬故?
顧名思義,_NSConcreteStackBlock存儲在棧上蚁鳖,_NSConcreteMallocBlock存儲在堆上磺芭,_NSConcreteGlobalBlock存儲在全局區(qū)
那這三種類型怎樣產(chǎn)生的呢?
Block中沒有用到外部變量醉箕,或只用到全局變量钾腺、靜態(tài)變量(包括局部靜態(tài)變量)的都是_NSConcreteGlobalBlock。
除了_NSConcreteGlobalBlock外讥裤,剛創(chuàng)建的Block都是_NSConcreteStackBlock垮庐。
對_NSConcreteStackBlock進行copy后,就變成了_NSConcreteMallocBlock
以下是Block_copy的一個實現(xiàn)坞琴,實現(xiàn)了從_NSConcreteStackBlock復(fù)制到_NSConcreteMallocBlock的過程哨查。對應(yīng)有9個步驟。
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
// 1
if (!arg) return NULL;
// 2
aBlock = (struct Block_layout *)arg;
// 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 8
result->isa = _NSConcreteMallocBlock;
// 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
對Block進行Copy的意義剧辐?
由于_NSConcreteStackBlock所屬的變量域一旦結(jié)束寒亥,那么該Block就會被銷毀,但很多情況下我們想延長Block的生命周期:
在MRC下荧关,我們可以通過將block從棧copy到堆上溉奕,來延長block的生命周期,所以一般block類型的屬性都會使用copy描述
在ARC下忍啤,編譯器會自動的判斷加勤,把block自動從棧copy到堆上,如以下四種情況:
- 手動調(diào)用copy
- Block是函數(shù)的返回值
- Block被強引用同波,Block被賦值給__strong或者id類型
- 調(diào)用系統(tǒng)API入?yún)⒅泻衭singBlcok的方法
因此鳄梅,ARC下,block類型的屬性直接用strong描述即可
Block如何捕獲變量
我們來測一下Block中引用四種類型的變量:
- 全局變量
- 靜態(tài)全局變量
- 靜態(tài)局部變量
- 自動變量
int global_i = 1;
static int static_global_j = 2;
int main(int argc, const char * argv[]) {
static int static_k = 3;
int val = 4;
void (^myBlock)(void) = ^{
global_i ++;
static_global_j ++;
static_k ++;
NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
};
global_i ++;
static_global_j ++;
static_k ++;
val ++;
NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
myBlock();
return 0;
}
轉(zhuǎn)換成C++源碼如下
int global_i = 1;
static int static_global_j = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_k;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_k = __cself->static_k; // bound by copy
int val = __cself->val; // bound by copy
global_i ++;
static_global_j ++;
(*static_k) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
static int static_k = 3;
int val = 4;
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
global_i ++;
static_global_j ++;
static_k ++;
val ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
在__main_block_impl_0中未檩,可以看到局部靜態(tài)變量static_k和自動變量val戴尸,被Block從外面捕獲進來,成為__main_block_impl_0這個結(jié)構(gòu)體的成員變量了冤狡。
總結(jié)一下
- 能在Block中被修改的變量:全局變量孙蒙、靜態(tài)全局變量项棠、靜態(tài)局部變量
- 能被Block捕獲的變量:靜態(tài)局部變量、自動變量
- Block捕獲外部變量僅僅只捕獲Block閉包里面會用到的值挎峦,其他用不到的值香追,它并不會去捕獲。
相關(guān)解釋:
- 首先全局變量和靜態(tài)全局變量可以被修改坦胶,是因為他們作用域很廣翅阵,所以在Block中和Block外被操作,它們的值依舊可以得以保存下來迁央。
- 靜態(tài)局部變量可以被修改掷匠,是因為被捕獲的是變量地址,在Block中通過變量地址可以修改原始變量岖圈。
- 自動變量不可以被修改讹语,是因為Block是通過拷貝值的方式捕獲的自動變量,因此不能修改原始變量蜂科。OC在編譯的層面就防止開發(fā)者可能犯的錯誤顽决,因為自動變量沒法在Block中改變外部變量的值,所以編譯過程中就報編譯錯誤导匣。
如何在Block中修改捕獲的變量
通過上述例子才菠,我們知道除了全局變量、靜態(tài)全局變量可以被修改外贡定,靜態(tài)局部變量也可以被修改赋访,因為捕獲的是變量地址。
而根據(jù)官方文檔我們可以了解到缓待,在自動變量前加入 __block關(guān)鍵字蚓耽,就可以在Block里面改變外部自動變量的值了。
總結(jié)一下在Block中改變變量值有2種方式:
- 傳遞內(nèi)存地址指針到Block中
- 改變存儲區(qū)方式(__block)旋炒。
__block實現(xiàn)原理
我們繼續(xù)研究一下__block實現(xiàn)原理步悠。
先來看看普通變量的情況。
int main(int argc, const char * argv[]) {
__block int i = 0;
void (^myBlock)(void) = ^{
i ++;
NSLog(@"%d",i);
};
myBlock();
return 0;
}
把上述代碼用clang轉(zhuǎn)換成源碼瘫镇。
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_i_0 *i = __cself->i; // bound by ref
(i->__forwarding->i) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
從源碼我們能發(fā)現(xiàn)鼎兽,帶有 __block的變量也被轉(zhuǎn)化成了一個結(jié)構(gòu)體__Block_byref_i_0,這個結(jié)構(gòu)體有5個成員變量。第一個是isa指針铣除,第二個是指向自身類型的__forwarding指針谚咬,第三個是一個標記flag,第四個是它的大小通孽,第五個是變量值序宦,名字和變量名同名睁壁。
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,
(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
源碼中是這樣初始化的背苦。__forwarding指針初始化傳遞的是自己的地址互捌。然而這里__forwarding指針真的永遠指向自己么?
我們做了個實驗行剂,將Block進行copy后再打印秕噪,發(fā)現(xiàn)__forwarding指向了堆上的地址氮趋,我們猜想__block也被拷貝到堆上了早抠。
我們把Block通過copy到了堆上,堆上也會重新復(fù)制一份__block辐怕,并且該Block也會繼續(xù)持有該__block铲觉。當Block釋放的時候澈蝙,__block沒有被任何對象引用,也會被釋放銷毀撵幽。
__forwarding指針這里的作用就是針對堆的Block灯荧,把原來__forwarding指針指向自己,換成指向_NSConcreteMallocBlock上復(fù)制之后的__block自己盐杂。然后堆上的變量的__forwarding再指向自己逗载。這樣不管__block怎么復(fù)制到堆上,還是在棧上链烈,都可以通過(i->__forwarding->i)來訪問到變量值厉斟。
所以在__main_block_func_0函數(shù)里面就是寫的(i->__forwarding->i)。
Block造成的環(huán)引用
為何會造成環(huán)引用呢强衡?
//MRC下運行
__block id block_obj = [[NSObject alloc]init];
id obj = [[NSObject alloc]init];
NSLog(@"***Block前****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
void (^myBlock)(void) = ^{
NSLog(@"***Block中****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
};
myBlock();
void (^myBlockCopy)(void) = [myBlock copy];
NSLog(@"***BlockCopy前****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
myBlockCopy();
輸出
***Block前****block_obj = [0x7fff5d509bd8 , 1] , obj = [0x7fff5d509ba8 , 1]
***Block中****block_obj = [0x7fff5d509bd8 , 1] , obj = [0x7fff5d509b80 , 1]
***BlockCopy前****block_obj = [0x608000243238 , 1] , obj = [0x7fff5d509ba8 , 2]
***Block中****block_obj = [0x608000243238 , 1] , obj = [0x6080002433b0 , 2]
在MRC下擦秽,對于普通的對象,_NSConcreteStackBlock是不強持有的漩勤,而_NSConcreteMallocBlock是強持有的号涯。也可以認為對一個_NSConcreteStackBlock進行copy時會強持有已捕獲的普通對象(引用計數(shù)增加)。
而ARC下經(jīng)常會自動將一個Block進行copy到堆上锯七,因此很容易強引用了已捕獲的普通對象链快,從而可能造成環(huán)引用問題。
在MRC下眉尸,__block修飾的對象在整個block進行copy時也會被copy到堆上域蜗,但是它的引用計數(shù)沒有變化,即沒有被強持有噪猾,這一點可以用來避免環(huán)引用霉祸。
而在ARC下,__block修飾的對象在整個Block進行copy時袱蜡,引用計數(shù)會增加丝蹭,即仍然會被強持有。
如何打破環(huán)引用呢坪蚁?
在MRC下:
- __block方式
myViewController * __block myController = [[MyViewController alloc] init];
myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
};
在ARC下:
- 主動打破環(huán)的方式
__block MyViewController * myController = [[MyViewController alloc] init];
myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
myController = nil; // 注意這里奔穿,保證了block結(jié)束myController強引用的解除
};
- 弱引用的方式(推薦)
MyViewController *myController = [[MyViewController alloc] init];
MyViewController * __weak weakMyViewController = myController;
myController.completionHandler = ^(NSInteger result) {
[weakMyViewController dismissViewControllerAnimated:YES completion:nil];
};
思考ARC下的弱引用方式是否很完善镜沽?會不會在Block執(zhí)行到一半時weak變量就被釋放掉了?在多線程環(huán)境下這種情況是可能發(fā)生的贱田。
解決方法就是我們在block內(nèi)新定義一個強引用strongMyController來指向weakMyController指向的對象缅茉,這樣多了一個強引用,就能保證block執(zhí)行時weakMyController指向的對象不會被釋放男摧。
strongMyController 雖然是強引用蔬墩,但是它屬于bolck新聲明的變量,存在于棧中耗拓。當函數(shù)執(zhí)行完成后拇颅,引用被銷毀,引用關(guān)系也被解除了乔询。
最終代碼如下:
MyViewController *myController = [[MyViewController alloc] init…];
MyViewController * __weak weakMyController = myController;
myController.completionHandler = ^(NSInteger result) {
MyViewController *strongMyController = weakMyController;
if (strongMyController) {
[strongMyController dismissViewControllerAnimated:YES completion:nil];
} else {
// Probably nothing...
}
};
捕獲變量的總結(jié)
對于非對象的變量來說
自動變量的值蔬蕊,被copy進了Block,不帶__block的自動變量只能在里面被訪問哥谷,并不能改變值岸夯。
帶__block的自動變量 和 靜態(tài)變量 就是直接地址訪問。所以在Block里面可以直接改變變量的值们妥。
而剩下的靜態(tài)全局變量猜扮,全局變量,函數(shù)參數(shù)监婶,也是可以在直接在Block中改變變量值的旅赢,但是他們并沒有變成Block結(jié)構(gòu)體__main_block_impl_0的成員變量,因為他們的作用域大惑惶,所以可以直接更改他們的值煮盼。
值得注意的是,靜態(tài)全局變量带污,全局變量僵控,函數(shù)參數(shù)他們并不會被Block持有,也就是說不會增加retainCount值鱼冀。
對于對象來說
對于不用__block修飾的普通對象报破,一開始會像自動變量一樣被拷貝到_NSConcreteStackBlock中,引用計數(shù)無變化千绪;但當Block被copy到堆上時充易,被捕獲的對象引用計數(shù)增加。
對于__block修飾的對象荸型,一開始會像自動變量一樣被拷貝到_NSConcreteStackBlock中盹靴,引用計數(shù)無變化;但當Block被copy到堆上時,分兩種情況:
- MRC下稿静,被捕獲的對象引用計數(shù)不變梭冠。
- ARC下,被捕獲的對象引用計數(shù)增加自赔。
__block作用的總結(jié):
MRC下
- 說明變量可改
- 說明指針指向的對象不做這個隱式的retain操作妈嘹,打破環(huán)引用
ARC下
- 說明變量可改