block

160 阅读5分钟

block是oc中比较常用的一个结构,被用来替换函数使用,然而其使用过程中会出现不同于函数的一些问题,本章就来研究一下block的一些问题 -- 案例demo

block种类

block中一共有3中block类型,分别为_NSConcreteStackBlock、_NSConcreteMallocBlock、_NSConcreteGlobalBlock

_NSConcreteStackBlock(栈block):

只用到外部局部变量,且没有强指针引用的block都是StackBlock,且没有被copy(ARC下block使用NSObject对象会被copy到堆), StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。

_NSConcreteMallocBlock(堆block):

有强指针引用(例如:控制器持有)或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,其生命周期由程序员控制

_NSConcreteGlobalBlock(全局block):

没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,其与应用共存活

block使用以及源码分析

block在使用过程,有时会用到外部的变量,因此block又具备了一个额外的操作,自动捕获用到的外部变量,且捕获的变量大致分为两种形式:正常变量、静态全局变量、__block修饰的变量

正常变量

正常变量,即除了全局静态变量、__block修饰的变量等都算正常变量,包括:平时创建的临时变量,或者使用self都算正常变量

正常变量在block是不可以进行值的更改的,但是可以修改指针指向的对象的内容,例如: self.name,但是不能直接修改self,好在编译器直接避免这这种问题,可以通过__block的方式修改

如果不通过__block修饰的话,只能读取变量的值,测试代码如下所示

int b = 2;
ViewController *vc = self;
void (^testBlock)(int a) = ^(int a) {
    NSLog(@"%d", a+b);
    vc.name = @"啦啦啦";
};
testBlock(20);

为了研究block对他们做了什么,下面通过clang命令查下一下源码

clang -rewrite-objc [编译文件名.m] -o [生成文件名.c] 

执行完毕上面代码后,生成了block捕获正常变量的源码,如下所示:

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int b;
  ViewController *vc;
  __ViewController__viewDidLoad_block_impl_0(void *fp, 
      struct __ViewController__viewDidLoad_block_desc_0 *desc, 
      int _b, ViewController *_vc, int flags=0) : b(_b), vc(_vc) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __ViewController__viewDidLoad_block_func_0(
    struct __ViewController__viewDidLoad_block_impl_0 *__cself, int a) {
    int b = __cself->b; // bound by copy
    ViewController *vc = __cself->vc; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_nmjyqqf54dsf35ycqc60bbz40000gn_T_ViewController_035e4c_mi_0, a+b);
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)vc, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_hr_nmjyqqf54dsf35ycqc60bbz40000gn_T_ViewController_035e4c_mi_1);
    }

看到了上面的代码,其中bound by copy就是对我们用到的参数的一个copy,其只copy了内容或者指针到block具局部使用,已经是另一个地址的变量了,因此对其内部的值更改无效(相信学习过c语言的小伙伴们都有这个经历)

所以在里面只能对这些正常变量进行读的操作,或者将读的操作加工应用到其他可以更改的变量身上

静态全局变量

静态和全局变量跟随应用一直存在,block使用的 过程中可以直接获取进行操作,在block相当于和外部使用同一个变量,不用关心指针或者值的问题,直接更改即可

测试代码如下所示:

int global_a = 0;
static int static_b = 2;

- (void)viewDidLoad {
    void (^testBlock)(void) = ^() {
        global_a++;
        static_b++;
    };
    testBlock();
}

通过clang命令生成后的block源码如下所示

int global_a = 0;
static int static_b = 2;

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;
  }
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
        global_a++;
        static_b++;
    }

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    void (*testBlock)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
}

可以看到上面连捕获copy的代码都没有了,直接在block实现中对全局变量的操作(__ViewController__viewDidLoad_block_func_0函数)

__block修饰变量

通过__block修饰的变量,其会对__block修饰的变量进行处理,创建新的结构体对变量地址和值进行保存,可以查看下面案例

测试案例代码如下所示:

__block int b = 2;
NSLog(@"%p", &b);
__block ViewController *vc = self;
void (^testBlock)(int a) = ^(int a) {
    NSLog(@"%p", &b);
    NSLog(@"%d", a+b);
    vc.name = @"啦啦啦";
};
testBlock(20);

通过clang编译后的代码如下所示

//编译后的b结构
struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b;
};
//编译后的vc结构
struct __Block_byref_vc_1 {
  void *__isa;
__Block_byref_vc_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 ViewController *vc;
};

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __Block_byref_b_0 *b; // by ref
  __Block_byref_vc_1 *vc; // by ref
  __ViewController__viewDidLoad_block_impl_0(void *fp, 
      struct __ViewController__viewDidLoad_block_desc_0 *desc, 
      __Block_byref_b_0 *_b, __Block_byref_vc_1 *_vc, int flags=0) : 
      b(_b->__forwarding), vc(_vc->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __ViewController__viewDidLoad_block_func_0(
    struct __ViewController__viewDidLoad_block_impl_0 *__cself, int a) {
  __Block_byref_b_0 *b = __cself->b; // bound by ref
  __Block_byref_vc_1 *vc = __cself->vc; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_nmjyqqf54dsf35ycqc60bbz40000gn_T_ViewController_78126d_mi_0, a+(b->__forwarding->b));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)(vc->__forwarding->vc), sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_hr_nmjyqqf54dsf35ycqc60bbz40000gn_T_ViewController_78126d_mi_1);
    }

通过clang后的代码,可以看到bound by ref,而不是之前的bound by copy了,这是直接取到了对象的地址,即直接饮用了当前对象

另外可以看到上面的源码取值的时候都是从结构体中的__forwarding中取值,那是怎么回事呢?

下面分别是我们打印出来的testBlock的内存情况图、int b的指针变化情况图

image.png

image.png

可以看到testblock的内存结构,发现其为__NSMallocBlock类型,即堆类型,可以大胆猜测,连变量带整个block都copy到了堆上,__block修饰的对象,直接被放到了新的结构体中,默认外层结构体存放的是栈中变量的地址信息,而__forwarding存放的是其所指向的存放到堆中的实际信息;即:viewcontroller为例,外部保存的是指向 viewContrller 的临时指针,__forwarding 保存的 viewController在堆中的实体

注:我们以往创建一个NSObject对象,都是创建到堆中,栈中声明的临时变量只是一个指向堆中对象地址的基本类型的指针罢了,使用中会自动引用堆中对象,即随着栈区空间的释放而释放,不走对象release那套

引用

通过上面编译后的block源码可以看到,block为了保证能够正常访问变量内容,会被变量进行额外引用一次,因此需要注意下block可能会导致的引用环问题,尽量避免内存泄露