前言
写这篇文章挣扎了很久,感觉很难简短而又精辟的写好block这篇文章,初探时走了一遍源码也看了一些帖子,但是探索完就忘了没什么感觉,只知道当需要修改block捕获的外部变量时,变量需要加__block修饰,但是为什么呢?block作为属性时,一般都是用copy修饰,但是又为什么呢?block最头疼的问题莫过于循环引用,我们总是无脑的用weak解决这个问题,但是弱引用真的是万能的吗?
Block是什么?
随便写一个block,clang编译一下看下底层结构
int a=10;
void(^Myblock)(void)=^{
NSLog("%d",a);
};
Myblock();
clang编译后的底层结构
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*Myblock)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((__block_impl *)Myblock)->FuncPtr)((__block_impl *)Myblock);
}
return 0;
}
分析:
block代码块最后被编译成了__main_block_impl_0结构体,结构体本质上就是对象,所以block也是一个有引用计数的对象- impl.FuncPtr = fp,结构体的构造函数把函数的实现fp赋值给了imp,也就是说block结构体保存了函数的实现,通过Myblock->FuncPtr(Myblock)调起函数实现说明block光申明不调用是没有用的。
- impl.isa = &_
NSConcreteStackBlock,说明block是存在于栈中的,但其实这个block是堆block,但为什么编译的时候是栈block?其实想想也能大概明白,堆内存是动态添加的,编译的时候不可能分配堆内存,此时只是临时的分配成栈block。
Block类型
既然block是对象,那么它在哪个内存区域呢?
全局block
void (^block)(void) = ^{
};
//或者
static int a=10;
void (^block)(void) = ^{
NSLog(@"%ld",a);
};
NSGlobalBlock全局block:在block内部不使用外部变量,或者只使用静态变量和全局变量
栈block
int b=10;
void (^__weak mblock)(void) = ^{
NSLog(@"%ld",b);
};
NSStackBlock栈block:在栈区,栈内有效,出栈后销毁。在内部使用外部变量,调用函数的时候,会在栈上开辟一块内存,函数执行完后,这块内存就销毁了也就是出栈了。所以不能赋值给强引用或者copy修饰的变量,因为函数调用完就销毁了,强引用的话会造成野指针。ARC下不特别指定,一般不会存在栈block
堆block
int b=10;
void (^mblock)(void) = ^{
NSLog(@"%ld",b);
};
NSMallocBlock堆block:在堆区,可以使用外部变量。堆block可以被强引用和copy,它的销毁取决于引用计数,跟我们创建对象一样,引用计数为0时就销毁。示例中mblock初始化后,其本身就是一次强引用。实际开发中百分之99的情况都是堆block,所以堆block是我们关注的重点。
Block引用外部变量
写个demo控制台打印情况如下:
//新建一个LGTeacher对象
@interface LGTeacher : NSObject
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end
LGTeacher* lgter=[[LGTeacher alloc]init];
NSLog(@"%@-%p",lgter,&lgter);
void(^Myblock)(void)=^{
lgter.hobby=@"test";
NSLog(@"%@-%p",lgter,&lgter);
};
Myblock();
输出:
<LGTeacher: 0x101218ad0>-0x7ffeefbff200
<LGTeacher: 0x101218ad0>-0x7ffeefbff1e8
分析:block外部lgter指针地址是0x7ffeefbff200,block内部lgter指针地址是0x7ffeefbff1e8,虽然他们都是指向相同的对象,但是指针地址却不相同,说明block内部的lgter已经是一个全新的变量,如果在block内部让lgter=nil,此时编译器会报错,因为我们的本意是想改变外部lgter的指针地址,但是在block内部此时的lgter已经不是外部的lgter,此时编译器会混乱。虽然block外部的指针地址和block内部的指针地址不一样,但是他们指向的是同一块内存,所以依然可以改变这块内存的值,比如此处lgter.hobby=@"test",这就是常说的值传递,不能改变外部指针,但是可以改变指针所指向的对象的值。
再来看一下使用__block修饰的情况。
@interface LGTeacher : NSObject
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end
__block LGTeacher* lgter=[[LGTeacher alloc]init];
NSLog(@"%@-%p",lgter,&lgter);
void(^Myblock)(void)=^{
lgter.hobby=@"test";
NSLog(@"%@-%p",lgter,&lgter);
lgter=nil;
};
Myblock();
输出:
<LGTeacher: 0x1007275d0>-0x7ffeefbff1f0
<LGTeacher: 0x1007275d0>-0x7ffeefbff1f0
block外部lgter指针地址是0x7ffeefbff1f0,block内部lgter指针地址也是0x7ffeefbff1f0,它们指向同一个LGTeacher对象0x1007275d0。说明使用__block修饰的对象,在block内部变量的指针地址没有发生改变,所以可以使用lgter=nil改变指针,因为内外部变量的指针是同一个,这就是常说的地址传递。
why?下面通过clang编译一下看下底层使用__block修饰和不使用__block有什么区别
这是没有使用__block修饰的底层结构,
block是一个结构体,结构体内部有一个属于结构体的对象lgter,外部的lgter对象作为参数传进block结构体的构造函数中,在构造函数中把外部的lgter赋值给block内部的lgter,注意这只是赋值,block内部的lgter在内存分配时有自己的指针地址。
再看一下使用__block修饰的对象
- 使用
__block修饰的对象LGTeacher已经被编译成了__block_byref_lgter_0结构体,在block构造函数中,把参数_block_byref_lgter_0结构体指针的__forwarding赋值给block中属性_block_byref_lgter_0,而__forwarding又是一个指向自己的__block_byref_lgter_0指针 - 虽然有点拗口,但是好好理解一下,简单总结就是这是一个
指针的传递,最终都指向了_block_byref_lgter_0结构体。_forwarding既可以指向自己,也可以指向复制后的自己,也就是说有了这个指针的存在,无论__block变量配置在堆上还是栈上都能够正确的访问__block变量。 - 仔细看一下如果使用__block修饰的对象,对象被编译成结构体的同时会有一个
__Block_byref_id_object_copy方法和___Block_byref_id_object_dispose方法,下面会分析到。
总结
__block修饰的对象是指针传递,无论__block变量配置在堆上还是栈上都能够正确的访问__block变量。没有用__block修饰的对象是值传递,block内部的变量和外部的变量不是同一个指针,只是值相同或者说指向的对象相同
block的循环引用
先想一下我们在申明一个block属性时,为什么总是用关键字copy修饰?
我们称block为代码块,他类似一个方法,方法是在内存的栈区,执行完就消失,它的内存管理是系统自动管理的。block不像OC中的类对象(在堆区),他也是在栈区的,我们没办法像控制对象一样控制他的消亡,所以我们常使用copy修饰block,系统会把该 block的实现拷贝一份到堆区,这样我们就可以像控制对象一样控制它的生命周期,在ARC,不使用copy修饰的block,系统也会拷贝block进堆区,所以说现在如果不特别制定block都是堆block。
还记得一开始我们写的那个demo吗,明明是一个堆block,但是编译的时候是一个栈block,那么是栈block是怎么变成堆block的呢?
int a=10;
void(^Myblock)(void)=^{
NSLog("%d",a);
};
Myblock();
汇编看一下调用了调用过程
汇编看流程,刚开始会调用objc_retainBlock,跟进
objc_retainBlock里又会调用Block_copy,我们研究的不就是堆block的拷贝吗?源码看下这个函数
void *_Block_copy(const void *arg) {
//block底层正在的结构是一个Block_layout
struct Block_layout *aBlock;
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
//block是否需要释放
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
//全局block直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// 栈 - 堆 (编译期),栈block的拷贝,堆block都是从栈拷贝过来的
size_t size = Block_size(aBlock);
//开辟堆block内存
struct Block_layout *result = (struct Block_layout *)malloc(size);
if (!result) return NULL;
//把栈block拷贝进堆block中
memmove(result, aBlock, size);
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
//省略...
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
//获取copy函数,其实是对对象的拷贝
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
分析:
- 我们上面通过clang分析了block底层结构是一个结构体,这个结构体具体的结构其实是
Block_layout结构体。 - 如果aBlock是
全局block,copy是直接返回 - 如果aBlock是
栈block,那么就会在堆区malloc开辟内存,并且把栈区block拷贝进新开辟的堆区。Block_call_copy_helper获取copy函数,copy里调用了_Block_object_assign方法改变外部对象的引用计数或内存拷贝。
_Block_object_assign
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
//对象 引用计数加1
case BLOCK_FIELD_IS_OBJECT:
_Block_retain_object(object);
*dest = object;
break;
//block类型,进行_Block_copy
case BLOCK_FIELD_IS_BLOCK:
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
//__block修饰的对象 会拷贝整个结构体进堆区
case BLOCK_FIELD_IS_BYREF:
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
*dest = object;
break;
default:
break;
}
}
- 如果
外部变量是对象,_Block_retain_object使外部对象引用计数加1,所以在block内使用外部变量,比如self就需要使用弱引用,否则block内部会强引用self使得引用计数加1。 - 如果外部变量是Block类型的,执行_Block_copy拷贝block,这里一定要注意,这里有坑点,下面会有例子。
- 如果外部变量是
__block修饰的,__block修饰的对象会被编译成一个block_byref结构体,那么会拷贝整个结构体进堆区。
示例分析
经过上面对block的分析,我们已经有一个大概的认知与理解,其最终目的还是要在开发中使用和避免犯错,下面看几个示例巩固和理解。
对象的引用计数理解
NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1
void(^strongBlock)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
strongBlock();
void(^__weak weakBlock)(void) = ^{ // + 1
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
weakBlock();
void(^mallocBlock)(void) = [weakBlock copy];
mallocBlock();
输出:
1
3
4
5
分析:上面输出objc对象的引用计数,第一次输出1能理解,第二次输出为什么是3呢?上面我们分析得出如果外部变量不使用__block修饰,那么在block内部是值传递,会生成一个新的指针变量指向这个对象,所以对象的引用计数会加1,strongBlock编译底层其实是栈block,运行时栈block拷贝进堆区调用_Block_retain_object函数使得对象的引用计数又加1,所以是3。至于第三次输出,特别指定这是一个栈block,其内部又会有一个新的指针变量指向objc,所以加1。第四次,将一个栈block拷贝进堆区,对象引用计数加1。
如果把上面的对象使用__block修饰呢?
__block NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1
void(^strongBlock)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
strongBlock();
输出:
1
分析:可以看到引用计数永远为1,并不会使对象的引用计数增加,因为这是指针传递。还记得上一篇关于弱引用的文章吗,weak可以解决block循环引用的本质就是不会使对象的引用计数增加,而且弱引用对象的引用计数是系统的弱引用表管理的。这里是不是感觉__block也有相同的作用,也可以解决循环引用。
内存拷贝的理解
int a = 0;
void(^ __weak weakBlock)(void) = ^{
NSLog(@"-----%d", a);
};
struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
// 深浅拷贝
id __strong strongBlock = [weakBlock copy];
blc->invoke = nil;
void(^strongBlock1)(void) = strongBlock;
strongBlock1();
输出
0
分析:weakBlock是栈block,blc通过类型强转也是一个跟weakBlock指向同一片内存空间的栈block,strongBlock是weakBlock的内存拷贝是堆block,调用blc->invoke=nil把栈block内存清空,但是这不影响堆block的内存,所以能够输出
堆栈释放差异
- (void)blocktest{
NSObject *a = [NSObject alloc];
//仔细看此处的block不是一个块
void(^weakBlock)(void) = nil;
{//临时作用域
// 栈block
void(^__weak strongBlock)(void) = ^{
NSLog(@"---%@", a);
};
weakBlock = strongBlock;
NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
}
weakBlock();
}
输出:
1 - <__NSMallocBlock__: 0x281e830c0> - <__NSStackBlock__: 0x16b80cf10>
---(null)
分析:weakBlock不做特别指定是一个堆block,临时作用域内strongBlock是一个栈block,堆block被赋值成栈block,注意这里不是copy,只是指向同一片内存空间的赋值。分别打印__NSMallocBlock__和__NSStackBlock__类型能理解,为什么打印a是null呢?lldb断点看下
在81行打断点,此时
weakBlock和strongBlock指向同一片栈内存空间,但是断点过了82行后,此时strongBlock栈内存被释放了,意思strongBlock生命周期在临时作用域内,出了作用域就释放了,既然栈内存释放了,堆block又指向这片内存,为什么会打印null呢?不应该是野指针吗,这点目前还不清楚,希望知道的指导一下。要想不打印为null,可以这样weakBlock = 【strongBlock copy】把栈block拷贝进堆,这里一定要注意copy和“=”的本质区别,"="指向的是同一块内存空间。
避免循环引用
造成循环引用的本质是对象的引用计数始终不为0,导致对象该释放的时候释放不掉,从而导致内存泄漏。所以我们可以使用weak或者__block修饰对象,让对象的引用计数不增加就可以避免循环引用。如下
//方式1
__weak typeof(self) weakself=self;
void(^astrongBlock)(void)=^(void){
__strong typeof(weakself) strongself=weakself;
NSLog(@"count=%ld",(long)CFGetRetainCount((__bridge CFTypeRef)strongself));
};
astrongBlock();
//方式2
__block ViewController* vc=self;
void(^bstrongBlock)(void)=^(void){
NSLog(@"count=%ld",(long**)CFGetRetainCount((__bridge CFTypeRef)vc));
vc=nil;
};
bstrongBlock();
//方式3
void(^cstrongBlock)(UIViewController* vc)=^(UIViewController* vc){
NSLog(@"count=%ld",(long)CFGetRetainCount((__bridge CFTypeRef)vc));
};
cstrongBlock(self);
分析:
- 方式1使用
weak修饰对象,不会使self的引用计数增加,同时弱引用对象weakself是系统弱引用表管理的,注意我们一般会在block内部使用__strong再强引用一次self,此时self引用计数会加1,如果我们在block中操作了一些耗时的工作,但是self被提前释放了,那么弱引用对象weakself也就释放了,那么就会造成奔溃,而在block内部__strong修饰的是一个零时变量,出了作用域就自动销毁了,self引用计数又会减1。 - 方式2使用
__block修饰一个对象,不会增加对象的引用计数,注意一定要在block内部把这个对象释放掉,weak弱引用对象的释放是系统完成的。 - 方式3通过传递一个
临时变量,我们知道临时变量是在栈区的,是系统自动释放了
强弱共舞是万能的吗?
我们经常使用__weak和__strong组合解决block循环引用,但这真的是万能的吗?看下面这个例子会闷逼,定义两个block作为控制器self的属性,即doWorkblock和doStudentblock,分别打印self和strongself的引用计数
__weak typeof(self)weakSelf =self;
NSLog(@"self引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)self));
self.doWorkblock = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"strongSelf引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)strongSelf));
weakSelf.doStudentblock = ^{
NSLog(@"strongSelf引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)strongSelf));
};
weakSelf.doStudentblock();
};
self.doWorkblock()
NSLog(@"self引用计数:%ld",CFGetRetainCount((__bridgeCFTypeRef)self));
控制台输出:
self引用计数:21
strong引用计数:22
strong引用计数:25
self引用计数:22
分析:控制台可以看到一开始self应用计数为21,最后self引用计数为22,很明显造成了循环引用,why?
strongSelf是一个临时变量,强持有self,所以引用计数+1,为22。进入doStudentblock后,在block内部引用外部变量,根据上面的分析引用计数应该+2后是24,但是为什么是25呢?特别注意doWorkblock捕获了外部变量doStudentblock,当外部变量是block类型时,会拷贝这个block进堆区,doStudentblock里对strongself强引用即对self强引用,所以引用计数又会加1,所以为25。- 如果doStudentblock
不作为属性呢?在doWorkblock内部自定义一个doStudentblock就不会造成循环引用,因为不存在block拷贝 - 如果在
doStudentblock里面不引用Strongself会造成循环引用吗?如果不强引用strongself,在block拷贝时就不会拷贝strongself,自然就不会使引用计数增加
NSTimer循环引用
有必要把NSTimer造成循环引用单独拎出来说一下的,一个是比较特殊一个是使用的时候会不注意。首先NSTimer是基于RunLoop的,以scheduledTimerWithTimeInterval:开头的方法会将NSTimer加到当前runloop的default mode上,而以timerWithTimeInterval:开头的方法,则需要使用runloop的addTimer:方法,将其手动加到runloop上。因此,这里通常有一个注意的点:即runloop的UITrackingMode下,定时器会失效。解决办法即将定时器加到runloop的commonModes上即可。
写个会造成循环引用的demo如下:
控制器
A push进控制器testvc中,在testvc中创建一个mytimer定时器,当控制器testvc pop返回上一个控制器时,理论上会调用dealloc方法,但实际情况并不会调用dealloc方法,因为mytimer强引用self,self又强引用mytimer,如果此时repeats=NO,并不会造成循环引用,但是一旦repeats=YES,那么mytimer对象就要一直持有self,从而造成循环引用,循环引用下self不会释放,只有self释放了才会调用dealloc方法,所以dealloc方法可以检测self是否正常释放。那么如何才能让dealloc正常调用呢?
中间对象解循环
分析:可以定义个
中间对象,注意这个中间对象必须强引用,不能是一个临时对象,否则执行完就销毁了,runtime动态给中间对象添加timego方法,timer的target设置为这个中间对象,这样就可以解除self循环引用,dealloc也能够正常调用。
消息转发解循环
@interface TimeCenterObj()
@property(nonatomic,weak)id weakobj;
@end
@implementation TimeCenterObj
+(instancetype)initWithObjc:(id)weakobj{
TimeCenterObj* myobj= [[TimeCenterObj alloc]init];
myobj.weakobj=weakobj;
return myobj;
}
-(id)forwardingTargetForSelector:(**SEL**)aSelector{
return self.weakobj;
}
分析:创建一个中间对象TimeCenterObj,定义一个弱引用weakobj去持有self,利用弱引用的特性解除循环,最最主要的是要定义一个消息快速转发的方法forwardingTargetForSelector(原理见文章消息转发),在这个方法里指定处理这个方法的实体为weakobj,也就是self本身。当然也可以添加慢速转发方法。
使用如图:
block弱引用解循环
ios10.0之后,苹果应该是意识到NSTimmer会造成循环引用,所以添加了一个新的API,可以在这个新API里弱引用self。
__weak typeof(self) weakself=self;
self.mytimer=[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakself timego];
}];
[self.mytimer fire];
但是这个API是ios10.0之后才有的,所以要兼容ios10.0以下的话需要使用分类动态创建一个方法,这里也推荐使用这种方法来解决NSTimer的循环引用
@implementation NSTimer (weakTimer)
+(NSTimer*)wgy_scheduledTimerWithTimeInterval:(NSTimeInterval)time repeats:(BOOL)repeat block:(void(^)(NSTimer * _Nonnull))block{
return [self scheduledTimerWithTimeInterval:time target:self selector: @selector(wgy_time:) userInfo:[block copy] repeats:repeat];
}
+(void)wgy_time:(NSTimer*)time{
void(^timeblock)(NSTimer* time)=time.userInfo;
if(timeblock){
timeblock(time);
}
}
注意:这里把target设置为NSTimer自己,SEL为添加的类方法wgy_time,在类方法里调用传进来的block。