iOS-OC|iOS-OC block详解
block的本质的就是一段代码块,也是一个OC对象,它在合适的时间进行调用,它最终继承自NSObject,验证如下:
static int age = 10;
void (^block)(void) = ^(){
NSLog(@"这是一个block %d ",age);
};
block();
NSLog(@"-------%@--------",[block class]);
NSLog(@"-------%@--------",[self.myBlock class]);
NSLog(@"-------%@--------",[[[block class] superclass] superclass]);
NSLog(@"-------%@--------",[[[[block class] superclass] superclass] superclass]);
打印结果
2020-05-07 15:49:49.003135+0800 block基本认识[62095:2729467] 这是一个block 10
2020-05-07 15:49:49.003308+0800 block基本认识[62095:2729467] -------__NSGlobalBlock__--------
2020-05-07 15:49:49.003430+0800 block基本认识[62095:2729467] -------__NSGlobalBlock--------
2020-05-07 15:49:49.003542+0800 block基本认识[62095:2729467] -------NSBlock--------
2020-05-07 15:49:49.003651+0800 block基本认识[62095:2729467] -------NSObject--------
有如下代码,
void (^block)(void) = ^(){
NSLog(@"XXXXXXXXXXXXXXXXXXXXX");
};
生成C++之后,block结构:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
}struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
上述block的OC代码转换成C++之后,生成的结构是
__ViewController__viewDidLoad_block_impl_0
结构体,这个结构体包含两个结构体和一个构造函数.1>
__block_impl
:它有四个属性, isa证明它是oc对象,或者反过来说,只要含有isa
指针的,我们就可以认为它是oc对象,Flags
传的是0 ,Reserved
作为保留字段,*FuncPtr
是一个指针,指向代码块的首地址,在本文中 对应的就是NSLog(@"XXXXXXXXXXXXXXXXXXXXX");
的地址.1.1: 验证方法如下: ViewController中定义如下结构体
@implementation ViewController
static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __ViewController__viewDidLoad_block_impl_0 {struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
};
1.2
- (void)viewDidLoad
中写如下代码,并在NSLog
和struct __ViewController__viewDidLoad_block_impl_0 *myBlock
打下断点,当断点走到这个结构体时,lldb下打印FuncPtr
,int age = 10;
void (^block)(void) = ^(){
NSLog(@"这是一个block %d ",age);
};
struct __ViewController__viewDidLoad_block_impl_0 *myBlock = (__bridgestruct __ViewController__viewDidLoad_block_impl_0 *)block;
block();
1.3进行如下操作后,过掉这个断点
image.png
1.4 看到下图是断点走到
NSLog
处,也就是代码块的首地址,对比地址,发现是一样的,从而印证了
FuncPtr
保存的是代码块的首地址 文章图片
image.png
2>
__ViewController__viewDidLoad_block_desc_0
:它保存的是代码块的大小,所谓的代码块就是block的{ }
包含的内容,可以理解为一个方法,block就是通过调用*FuncPtr
保存的方法地址,来调用这个方法,reserved
是保留字段,Block_size
是这个代码块所占用的内存大小.3> 下面这个就是C++里面的构造函数,与OC里面的init相似,它是在block创建之初,初始化block
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
当block 引用外部变量的情况
int age = 10;
void (^block)(void) = ^(){
NSLog(@"这是一个block %d ",age);
};
结果如下:
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
int age;
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这里有一道经典题目,就是在block之前设置
age = 10
在block之后设置age = 20
,打印出来,age = 10
,看block结构不难得知,它里面创建了一个age属性,来存储外部的值,不管你外面怎么改,它里面的值都不受影响,这个操作叫做捕获(capture),【iOS-OC|iOS-OC block详解】4> 下面验证一下 什么情况下,block会捕获外部的值,
static int age = 10;
void (^block)(void) = ^(){
NSLog(@"这是一个block %d ",age);
};
直接看源码: 这种情况下block保存的是age的地址
int *age
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
int *age;
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
创建一个全局变量
@property (nonatomic,strong)NSString *name;
在block中打印, self.name = @"hahah";
void (^block)(void) = ^(){
NSLog(@"这是一个block %@ ",_name);
};
结果如下,block保存了
ViewController *self
的地址,因为_name
属性是通过self->name来访问的struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
ViewController *self;
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们知道oc方法都有两个隐式参数
self ,_cmd
,之所以我们能在方法里访问self,是因为这两个隐式参数,这种情况,block也是保存的指针,那么这里我们可以做个小结:4.1 一般的局部变量 前面有默认参数
auto
修饰,这种被auto修饰的局部变量为自动变量,即:在超过作用域之后,就被自动销毁,这种变量block会对它进行一次内存拷贝,也就是捕获(capture)4.2 被static 修饰的变量的特点,在内存中只有一份,在整个程序运行阶段都会存在,block会捕获它的内存地址,从block的设计角度来说,也没必要存它的值,
4.3 当
int age = 10
作为全局变量的时候,不会被捕获,因为age的作用域存在于整个文件,所以在哪里都可以访问.- (void)YPTest{
NSLog(@"---------------");
}
转成C++
static void _I_ViewController_YPTest(ViewController * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_j5_mv339p2534324qm28kc3v6rm0000gn_T_ViewController_399bc6_mi_2);
}
5> block的类型分为三种
__NSGlobalBlock__
,__NSMallocBlock__
,__NSStackBlock__
,分别为全局block,堆block,栈block,
那么什么情况下是全局block, 堆block, 栈block
5.1 全局block: 当局部变量被static修饰,block访问的是全局变量,block没有访问外部变量的时候,都是全局block,一句话, 当block内部没有访问auto变量,则为全局block ,此时block存放在数据段,一些静态变量,常量字符串等都放在这里.
5.2 栈block: 当访问局部变量的时候(MRC情况下), 访问auto变量,为栈block,如果此时我们对block进行一次copy操作,栈block会变成堆block,在ARC环境下,一旦block被强引用着,编译器会自动将栈上的block复制到堆上.(block存放在栈空间,指针地址存放在这里,它是内存连续的,系统自动管理内存,不需要手动释放)
文章图片
image.png 5.3 堆block: NSStackBlock调用了copy,变成堆block,(此时block存放在堆空间,需要我们手动管理内存,所有通过 malloc,new出来的变量,都是存放在这里,它是内存不连续的,优点是程序员可以时刻掌握变量的生命周期)
6> 当block内部包含对象的时候,block的结构有一丢丢的变化,
void (*copy)
,void (*dispose)
,这里涉及到block堆外部变量强引用,弱引用的问题static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
}
6.1 如果block从堆上移除,内部会调用dispose函数,函数会释放引用的auto变量,
6.2 如果是栈block: 不会对auto变量产生强引用,因为block在栈上,销毁的时机都不确定,对外部变量产生强引用没有意义.
6.3 如果是堆block: block里的copy函数就会调用,
_Block_object_assign((void*)&dst->person
,这个机制决定了,如果外部是强引用,则这个就强引用,反之则弱引用static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {
_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
7> 当
__block
修饰auto
变量时,会将自动变量包装成一个对象,并通过该对象改变自动变量的值(__block
不能修饰全局变量,静态变量(static)),代码如下:里面的__forwarding
指向它自己,通过(age->__forwarding->age) = 20
这种方式修改自动变量的值.struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
7.1 为什么访问不直接访问age属性,要通过forwarding->age这么访问,如图
文章图片
image.png 当block在栈上的时候, forwarding指向它自己,这样可以通过forwarding访问它自己,当block被
复制
到堆上之后, forwarding指向的是它堆上的自己,这时候它通过forwarding访问的就是堆上的age,这就是它这样设计的目的.
8> 循环引用-内存泄露的问题
当某个类持有block,而block内部又对该类强引用,则会出现block和该类无法释放的问题,从而出现内存泄露,解决的办法是使block内部引用的变量变成弱引用.
8.1 __unsafe_unretained : 当修饰的对象被销毁后,其指针不会被置空,再次访问的话,容易出现野指针错误.
8.2 __weak: 当修饰的对象被销毁后,其指针会被置空,其原理是runtime维护了一个哈希表,以对象的内存地址做key,以weak修饰的值作为value,当其对象被销毁的时候,runtime会通过内存地址,将该对象的值置为nil,当再次访问的时候,返回nil.
记录: 在使用clang转换OC为C++代码时,可能会遇到以下问题
cannot create __weak reference in file using manual reference
解决方案:支持ARC、指定运行时系统版本,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
推荐阅读
- Java|Java OpenCV图像处理之SIFT角点检测详解
- C语言浮点函数中的modf和fmod详解
- iOS中的Block
- 虚拟DOM-Diff算法详解
- LSTM网络层详解及其应用实例
- OC:|OC: WKWebView详解
- vue中的条件判断详解v-if|vue中的条件判断详解v-if v-else v-else-if v-show
- Vue组件之事件总线和消息发布订阅详解
- JS截取字符串的方法详解
- C语言进阶栈帧示例详解教程