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 = (__bridge  struct __ViewController__viewDidLoad_block_impl_0 *)block;
    block();

1.3进行如下操作后,过掉这个断点

image.png

1.4 看到下图是断点走到 NSLog处,也就是代码块的首地址,对比地址,发现是一样的,从而印证了 FuncPtr保存的是代码块的首地址
iOS-OC block详解_第1张图片
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),

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 block详解_第2张图片
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 block详解_第3张图片
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

你可能感兴趣的:(iOS-OC block详解)