Block使用注意點及常見問題淺析

本文將淺分析幾個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釋放了");
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容