本文將淺分析幾個Block使用問題:
- 解析問題一:Block作為類變量屬性時為啥用copy修飾?堆棧存儲位置是怎樣的?
- 解析問題二:為什么需要__block 修飾自動變量后,才能在Block中更改?
- 解析問題三:關于常見block中self的循環(huán)引用問題,可以用__weak打破強引用鏈;那么為什么AFN或像UIView動畫不需要__weak修飾self?
Block:帶有自動變量(局部變量)的匿名函數。
Block類型:
內聯Block(inline):說白了就是Block被嵌入到一個函數中,較常使用;
獨立Block:即在方法外定義的。不能直接訪問 self,只能通過將 self 當作參數傳遞到 Block 中才能使用,并且此時的 self 只能通過 setter 或 getter 方法訪問其屬性,不能使用句點式方法。但內聯 Block 不受此限制.
// 獨立Block
void (^correctBlockObject)(id) = ^(id self){
// 將self作為參數傳遞
NSLog(@"self = %@", self);
// 訪問self變量
NSLog(@"self = %@", [self strName]);
};
使用注意點:
Block作為屬性用copy修飾:
Block作為類屬性,要用copy修飾,把Block從棧拷貝到堆中,防止被釋放掉;不能修改外部自動變量問題:
Block里面能修改全局或靜態(tài)的變量,但是不能修改在Block外并且定義在所在方法內的自動變量,這時需要__block符修飾此變量;
例:
__block NSInteger val = 0;
void (^block)(void) = ^{
val = 1;
};
block();
NSLog(@"val = %ld", val);
//(這是在ARC下這樣使用,MRC下__block含義是不增加此對象引用計數,相當于ARC下的__weak)
(自動(automatic)變量,即局部變量:不作專門說明的局部變量,均是自動變量。自動變量也可用關鍵字auto作出說明,auto int c=3;/c為自動變量/。自動變量只有一種存儲方式,就是存儲在棧中。由于自動變量存儲在棧中,所以自動變量的作用域只在函數內,其生命周期也只持續(xù)到函數調用的結束。)
- 循環(huán)引用問題:
Block在方法中被定義時,該方法執(zhí)行完是需要被釋放的,Block會強引用自己持有的對象,使其引用計數+1,如果所持有的對象也持有此Block時,需要__weak 在外修飾此對象,標識不+1,常見的是self或一些強類型的對象:
例如:(ARC下)
__weak HomeViewController * VC = self;
__weak typeof(self) weakSelf = self;
__weak __typeof(&*self)weakSelf = self;
__weak typeof(_tableView) weakTableView = _tableView;
(MRC下,無__weak)
__block HomeViewController * VC = self;
ARC與MRC下Block內存分配機制不一樣,ARC中iOS5+用
__weak
,之前是用__unsafe_unretained
修飾符;
__unsafe_unretained缺點是指針所引用的對象被回收后,自己不會置為nil,因此可能導致程序崩潰;而weak指針不同,對象被回收后,指針會被賦值為nil。一般來說,weak修飾符更加安全。
另一種寫法:
有時候weakSelf會在Block未執(zhí)行完就會釋放掉成為nil,為了防止這種情況出現,在Block內需要__strong對weakSelf強引用一下(更高級寫法見RAC的@weakify(self),@strongify(self))。
//宏定義 - Block循環(huán)引用
#define weakify(var) __weak typeof(var) weakSelf = var
#define strongify(var) __strong typeof(var) strongSelf = var
weakify(self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
strongSelf(weakSelf);
[strongSelf doSomething];//防止weakSelf釋放掉
[strongSelf doOtherThing];
});
這樣不會造成循環(huán)引用是因為strongSelf是在Block里面聲明的一個指針,當Block執(zhí)行完畢后,strongSelf會釋放,這個時候將不再強引用weakSelf,所以self會正確的釋放。
解析問題一:Block的存儲位置
在Objective-C語言中,一共有3種類型的block:
1._NSConcreteGlobalBlock 全局的靜態(tài)block,不會訪問任何外部變量。
2._NSConcreteStackBlock 保存在棧中的block,當函數返回時會被銷毀。
3._NSConcreteMallocBlock 保存在堆中的block(從棧中copy過去的),當引用計數為0時會被銷毀。
Block內捕獲變量會改變自身存儲位置,包括讀取變量和__block這種寫變量,兩種方式(其實結果是一樣的)。
【在MRC下】:存在棧、全局、堆這三種形式。
【在ARC下】:大部分情況下系統(tǒng)會把Block自動copy到堆上。
Block作為變量:
- 方法中聲明一個 block 的時候是在棧上;
- 引用了外部局部變量或成員變量,并且有賦值操作(有名字),會被 copy 到堆上;
- 賦值給附有__strong修飾符的id類型的類或者Blcok類型成員變量時;
- 賦值給一個 weak 變量不會被 copy;
Block作為屬性:
- 用 copy 修飾會被 copy;
Block作為函數參數:
- 作為參數傳入函數不會被 copy,依然在棧上,方法執(zhí)行完即釋放;
- 作為函數的返回值會被 copy 到堆;
例如:
-(void)method {
//在ARC環(huán)境下,Block也是存在__NSStackBlock的時候的,平時見到最多的是_NSConcreteMallocBlock,是因為我們會對Block有賦值操作,所以ARC下,block 類型通過=進行傳遞時,會導致調用objc_retainBlock->_Block_copy->_Block_copy_internal方法鏈。并導致 __NSStackBlock__ 類型的 block 轉換為 __NSMallocBlock__ 類型。
int a = 3;
void(^block)() = ^{
NSLog(@"調用block%d",a);//這里的變量a,和self.string是一樣效果
};
NSLog(@"%@",block);
//打印結果:<__NSMallocBlock__: 0x7fc498746000>
//此時后面的匿名函數賦值給block指針(創(chuàng)建帶名字的block),且引用了外部局部變量,block會copy到堆
NSLog(@"%@",^{NSLog(@"調用block%d",a);});
//打印結果:<__NSStackBlock__: 0x7fff54f0c700>
//匿名函數無賦值操作,只存于棧上,會不定釋放
static int b = 2;
void(^block)() = ^{
NSLog(@"調用block%d",b);//若不引用任何變量,也是__NSGloBalBlock__
};
NSLog(@"%@",block);
}
//打印結果:<__NSGloBalBlock__: 0x7fc498746000>
//此時引用了全局變量,block放在全局區(qū)
解析問題二:為什么__block
修飾自動變量后,就能在Block中更改?
首先,為什么Block不能修改外部自動變量?
自動變量存于棧中,在當前方法執(zhí)行完,會釋放掉。一般來說,在 block 中用的變量值是被復制過來的,自動變量是值類型復制,新開辟??臻g,所以對于新復制變量的修改并不會影響這個變量的真實值(也稱只讀拷貝)。大多情況下,block是作為參數傳遞以供后續(xù)回調執(zhí)行的。所以在你想要在block中修改此自動變量時,變量可能已被釋放,所以不允許block進行修改是合理的。
對于 static 變量,全局變量,在 block中是有讀寫權限的,因為此變量存于全局數據區(qū)(非棧區(qū)),不會隨時釋放掉,也不會新開辟內存創(chuàng)建變量, block 拷貝的是指向這些變量的指針,可以修改原變量。
那么怎么讓自動變量不被釋放,還能被修改呢?
__block修飾符把所修飾的變量包裝成一個
結構體對象
,即可完美解決。Block既可以強引用此結構體對象,使之不會自動釋放,也可以拷貝到指向該結構體對象的指針,通過指針修改結構體的變量,也就是__block所修飾的自動變量。
將下面main.m文件進行clang -rewrite-objc main.m命令查看編譯時的c++代碼:
//寫在main.m文件的main方法里
__block NSInteger val = 0;
void (^block)(void) = ^{
val = 1;
};
block();
NSLog(@"val = %ld", val);
如下:
//把val變量封裝成了結構體
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;//注意這個__forwarding指針
int __flags;
int __size;
NSInteger val;
};
//block變量
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;
}
};
//結構體作為參數是通過指針傳遞,這里__cself指針傳入變量所在結構體,并在__main_block_func_0中實現變量值的改變
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;//通過__forwarding指針找到val變量
}
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的描述
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};
在__main_block_func_0里,發(fā)現是通過(val->__forwarding->val) = 1;
找到val變量,這么做的原因是什么?
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
我們知道,ARC下Block很多情況會被自動拷貝到堆,而在棧上的__block變量被復制到堆上之后,會將結構體__Block_byref_val_0
的成員變量__Block_byref_val_0 *__forwarding;
的值替換為堆上的__block變量的地址。因此使用 __forwarding 指針就是為了無論在棧還是在堆,都能正確訪問到該__block變量。
解析問題三:循環(huán)引用問題
循環(huán)引用的根本原因是Block和另一個對象互相持有,導致都無法釋放,經常碰到的是self(當前控制器),導致控制器無法釋放,內存泄漏嚴重。
解決循環(huán)引用問題主要有兩個辦法:
- 事前避免,我們在會產生循環(huán)引用的地方使用 weak 弱引用,以避免產生循環(huán)引用。
- 事后補救,我們明確知道會存在循環(huán)引用,但是我們在合理的位置主動斷開環(huán)中的一個引用,使得對象得以回收。
//巧神在YTKNetWorking是這么設計的:
//Block應該結束完的時候,手動把該釋放的Block置空
- (void)clearCompletionBlock {
// nil out to break the retain cycle.
self.successCompletionBlock = nil;
self.failureCompletionBlock = nil;
}
有時候納悶為什么AFN或像UIView的Block動畫不需要weakSelf也可以自己釋放掉。其實明白了無法釋放的原理,也就明白了。雖然Block強引用了self,但是self不強引用這個Block呀。
舉例說明(需要和不需要使用__weak打破循環(huán)引用):
typedef void(^Block)();
@interface SecViewController ()
@property (nonatomic , strong) NSString *str;
@property (nonatomic , copy) Block blk;
@end
@implementation SecViewController
- (void)viewDidLoad {
self.str = @"string";
//以下2種Block都不會被self強引用
//doSomthing1方法執(zhí)行完,pop后此控制器會被釋放掉
[self doSomthing1:^{
NSLog(@"str111:%@",self.str);
}];
//doSomthing2方法執(zhí)行完,pop后此控制器也會被釋放掉
[self doSomthing2:^(int a, int b) {
NSLog(@"str111:%@",self.str);
}];
//self持有此Block,要用__weak
__weak typeof(self) weakSelf = self;
self.blk = ^(){
NSLog(@"str111:%@",weakSelf.str);
};
//在doSomthing3方法中block參數賦值給self.blk,這樣block參數就間接被self強引用,需用weakSelf,用self會導致循環(huán)引用,當前控制器無法釋放;
//經常碰到的都是這種間接持有導致的循環(huán)引用問題
[self doSomthing3:^{
NSLog(@"str111:%@",weakSelf.str);
}];
}
- (void)doSomthing1:(void(^)())block{
if(block){
block();
}
}
- (void)doSomthing2:(Block)block{
if(block){
block();
}
}
- (void)doSomthing3:(Block)block{
if(block){
self.blk = block;
block();
}
}
-(void)dealloc{
NSLog(@"SecVC釋放了");
}