目錄
一庆锦、block捕獲普通變量
? 1、block會捕獲局部變量轧葛,普通則值傳遞搂抒,靜態(tài)則指針傳遞
? 2、block不會捕獲全局變量
二尿扯、block捕獲指針變量
? 1燕耿、block會捕獲局部對象類型的指針變量,強指針(__strong
)則持有對象姜胖、弱指針(__weak
)則不持有對象
? 2誉帅、block會捕獲self
,強指針(__strong
)則持有對象右莱、弱指針(__weak
)則不持有對象
這個一定要記住蚜锨,拿著這個分析面試題很穩(wěn):block捕獲變量是指,如果block的執(zhí)行體里使用了外界的局部變量慢蜓,那么block內(nèi)部就會生成一個與局部變量同名的成員變量亚再,并且局部變量還會把值傳遞給這個成員變量,當然可能是值傳遞——使用了外界的普通局部變量時晨抡,也有可能是指針傳遞——使用了外界的靜態(tài)局部變量時氛悬。那么接下來block執(zhí)行體里使用的這個變量就不是外界的局部變量了,而是block體內(nèi)的成員變量耘柱。而如果block的執(zhí)行體里使用了外界的全局變量如捅,那block是不會捕獲它們的,會直接使用它們调煎。所以要想知道一個變量會不會被block捕獲镜遣,你只需要搞清變量是個局部變量還是個全局變量就行了,別去管block是什么類型的block士袄。
那為什么系統(tǒng)要給block添加捕獲變量機制呢悲关?又為什么只捕獲局部變量而不捕獲全局變量呢?實際開發(fā)中娄柳,我們難免要在block的執(zhí)行體里使用外界的局部變量寓辱,我們知道block其實是把block的參數(shù)、返回值赤拒、執(zhí)行體封裝成一個函數(shù)秫筏,而這個函數(shù)在調(diào)用時卻僅僅接收了block本身作為參數(shù)诱鞠,
來自上一篇
// 創(chuàng)建一個block
void (*block)(void) = &__block_impl_0(
__block_func_0,// 把函數(shù)的地址傳進去
&__block_desc_0_DATA // 把結構體的地址傳進去
);
// 調(diào)用block
block->impl.FuncPtr(block);
并沒有接收額外的參數(shù),所以一個函數(shù)怎么可能無緣無故就訪問到函數(shù)外部的變量呢跳昼。于是系統(tǒng)就為block設計了捕獲變量機制般甲,把局部變量捕獲到block體內(nèi)肋乍,以便函數(shù)僅僅接收block本身作為參數(shù)就能正常使用外界的局部變量鹅颊。而全局變量存儲在全局區(qū),block能直接訪問到墓造,所以不需要捕獲堪伍。
一、block捕獲普通變量
- block會捕獲局部變量
- block不會捕獲全局變量
1觅闽、block會捕獲局部變量帝雇,普通則值傳遞,靜態(tài)則指針傳遞
- block會捕獲普通局部變量蛉拙,且局部變量與成員變量之間是值傳遞
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 普通局部變量
int age = 25;
void (^block)(void) = ^{ // ARC下這是個堆block尸闸,因為block賦值給強指針,系統(tǒng)會自動復制一份到堆區(qū)
NSLog(@"%d", age); // 25
};
age = 26;
block();
}
return 0;
}
按正常邏輯來說孕锄,上面的代碼應該打印“26”吮廉,因為在block調(diào)用之前age
被改成“26”了,但實際上卻打印“25”畸肆,為什么宦芦?我們看看這段代碼的C/C++實現(xiàn)(偽代碼)。
// block對應的結構體
struct __block_impl_0 {
struct __block_impl impl;
struct __block_desc_0* Desc;
int age; // 多了一個成員變量
// : age(_age)轴脐,C++的語法调卑,意思是直接把_age參數(shù)的值賦值給age成員變量,相當于下面又多了一句賦值語句
__block_impl_0(void *fp, struct __block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
// age = _age;// 相當于這樣
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 25;
// 創(chuàng)建block
void (*block)(void) = &__block_impl_0(
__block_func_0,
&__block_desc_0_DATA,
age // 多了一個參數(shù)
);
age = 26;
// 調(diào)用block
block->impl.FuncPtr)(block);
}
return 0;
}
void __block_func_0(struct __block_impl_0 *__cself) {
int age = __cself->age; // 獲取age成員變量的值
NSLog(age);
}
我們看到block內(nèi)部多了一個成員變量age
大咱。
也看到在創(chuàng)建block的時候恬涧,block構造函數(shù)多了一個age
參數(shù),直接把變量的值“25”給傳進去了碴巾,并賦值給block的成員變量age
气破。
然后外界把變量age
的值改為“26”。
調(diào)用block時餐抢,系統(tǒng)讀取的是block內(nèi)部那個成員變量的值现使,所以打印了“25”。
- block會捕獲靜態(tài)局部變量旷痕,但局部變量與成員變量之間是指針傳遞
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 靜態(tài)局部變量
static int height = 25;
void (^block)(void) = ^{ // 這是個全局block
NSLog(@"%d", height); // 26
};
height = 26;
block();
}
return 0;
}
打印“25”紅還是“26”??碳锈?直接看C/C++實現(xiàn)吧(偽代碼)。
// block對應的結構體
struct __block_impl_0 {
struct __block_impl impl;
struct __block_desc_0* Desc;
int *height; // 多了一個成員變量欺抗,注意是個指針
// : height(_height)售碳,C++的語法,意思是直接把_height參數(shù)的值賦值給height成員變量,相當于下面又多了一句賦值語句
__block_impl_0(void *fp, struct __block_desc_0 *desc, int *_height, int flags=0) : height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
// height = _height; // 相當于這樣
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int height = 25;
// 創(chuàng)建block
void (*block)(void) = &__block_impl_0(
__block_func_0,
&__block_desc_0_DATA,
&height // 多了一個參數(shù)贸人,注意是個地址
);
height = 26;
// 調(diào)用block
block->impl.FuncPtr)(block);
}
return 0;
}
void __block_func_0(struct __block_impl_0 *__cself) {
int *height = __cself->height; // 獲取height成員變量的值
NSLog(*height);
}
沒問題间景,我們看到block內(nèi)部多了一個成員變量height
,但要注意它是個指針類型艺智。
也看到在創(chuàng)建block的時候倘要,block構造函數(shù)多了一個height
參數(shù),但這里不是直接把變量的值傳進去十拣,而是把變量的地址給傳進去了封拧,并賦值給block的成員變量height
。
然后外界把變量height
的值改為“26”夭问。
調(diào)用block時泽西,系統(tǒng)讀取的是block內(nèi)部那個成員變量的值沒問題,但因為它是個指針缰趋,指向外界的那個變量捧杉,所以打印了“26”。
1秘血、再加深一下印象:block會捕獲局部變量
- block會捕獲普通局部變量味抖,局部變量與成員變量之間是值傳遞
- block會捕獲靜態(tài)局部變量,局部變量與成員變量之間是指針傳遞
2直撤、那系統(tǒng)為什么要這樣設計呢非竿?同樣都是局部變量,為什么普通局部變量是值傳遞谋竖,而靜態(tài)局部變量是指針傳遞红柱?
void (^block)(void); void test() { // 普通局部變量 int age = 25; // 靜態(tài)局部變量 static int height = 25; block = ^{ // ARC下這是個堆block,因為block賦值給強指針蓖乘,系統(tǒng)會自動復制一份到堆區(qū) NSLog(@"%d %d", age, height); }; age = 26; height = 26; } int main(int argc, const char * argv[]) { @autoreleasepool { test(); block(); } return 0; }
一看上面這段代碼锤悄,你就明白了。
test
函數(shù)執(zhí)行完嘉抒,也就是說出了test
函數(shù)的作用域零聚,- 普通局部變量
age
就釋放了,也就是說它對應的那塊棧內(nèi)存就釋放了些侍,有可能被別人征用隶症,里面填充別的數(shù)據(jù),那內(nèi)存釋放后你再去訪問這塊內(nèi)存岗宣,訪問到不一定是原來的數(shù)據(jù)蚂会,所以普通局部變量采用指針傳遞根本沒有意義,因為它對應的那塊內(nèi)存說不定什么時候(即有可能在我們使用它之前)就釋放掉了耗式,所以還不如趁早把局部變量的值給存下來胁住。- 而靜態(tài)局部變量就不一樣了趁猴,出了
test
函數(shù)的作用域,height
變量雖然也被釋放掉了彪见,但這僅僅是表明在代碼層我們無法再繼續(xù)通過height
變量去訪問它對應的那塊內(nèi)存而已儡司,并不代表那塊內(nèi)存也釋放了,因為這塊內(nèi)存是靜態(tài)全局區(qū)的一塊內(nèi)存余指,所以我們只要用一個指針變量來記住這塊內(nèi)存的地址捕犬,那height
變量釋放后,我們依舊可以通過自己的指針變量去訪問那塊內(nèi)存浪规。
2或听、block不會捕獲全局變量
// 普通全局變量
int age = 25;
// 靜態(tài)全局變量
static int height = 25;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{ // 這是個全局block
NSLog(@"%d %d", age, height); // 26, 26
};
age = 26;
height = 26;
block();
}
return 0;
}
C/C++實現(xiàn)(偽代碼)探孝。
// block對應的結構體
struct __block_impl_0 {
struct __block_impl impl;
struct __block_desc_0* Desc;
__block_impl_0(void *fp, struct __block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int age = 25;
static int height = 25;
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 創(chuàng)建block
void (*block)(void) = &__block_impl_0(
__block_func_0,
&__block_desc_0_DATA,
);
age = 26;
height = 26;
// 調(diào)用block
block->impl.FuncPtr)(block);
}
return 0;
}
void __block_func_0(struct __block_impl_0 *__cself) {
NSLog(age, height); // 直接訪問全局變量
}
我們看到block內(nèi)部并不會多出成員變量笋婿,而且調(diào)用block時,是直接通過全局變量訪問對應內(nèi)存里的數(shù)據(jù)顿颅。
二缸濒、block捕獲指針變量
1、block會捕獲局部對象類型的指針變量粱腻,強指針(__strong
)則持有對象庇配、弱指針(__weak
)則不持有對象
block會捕獲局部對象類型的指針變量,而且捕獲后如果發(fā)現(xiàn)它是個強指針(即__strong
修飾)绍些,block還會強引用(即持有)它指向的對象捞慌,如果發(fā)現(xiàn)它是個弱指針(即__weak
修飾),block則會弱引用(即不持有)它指向的對象柬批。(如果更嚴謹一點的話啸澡,棧block永遠只是弱引用對象,只不過因為我們是ARC下氮帐,用的基本上都是堆block嗅虏,所以就故意忽略掉了這一點,免得大家混淆)
創(chuàng)建一個Person類上沐,簡單實現(xiàn)一下皮服,來驗證上面這條結論。
// INEPerson.h
@interface INEPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@end
// INEPerson.m
@implementation INEPerson
- (void)dealloc {
NSLog(@"INEPerson dealloc");
}
@end
- block捕獲強指針
// main.m
typedef void (^INEBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool
{ // 作用域2起點
INEBlock block;
{ // 作用域1起點
INEPerson *person;
person = [[INEPerson alloc] init];
person.age = 25;
block = ^{
NSLog(@"%ld", person.age);
};
} // 作用域1終點
NSLog(@"11");
} // 作用域2終點
return 0;
}
控制臺打硬瘟:
11
INEPerson dealloc
block的C/C++實現(xiàn)(偽代碼):
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
INEPerson *__strong person; // 確實捕獲了龄广,是個強指針
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, INEPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
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*);
}
我們知道person
指針變量是個局部變量,所以它肯定會被block捕獲蕴侧,而且person
指針變量默認是個強指針择同,所以block內(nèi)部生成的同名成員變量也是一個強指針,于是block就通過它內(nèi)部的那個強指針強引用了person
指針變量指向的Person對象戈盈。
所以出了作用域1后奠衔,雖然person
指針變量銷毀了谆刨,但此時block還沒銷毀,它還強引用著Person對象归斤,所以這個時候就不會走Person對象dealloc
方法痊夭,而是繼續(xù)往下走,打印完“11”脏里、出了作用域2后她我,block銷毀,同時也就釋放了對Person對象的強引用迫横,所以此時才走Person對象的dealloc
方法打印了“INEPerson dealloc”番舆。
- block捕獲弱指針
// main.m
typedef void (^INEBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool
{ // 作用域2起點
INEBlock block;
{ // 作用域1起點
__weak INEPerson *person;
person = [[INEPerson alloc] init];
person.age = 25;
block = ^{
NSLog(@"%ld", person.age);
};
} // 作用域1終點
NSLog(@"11");
} // 作用域2終點
return 0;
}
控制臺打印:
INEPerson dealloc
11
block的C/C++實現(xiàn)(偽代碼):
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
INEPerson *__weak person; // 確實捕獲了矾踱,是個弱指針
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, INEPerson *__weak _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
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*);
}
block確實會捕獲person
指針變量恨狈,但因為它是個弱指針,所以block就通過它內(nèi)部的那個弱指針弱引用了person
指針變量指向的Person對象呛讲。
所以出了作用域1后禾怠,person
指針變量銷毀,Person對象身上就沒有強引用了贝搁,所以這個時候就走Person對象的dealloc
方法打印了“INEPerson dealloc”吗氏,然后繼續(xù)往下走,打印完“11”雷逆、出了作用域2后弦讽,block銷毀。
此時膀哲,你可能會問:block捕獲指針倒是沒問題往产,但你憑什么說捕獲到強指針就持有對象,捕獲到弱指針就不持有對象等太,上面雖然通過代碼驗證了捂齐,但這底層是怎么實現(xiàn)的?
從上面的代碼中缩抡,我們可以看到只要是block捕獲了對象類型的指針變量奠宜,那它結構體內(nèi)第二個成員變量里就會多出兩個函數(shù),
copy
函數(shù)和dispose
函數(shù)瞻想,這兩個函數(shù)是專門用來負責對象的內(nèi)存管理的压真,這也是為什么block捕獲基本數(shù)據(jù)類型的變量時,它內(nèi)部不會生成這兩個函數(shù)蘑险。持有不持有主要靠的是block內(nèi)部的
copy
函數(shù)和dispose
函數(shù)滴肿,當我們把block從棧區(qū)copy
到堆區(qū)時,系統(tǒng)就會自動調(diào)用block內(nèi)部的copy
函數(shù)佃迄,該函數(shù)內(nèi)部會根據(jù)捕獲到的是個強指針還是弱指針來決定要不要把對象的引用計數(shù)加1泼差,而當block銷毀的時候贵少,系統(tǒng)又會自動調(diào)用內(nèi)部的dispose
函數(shù),來解除對對象的引用堆缘。
2滔灶、block會捕獲self
(指針變量),強指針(__strong
)則持有對象吼肥、弱指針(__weak
)則不持有對象
創(chuàng)建一個Person類录平,簡單實現(xiàn)一下,來驗證上面這條結論缀皱。
// INEPerson.m
@implementation INEPerson
- (void)test {
void (^block)(void) = ^{
NSLog(@"%@", self);
};
block();
}
@end
block的C/C++實現(xiàn)(偽代碼)斗这。
struct __INEPerson__test_block_impl_0 {
struct __block_impl impl;
struct __INEPerson__test_block_desc_0* Desc;
INEPerson *__strong self;
__INEPerson__test_block_impl_0(void *fp, struct __INEPerson__test_block_desc_0 *desc, INEPerson *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可見self
被block捕獲了,那為什么會捕獲self
呢啤斗?這是因為所有的OC方法其實都有兩個默認的參數(shù):self
指針變量和_cmd
selecotr
表箭,即該方法調(diào)用者和該方法的selector
,而方法的參數(shù)也是一種局部變量争占,所以self
會被block捕獲燃逻。上面的test
方法其實就是這樣(偽代碼):
- (void)test(id self, SEL _cmd) {
void (^block)(void) = ^{
NSLog(@"%@", self);
};
block();
}
self
指針默認也是個強指針序目,所以block會持有它指向的對象臂痕,而如果把self
指針變成弱指針,block就不會持有它指向的對象了猿涨。
// INEPerson.m
@implementation INEPerson
- (void)test {
__weak INEPerson *weakSelf = self;
void (^block)(void) = ^{
NSLog(@"%@", weakSelf);
};
block();
}
@end
block的C/C++實現(xiàn)(偽代碼)握童。
struct __INEPerson__test_block_impl_0 {
struct __block_impl impl;
struct __INEPerson__test_block_desc_0* Desc;
INEPerson *__weak weakSelf;
__INEPerson__test_block_impl_0(void *fp, struct __INEPerson__test_block_desc_0 *desc, INEPerson *__weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};