1.1 局部變量
局部自動變量,在Block中可被讀取。Block定義時copy變量的值,在Block中作為常量使用,所以即使變量的值在Block外改變,也不影響他在Block中的值,Block此時對局部變量只是做了值傳遞的操作。
1.2 static 修飾的全局變量
因為全局變量或靜態變量在內存中的地址是固定的,Block在讀取該變量值的時候是直接從其所在內存地址讀出,獲取到的是最新值,而不是在定義時copy的常量。
1.3 對OC對象的截獲
NSMutableArray?*array?=?[NSMutableArray?array];
void(^block)()?=?^(){
NSObject?*obj?=?[[NSObject?alloc]?init];
[array?addObject:obj];
};
block();
上述代碼編譯通過,Block截獲的值為NSMutableArray類的對象,用C語言表述,就是用的NSMutableArray類的對象所用的結構體實例的指針,所以向該對象中添加元素操作屬于使用截獲變量的值,因此是沒有問題的。那么對該截獲的變量進行賦值
屏幕快照 2016-06-23 下午2.12.28.png
編譯未通過,提示缺少__block修飾符。
1.4 C語言數組
屏幕快照 2016-06-23 下午2.18.28.png
上圖代碼中, 在Block外部定義一個C語言字符串字面量數組, 在Block內部截獲自動變量的方法并沒有實現對C語言數組的截獲, 此時訪問數組元素text[2]會報錯. 此時使用指針可以解決.
const?char?*text?=?"adsdczv";
void(^block)()?=?^(){
NSLog(@"%c",text[2]);
};
block();
1.5__block 修飾的變量
某些場景下,我們需要在Block內部對外部變量進行修改。這時需要使用__block來修飾該變量實現在Block內部的修改,此時Block是復制其引用地址來實現訪問的。
關于__block 修飾符
從上面講解我們已經知道,Block內部能夠讀取外部局部變量的值。但如果我們需要在Block內部修改變量的值,則需要在Block外部給該變量添加一個__block修飾符。
__block另一個使用場景是,避免某些情況下Block使用中出現的循環引用的問題,此時可以給相應的對象加上一個__block來修飾。
為什么使用__block可以實現在Block內部修改外部變量的值?
這邊我們用一個Block代碼,并使用clang _rewrite_objc命令轉換成C++的代碼來說明__block是怎么實現內部變量的修改。
Block在main中實現
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger val = 10;
void (^block)(void) = ^{
NSLog(@"%ld", val);
};
block();
}
return 0;
}
轉碼后:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : 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 val = __cself->val;? // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_41daf1_mi_0, 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[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int val = 10;
void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
從展開代碼可以發現,Block被轉成了一個struct __main_block_impl_0類型的結構體實例,并且該結構體成員中包含局部變量val。當執行Block時,通過該實例找到Block執行部分void __main_block_func_0,并把該結構體實例傳入到void __main_block_func_0方法中。
void __main_block_func_0方法中第一個參數聲明如下
struct __main_block_impl_0 *__cself
注意:這里的__cself就類似于OC中的self,而它指向結構體的指針。
此時我們就可以通過__cself->val訪問該局部變量。
那么問題來了,為什么此時不對變量val進行修改?
因為main函數中的局部變量val和函數__main_block_func_0不在同一個作用域中,調用過程中只是進行了值傳遞。
當然,在上面代碼中,我們可以通過指針來實現局部變量的修改。不過這是由于在調用__main_block_func_0時,main函數棧還沒展開完成,變量val還在棧中。
但是在很多情況下,Block是作為參數傳遞以供后續回調執行的。通常在這些情況下,Block被執行時,定義時所在的函數棧已經被展開,局部變量已經不在棧中了,再用指針訪問會產生野指針錯誤。
所以,這類情況下對于auto類型的局部變量,不允許Block進行修改是合理的。
__block是如何實現變量修改的
此時使用更新后的代碼
添加__block修飾符后
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSInteger val = 0;
void (^block)(void) = ^{
val = 1;
};
block();
NSLog(@"val = %ld", val);
}
return 0;
}
使用_rewrite_objc展開
struct?__Block_byref_val_0?{
void?*__isa;
__Block_byref_val_0?*__forwarding;
int?__flags;
int?__size;
NSInteger?val;
};
struct?__main_block_impl_0?{
struct?__block_impl?impl;
struct?__main_block_desc_0*?Desc;
__Block_byref_val_0?*val;?//?by?ref
__main_block_impl_0(void?*fp,?struct?__main_block_desc_0?*desc,?__Block_byref_val_0?*_val,?int?flags=0)?:?val(_val->__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_val_0?*val?=?__cself->val;?//?bound?by?ref
(val->__forwarding->val)?=?1;
}
static?void?__main_block_copy_0(struct?__main_block_impl_0*dst,?struct?__main_block_impl_0*src)?{_Block_object_assign((void*)&dst->val,?(void*)src->val,?8/*BLOCK_FIELD_IS_BYREF*/);}
static?void?__main_block_dispose_0(struct?__main_block_impl_0*src)?{_Block_object_dispose((void*)src->val,?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[])?{
/*?@autoreleasepool?*/?{?__AtAutoreleasePool?__autoreleasepool;
__attribute__((__blocks__(byref)))?__Block_byref_val_0?val?=?{(void*)0,(__Block_byref_val_0?*)&val,?0,?sizeof(__Block_byref_val_0),?0};
void(*block)(void)?=?((void?(*)())&__main_block_impl_0((void?*)__main_block_func_0,?&__main_block_desc_0_DATA,?(__Block_byref_val_0?*)&val,?570425344));
((void?(*)(__block_impl?*))((__block_impl?*)block)->FuncPtr)((__block_impl?*)block);
NSLog((NSString?*)&__NSConstantStringImpl__var_folders_2h_70k4gzp53qn7ytk0cdjr9kk80000gn_T_main_7eb9e7_mi_0,(val.__forwarding->val));
}
return?0;
}
這次轉碼后似乎比剛才多了些東西,仔細看下,
一個是__Block_byref_val_0的結構體以及兩個方法static void __main_block_copy_0和static void __main_block_dispose_0; 后面的兩個方法先暫且不關注(后面會涉及)。
其實結構體__Block_byref_val_0產生的實例就是我們使用__block修飾過的變量。
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
NSInteger val;
};
從該結構體聲明可以看出,這個結構體中包含了該實例本身的引用 __forwarding。
我們從上述被轉化的代碼中可以看出 Block 本身也一樣被轉換成了__main_block_impl_0結構體實例,該實例持有__Block_byref_val_0結構體實例的指針。
我們再看一下Block實現和調用部分代碼被轉化后的結果:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
不難發現從__cself找到__Block_byref_val_0結構體實例,然后通過該實例的__forwarding訪問成員變量val。成員變量val是該實例自身持有的變量,指向的是原來的局部變量。
詳情參見下圖:
__block.jpg
至此,已經展示了__block變量在Block中查找和修改的過程,那么:
當Block作為回調執行時,局部變量val已經出棧了,這個時候代碼為什么還能正常工作呢?
我們為什么通過成員變量__forwarding而不是直接去訪問結構體中我們需要修改的變量呢?
我們在上述轉換過的代碼中可以發現__main_block_impl_0結構體構造函數中,isa指針指向的是_NSConcreteStackBlock; 而Block還有另外兩個與之相似的類:
_NSConcreteGlobalBlock //全局的靜態block 不會訪問任何外部變量
_NSConcreteMallocBlock //保存在堆區的,引用計數為0時會被銷毀。
_NSConcreteStackBlock //保存在棧區,出棧后被銷毀
上述示例代碼中,Block是被設為_NSConcreteStackBlock,在棧上生成。當我們把Block作為全局變量使用時,對應生成的Block將被設為_NSConcreteGlobalBlock
void?(^block)(void)?=?^{NSLog(@"This?is?a?Global?Block");};
int?main(int?argc,?const?char?*?argv[])?{
@autoreleasepool?{
block();
}
return?0;
}
該代碼轉碼c++后,Block結構體的isa指針初始化時如下:
impl.isa = &_NSConcreteGlobalBlock;
那_NSConcreteMallocBlock何時被使用
分配在全局變量上的Block,在變量作用域外也可以通過指針安全的訪問。
但分配在棧上的Block,如果它所屬的變量作用域結束,該Block就被廢棄。同樣,__block變量也分配在棧上,當超過該變量的作用域時,該__block變量也會被廢棄。
此時,就需要使用_NSConcreteMallocBlock,OC中提供了將Block和__block變量從棧上復制到堆上的方法,將分配到棧上的Block復制到堆上,這樣當棧上的Block超過它原本作用域時,堆上的Block還可以繼續存在。
復制到堆上的Block,它的結構體成員變量isa將變為:
impl.isa = &_NSConcreteMallocBlock;
而_block變量中結構體成員__forwarding就在此時保證了從棧上復制到堆上能夠正確訪問__block變量。在這種情況下,只要棧上的_block變量的成員變量__forwarding指向堆上的實例,我們就能夠正確訪問。
我們一般可以使用copy方法手動將 Block 或者 __block變量從棧復制到堆上。比如我們把Block做為類的屬性訪問時,我們一般把該屬性設為copy。有些情況下我們可以不用手動復制,比如Cocoa框架中使用含有usingBlock方法名的方法時,或者GCD的API中傳遞Block時。
當一個Block從棧復制到堆中,與之相關的__block變量也會被復制到堆中。此時堆中的Block持有相應堆上的__block變量,當堆上的__block變量沒有持有者,才會被釋放。
而在棧上的__block變量被復制到堆上之后,會將成員變量__forwarding的值替換為堆上的__block變量的地址。這個時候我們可以通過以下代碼訪問:
val.__forwarding->val
如下圖:
__block變量和循環引用問題
__block修飾符可以指定任意類型的局部變量。此時還記這兩個方法嗎?
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src { ??? _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { ??? _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); }
當Block從棧復制到堆時,會調用_Block_object_assign函數持有該變量(相當于retain);
當堆上的Block被廢棄時,會調用_Block_object_dispose函數釋放該變量(相當于release)。
由上文描述可知,我們可以使用下述代碼解除Block循環引用的問題:
__block?id?tmp?=?self;
void(^block)(void)?=?^{
tmp?=?nil;
};
block();
通過執行block方法,nil被賦值到_block變量tmp中。這個時候_block變量對 self 的強引用失效,從而避免循環引用的問題。
總結:
通過__block變量可以控制對象的生命周期,在不能使用__weak修飾符的環境中,我們可以避免使用__unsafe_unretained修飾符。
在執行Block時可動態地決定是否將nil或者其它對象賦值給__block變量。
但是這種方法有一個明顯的缺點就是,我們必須去執行Block才能夠解除循環引用問題,否則就會出現問題。
4. 比較__weak 和 __strong
這邊用AFN中的一段代碼
__weak?__typeof(self)weakSelf?=?self;
AFNetworkReachabilityStatusBlock?callback?=?^(AFNetworkReachabilityStatus?status)?{
__strong?__typeof(weakSelf)strongSelf?=?weakSelf;
strongSelf.networkReachabilityStatus?=?status;
if?(strongSelf.networkReachabilityStatusBlock)?{
strongSelf.networkReachabilityStatusBlock(status);
}
};
1. __weak
我們在使用Block時,有時候會用到self,而Block內部對self默認都是強引用。在ARC下,編譯器將Block從棧區拷貝到堆區,Block會強引用和持有self,而self 也會強引用和持有Block,于是就造成了循環引用。
此時就需要使用__weak,在修飾變量時,修飾符修飾變量self,讓 block 不強引用self,從而破除循環。
__weak?typeof(self)?weakSelf?=?self;
self.passValueBlock?=?^(NSString?*string){
dispatch_async(dispatch_get_main_queue(),?^{
weakSelf.pointView.startLabel.text?=?string;
});
};
弱引用不會影響對象釋放,當一個對象被釋放是,所有指向它的弱引用會被置空,也避免出現野指針。
2. __strong
上面提到,__weak很好的解決retain Cycle,但還是會存在一些隱患。不知道self什么時候被釋放,為了保證在Block內部不會被釋放,所以使用__strong修飾。
看下一段測試代碼
在ViewController添加屬性
@property (nonatomic, strong) ViewController *vc;
viewDidLoad中
ViewController?*vc?=?[[ViewController?alloc]?init];
self.vc?=?vc;
__weak?ViewController?*?weakVC?=?self.vc;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0),?^{
NSInteger?count?=?0;
while?(count?<?4)?{
count++;
NSLog(@"%@",weakVC);
sleep(1);
}
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,?(int64_t)(2.0?*?NSEC_PER_SEC)),?dispatch_get_main_queue(),?^{
self.vc?=?nil;
});
實現dealloc方法
-?(void)dealloc?{
NSLog(@"%@",[self?class]);
}
看輸出結果:
2016-06-20?15:12:27.797?__strongTest[14823:1753981]?
2016-06-20?15:12:28.802?__strongTest[14823:1753981]?
2016-06-20?15:12:29.797?__strongTest[14823:1753934]?ViewController
2016-06-20?15:12:29.804?__strongTest[14823:1753981]?(null)
2016-06-20?15:12:30.808?__strongTest[14823:1753981]?(null)
可以看出:Block內部的對self.vc是弱引用。當2s后,self.vc在外部被釋放,則Block內部對self.vc的持有也失效。
現在在Block內部對self.vc進行強引用,Block內部代碼調整為:
__strong?ViewController?*strongVC?=?weakVC;
NSInteger?count?=?0;
while?(count?<?4)
count++;
NSLog(@"%@",strongVC);
sleep(1);
}
再看輸出結果:
2016-06-20?15:22:38.423?__strongTest[14839:1762881]?
2016-06-20?15:22:39.424?__strongTest[14839:1762881]?
2016-06-20?15:22:40.429?__strongTest[14839:1762881]?
2016-06-20?15:22:41.430?__strongTest[14839:1762881]?
2016-06-20?15:22:42.431?__strongTest[14839:1762835]?ViewController
Block內部對對象采用strong修飾后,既使原持有對象在block外部已經被釋放,但Block內部扔能持有,于是執行完Block后,該對象才被dealloc。
總結:weakSelf是為了Block不持有self,避免循環引用,而再聲明一個strongSelf是因為一旦進入Block執行,就不允許self在這個執行過程中釋放。Block執行完后這個strongSelf會自動釋放,沒有循環引用問題。
最后,使用Block時的注意事項
1.Block內部不能直接修改局部變量
Block內部可以訪問外部的變量, 默認是將其拷貝到其數據結構中來實現訪問的, 屬性是只讀的. Block內部不能修改外面的局部變量.
如果要修改需要對要修改的局部變量用__block修飾, 這樣局部變量就可以在Block內部修改了,Block是復制其引用地址來實現訪問的
2.當Block里面的出現self,造成的循環引用
循環引用就是當self 擁有一個Block的時候,在Block中又調用self的方法。形成了你中有我,我中有你,造成誰都無法將誰釋放。從而發生內存泄漏。
解決方法:
__weak typeof (self) weakSelf = self;
定義一個weakSelf變量并加上__weak修飾符,在Block代碼塊中,所有需要self的地方都用weakSelf來替代。這樣就不會增加引用計數,所以Block持有self對象也就不會造成循環引用,從而避免內存泄漏。
參考
Objective-C 高級編程: iOS和OS X多線程和內存管理