您的位置:首页 > 移动开发 > IOS开发

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

2017-08-31 18:14 232 查看
原写于简书Block使用注意点及常见问题浅析,现转在CSDN

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

- 问题一:Block作为类变量属性时为啥用copy修饰?堆栈存储位置是怎样的?

- 问题二:为什么需要__block 修饰自动变量后,才能在Block中更改?

- 问题三:关于常见block中self的循环引用问题,可以用__weak打破强引用链;那么为什么AFN或像UIView动画不需要__weak修饰self?

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(@"%@"
d4c9
,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释放了");
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ios block