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

xiaoxiao2021-02-28  62

原写于简书Block使用注意点及常见问题浅析,现转在

本文将浅分析几个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(@"%@",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释放了"); }
转载请注明原文地址: https://www.6miu.com/read-51722.html

最新回复(0)