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中写如下代码,并在NSLogstruct __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保存的是代码块的首地址 iOS-OC|iOS-OC block详解
文章图片
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存放在栈空间,指针地址存放在这里,它是内存连续的,系统自动管理内存,不需要手动释放)
iOS-OC|iOS-OC 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这么访问,如图

iOS-OC|iOS-OC block详解
文章图片
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

    推荐阅读