Block使用注意点及常见问题浅析

本文将浅分析几个Block使用问题:

  • 解析问题一:Block作为类变量属性时为啥用copy修饰?堆栈存储位置是怎样的?
  • 解析问题二:为什么需要__block 修饰自动变量后,才能在Block中更改?
  • 解析问题三:关于常见block中self的循环引用问题,可以用__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里面能修改全局或静态的变量,但是不能修改在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为自动变量/。自动变量只有一种存储方式,就是存储在栈中。由于自动变量存储在栈中,所以自动变量的作用域只在函数内,其生命周期也只持续到函数调用的结束。)

  • 循环引用问题:
    Block在方法中被定义时,该方法执行完是需要被释放的,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未执行完就会释放掉成为nil,为了防止这种情况出现,在Block内需要__strong对weakSelf强引用一下(更高级写法见RAC的@weakify(self),@strongify(self))。

//宏定义 - Block循环引用
#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];
});

这样不会造成循环引用是因为strongSelf是在Block里面声明的一个指针,当Block执行完毕后,strongSelf会释放,这个时候将不再强引用weakSelf,所以self会正确的释放。

解析问题一:Block的存储位置

在Objective-C语言中,一共有3种类型的block:
1._NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量。
2._NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。
3._NSConcreteMallocBlock 保存在堆中的block(从栈中copy过去的),当引用计数为0时会被销毁。

Block内捕获变量会改变自身存储位置,包括读取变量和__block这种写变量,两种方式(其实结果是一样的)。

【在MRC下】:存在栈、全局、堆这三种形式。
【在ARC下】:大部分情况下系统会把Block自动copy到堆上。

Block作为变量:
  • 方法中声明一个 block 的时候是在栈上;
  • 引用了外部局部变量或成员变量,并且有赋值操作(有名字),会被 copy 到堆上;
  • 赋值给附有__strong修饰符的id类型的类或者Blcok类型成员变量时;
  • 赋值给一个 weak 变量不会被 copy;
Block作为属性:
  • 用 copy 修饰会被 copy;
Block作为函数参数:
  • 作为参数传入函数不会被 copy,依然在栈上,方法执行完即释放;
  • 作为函数的返回值会被 copy 到堆;

例如:

-(void)method { 
//在ARC环境下,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指针(创建带名字的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放在全局区

解析问题二:为什么__block 修饰自动变量后,就能在Block中更改?

首先,为什么Block不能修改外部自动变量?

自动变量存于栈中,在当前方法执行完,会释放掉。一般来说,在 block 中用的变量值是被复制过来的,自动变量是值类型复制,新开辟栈空间,所以对于新复制变量的修改并不会影响这个变量的真实值(也称只读拷贝)。大多情况下,block是作为参数传递以供后续回调执行的。所以在你想要在block中修改此自动变量时,变量可能已被释放,所以不允许block进行修改是合理的。
对于 static 变量,全局变量,在 block中是有读写权限的,因为此变量存于全局数据区(非栈区),不会随时释放掉,也不会新开辟内存创建变量, 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里,发现是通过(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变量。

解析问题三:循环引用问题

循环引用的根本原因是Block和另一个对象互相持有,导致都无法释放,经常碰到的是self(当前控制器),导致控制器无法释放,内存泄漏严重。

解决循环引用问题主要有两个办法:
  • 事前避免,我们在会产生循环引用的地方使用 weak 弱引用,以避免产生循环引用。
  • 事后补救,我们明确知道会存在循环引用,但是我们在合理的位置主动断开环中的一个引用,使得对象得以回收。
//巧神在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打破循环引用):
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方法执行完,pop后此控制器会被释放掉
    [self doSomthing1:^{
        NSLog(@"str111:%@",self.str);
    }];
    //doSomthing2方法执行完,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会导致循环引用,当前控制器无法释放;
    //经常碰到的都是这种间接持有导致的循环引用问题
    [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释放了");
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容