本章提纲
1、Block的几种类型
2、Block常见试题
3、Block的循环引用问题
1、Block的几种类型
Block分为三种类型,分别是GlobalBlock
,MallocBlock
,StackBlock
。
-
GlobalBlock
位于全局区,在Block
内部不使用外部变量,或者只使用静态变量或者全局变量。 -
MallocBlock
位于堆区,在Block
内部使用局部变量或者OC属性,并且赋值给强引用或者Copy
属性变量。 -
StackBlock
位于栈区,与MallocBlock
一样,可以在内部使用强引用或者OC属性。但是不能赋值给强引用或者Copy
修饰变量。
1.1 GlobalBlock类型实例
全局Block
类型,不使用局部变量,或者内部使用的变量是全局的。
Block内部无操作
void (^block)(void) = ^{
};
NSLog(@"%@",block);
打印结果:<__NSGlobalBlock__: 0x100b72100>
Block内部引用静态变量
static int jingtai = 3;
void (^block)(void) = ^{
NSLog(@"%d",jingtai);
};
NSLog(@"%@",block);
打印结果:<__NSGlobalBlock__: 0x10430d100>
1.2 MallocBlock类型实例
堆Block
,内部使用局部变量或者OC属性,并且赋值给强引用
Block或者Copy变量。
使用局部变量并且强引用
int a = 2;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
NSLog(@"%@",block);
打印结果:<__NSMallocBlock__: 0x600001dc8b40>
block,默认是强引用。
Copy变量的情况
int a = 10;
void (__weak ^block)(void) = ^{
NSLog(@"%d",a);
};
NSLog(@"%@",[block copy]);
打印结果:<__NSMallocBlock__: 0x600002d307e0>
如果我不进行[block copy]
的操作,直接打印这个block的话,是一个栈Block,经过copy,打印就是堆Block。
1.2 StackBlock类型实例
栈Block
和堆Block
很相似,区别在于赋值的对象是强引用还是弱引用。还是上边那个例子🌰,如果我进行Copy
操作,打印就是一个stackBlock
。
int a = 10;
void (__weak ^block)(void) = ^{
NSLog(@"%d",a);
};
NSLog(@"%@",block);
打印结果:<__NSStackBlock__: 0x30d7e5548>
2、Block常见试题
- Block的引用计数考题
下面代码输出的结果是。
- (void)blockDemo{
NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
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
。
解析
:
1、首先是创建一次,引用计数+1,所以第一次打印是引用计数是1。
2、然后来到这个strongBlock
关键的地方,它是一个堆Block
,strongBlock本身引用了一次 底层实际执行了copy操作,所以又引用一次 +2,此时引用计数变成3。
3、然后后面来到weakBlock
这里,它是弱引用,是个栈Block,底层不会拷贝 所以+1,此时引用计数变成了4。
4、最后mallocBlock是由weakBlock手动copy
一次得到的,所以再加1,引用计数为5。
- Block的浅拷贝
下面代码运行结果
- (void)blockDemo1{
int a = 0;
void(^__weak weakBlock)(void) = ^{
NSLog(@"a:%d", a);
};
struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
id __strong strongBlock = weakBlock;
NSLog(@"weakBlock:%@",weakBlock);
NSLog(@"strongBlock:%@",strongBlock);
blc->invoke = nil;
void(^strongBlock1)(void) = strongBlock;
NSLog(@"strongBlock1:%@",strongBlock1);
strongBlock1();
}
运行结果:程序crash
解析
:weakBlock
是一个栈类型的Block,blc
的初始化是直接把自己的指针指向了这个weakBlock
,他们指向的是同一块内存空间
。然后后面的strongBlock
赋值和blc
的情况是相似的,也是指针指向了那块内存,所以
blc->invoke置为空相当于把那块内存值为空了,所以此刻不管是blc
的指向还是weakBlock
的指向,或者是strongBlock
的指向,他们都指向了一块nil空间,是不可以再去访问的,所以会崩溃了(EXC_BAD_ACCESS)。
上述代码修复
:
- (void)blockDemo1{
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();
}
最后是调用strongBlock1,所以strongBlock1赋值时可以进行copy,让它在堆空间上,开辟了另外一块空间,原来的那块置为nil
不会对新的空间有影响。
- Block的堆栈释放差异
下面代码的输出是什么:
- (void)blockDemo3{
int a = 8;
void(^__weak weakBlock)(void) = nil;
{
// 栈区
void(^__weak strongBlock)(void) = ^{
NSLog(@"---%d", a);
};
weakBlock = strongBlock;
NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
}
weakBlock();
}
运行结果: 1 - <__NSStackBlock__: 0x308640520> - <__NSStackBlock__: 0x308640520>
---8
解析
:
-
weakBlock
是一个栈区的Block但是初始化为nil,一开始是空。 - 然后下面的
{}
是一个代码块,这个主要影响部分代码的作用域。 - strongBlock是栈区的Block,初始化正常。
- 然后
weakBlock = strongBlock;
把weakBlock
指向了strongBlock
指针指向的的内存。 - 所以后面打印weakBlock和strongBlock他们的地址是一样的
0x308640520
。 - 然后出作用域,调用
weakBlock()
,因为还有指针weakBlock
指向0x308640520
,但是它是个弱引用,strongBlock出了整个函数才会被释放,所以后面weakBlock()还可以调用代码块,调用的时候代码块没有被释放。
如果上述代码做简单的修改,把strongBlock前边的__weak
去掉:
- (void)blockDemo3{
int a = 8;
void(^__weak weakBlock)(void) = nil;
{
// 堆
void(^strongBlock)(void) = ^{
NSLog(@"---%d", a);
};
weakBlock = strongBlock;
NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
}
weakBlock();
}
那么运行就会崩溃了。原因就是,堆空间,出了第一个花括号的作用域,strongBlock就被释放掉了,而weakBlock是弱持有,不是强持有,所以就调用不到块了。花括号去掉就没问题了。截图看下花括号对比前后的结果。
去掉花括号之前 去掉花括号之后
通过血与泪的教育😭😭😭,这道题告诉我们了一个很重要的人生哲理!就是
花括号不要乱写!!!!!!
3、Block的循环引用问题
正常情况下,一个对象A如果强持有对象B,那么B的引用计数retainCount会进行+1的操作,当A对象释放时会发送delloc
给B,此时B的引用计数retainCount会进行-1,当B的引用计数为0的时候,B也会被释放掉。
但是,总有那么些特殊的情况,A已经持有了B的时候,B非得又持有了A,他们相互持有了,都等着对方先放手,此时就形成了一个环,不靠外力是断不开的,这种情况,就发生了循环引用
。
3.1 循环引用的形成
下面来看一段经典的循环引用
- 经典循环引用示例
- (void)test1{
self.name = @"ABC";
self.block = ^{
NSLog(@"%@",self.name);
};
}
- 上述示例
self
->block ,block
->self,相互持有,形成了环,delloc不走。
3.2 解决循环引用
通常我们解决循环引用的方式有三种
- 进行_ weak弱引用,有时候要和 _strong配合使用
- 强制断链,将其中一个置为nil。
- 将持有者改为Block的参数进行传递使用。
3.2.1 _ _weak弱引用
上述代码,我们进行简单的改造,就可以解决循环引用的问题,通过用_ _weak修饰的方式。
image.png
有时候在block的内部会有需要进行异步线程的操作,为了避免使用的对象提前被释放,通常会在内部添加_ _strong来增加一次引用计数。
3.2.2 强制断链
强制断链就是在使用完对象后,强制设置为空。如下:
强制断链
但是也会有问题,这种情况必须block被调用,代码块中的代码被执行才会解决循环引用,容易引发bug,如果不调用block中的内容循环引用还是存在的。
Block未被调用的情况所以这样dealloc是没有走的。
3.2.3作为参数进行传递
我们把上述的vc
作为参数传递到block中再进行使用也是可以解决这个问题的。
可以看到作为参数传进来是没有问题,参数就避免了Block对外部变量的捕获的问题。
这篇文章对Block
日常的一些使用情况做了一个简单概括,下一篇会深入研究Block
的结构,底层原理等。
网友评论