iOS进阶

探究 Block 的奥秘

2018-08-30  本文已影响6人  Miss_QL

闲来无事,总结了一下 block 的几点知识,以作巩固,欢迎指正。

一、block 的本质
block 本质上是一个 OC 对象,它内部有一个 isa 指针。
block 是封装了函数调用以及函数环境的 OC 对象。

要研究 block 在底层编译器具体的源码实现方式,可以使用 llvm 编译器中的 clang 命令clang-rewrite-objc main.m查看,它会将 OC 的源码 main.m 文件改写成 C++ 的 main.cpp 文件(但只可作为参考,因为 llvm 编译器生成的中间文件和 C++ 文件还是有所差异的)。更加完整的命令是:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m,感兴趣的可以试一试。
从源码中可以发现,block 在转换成 C++ 的时候显示为:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;  //block中调用的函数的地址
};

// block的描述信息
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;  //block所占的内存大小
}

// block的源码结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; 
};

显而易见,block 结构体的第一个元素是结构体 __block_impl,而 __block_impl 结构体的第一个元素是 isa,由此可知 block 实际上是一个 OC 对象。
其次,我们都知道 OC 对象都有其类型,可调用-class方法查看对象所属的类。

void (^block)(void) = ^{
  NSLog(@"hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
控制台打印结果如下: image.png

明显看出,__NSGlobalBlock __ 继承自__NSGlobalBlock,__NSGlobalBlock 继承自 NSBlock,NSBlock 继承自NSObject,即 block 所属的类最终是继承自 NSObject 的,所以说 block 是 OC 对象。

二、block分类
OC 中有3种类型的 block,可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类。
1、_NSConcreteGlobalBlock 全局的静态 block,在常量区(数据区域),不会访问任何外部变量。
2、_NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
3、_NSConcreteMallocBlock 保存在堆中的 block,动态分配内存,当引用计数为 0 时会被销毁。

// _NSConcreteGlobalBlock:没有访问auto变量
void (^block)(void) = ^{
  NSLog(@"hello");
};
NSLog(@"%@", [block class]); //输出__NSGlobalBlock__

// _NSConcreteStackBlock :访问了auto变量
int a = 10;
NSLog(@"%@", [^{
  NSLog(@"hello %d", a);
} class]); //输出__NSStackBlock__

// _NSConcreteMallocBlock:__NSStackBlock__调用copy方法
int a = 10;
void (^block)(void) = [^{
  NSLog(@"hello %d", a);
} copy];
NSLog(@"%@", [block class]);//输出__NSMallocBlock__
每一种类型的 block,调用 copy 后的结果如下所示: image.png

注意!在ARC环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如以下情况:
1、block 作为函数返回值时
2、将 block 赋值给 __strong 指针时
3、block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
4、block 作为 GCD API 的方法参数时

当 block 内部访问了对象类型的 auto 变量的时候,会产生两种可能:
1、如果 block 是在栈上,将不会对 auto 变量产生强引用;
2、如果 block 被拷贝到堆上,会调用 block 内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)操作,类似于 retain (形成强引用、弱引用)。
如果 block 从堆上移除,会调用 block 内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的 auto 变量,类似于 release。

三、block 的变量捕获
问题1:下面的代码输出结果为何?

int a = 10;
void (^block)(void) = ^{
  NSLog(@"a = %d", a);
};
a = 20;
block();

通过终端命令,可以查看到编译后的最终代码如下:

// 当前block的结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int a = 10;
//此时传入的参数a=10,通过__main_block_impl_0函数传入的最后一个参数为a,被block结构体内部的int a;元素所捕获,故此block结构体内部的a元素保存的是传入的参数a的值
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
//再修改外部参数a=20也无用,因为这并不影响block结构体内部a元素的值
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
在源码的注释中已经解释了实际的执行逻辑,故此可以看出输出的结果为: image.png

问题2:若将上述问题中的int a = 10;修改成static int a = 10;,结果是否会有所不同?
同上,粘代码便一目了然(截取部分 OC 代码与 C++ 代码)。

/***** 编译前的OC代码 *****/
static int a = 10;
void (^block)(void) = ^{
  NSLog(@"a = %d", a);
};
a = 20;
block();

/***** 编译后的C++代码 *****/
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static int a = 10;
//此时传入的参数a=10,通过__main_block_impl_0函数传入的最后一个参数为&a,被block结构体内部的int *a;元素所捕获,故此block结构体内部的指针a元素保存的是传入的参数a的地址值
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
//再修改外部参数a=20,在调用block函数时,因其结构体内部a元素保存的是外部参数a的地址值,此时该地址值所指向的外部参数a=20
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
在源码的注释中已经解释了实际的执行逻辑,故此可以看出输出的结果为: image.png

问题3:若将上述问题中的int a = 10;声明为全局变量,结果又是否会有所不同?
同上,粘代码便一目了然(截取部分 C++ 代码)。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
很明显,block 结构体内部并没有捕获全局变量a的值或者地址,而是直接访问全局变量的,故此可以看出输出的结果为: image.png

总结:
为了保证 block 内部能够正常访问外部的变量,block 有个变量捕获机制:
全局变量:不捕获到 block 内部,直接访问
局部变量:捕获到 block 内部,其中auto变量通过值传递访问,static变量通过指针传递访问

备注:auto自动变量,离开作用域就会被销毁。

- (void)blockTest {
    // block外面的a和block内部的a实际上不是同一个对象
    int a = 10; //a在栈上
    NSLog(@"outside a === %p", &a);
    void (^blocka)(void) = ^(void) {
        NSLog(@"a ==== %p", &a); //a被copy了一份,实际上C方法的代码块中传递的是copy得到的a的值
    };
    blocka();
    
    __block int b = 10; //b在栈上
    NSLog(@"outside b === %p", &b);
    void (^blockb)(void) = ^(void) {
        NSLog(@"b ==== %p", &b); //b被copy到堆上,实际上C方法的代码块中传递的是copy得到的b的指针地址
    };
    blockb();
}

四、__block 的作用

int age = 10;
void (^block)(void) = ^{
  NSLog(@"age is %d", age);
};
block();

毫无疑问,控制台会输出 “age is 10”。如果想在 block 代码块中修改局部变量 age 的值为20,该怎么做?
直接修改肯定会报错,当然可以直接将 age 定义为 static 变量或者全局变量,问题就会迎刃而解。但是这样的话,age 就会一直存在在内存的全局区中,这并不是最理想的状态,希望 age 还是一个临时变量,在不用的时候会自动销毁。这时候可以使用 __block 修饰 age 即可。

__block int age = 10;
void (^block)(void) = ^{
  age = 20;
  NSLog(@"age is %d", age);
};
block();

此时控制台会输出 “age is 20”。用 __block 的好处是不会修改变量的性质,age 还是一个 auto 类型的自动变量。

__block 可以用来用于解决 block 内部无法修改 auto 变量值的问题。
__block 不能修饰全局变量、静态变量(static)。
编译器会将 __block 变量包装成一个对象。
上面的代码中__block int age = 10;会编译成下面的代码结构:

struct __Block_byref_age_0 {
  void *__isa;
  __Block_byref_age_0 *__forwarding; //指向结构体自身的指针
  int __flags;
  int __size;
  int age; //这个age才是对应外部 __block 修饰的局部变量,共享同一块内存地址
};

//block 内部有个 *age 指针,指向 __Block_byref_age_0 结构体,__Block_byref_age_0 结构体内部有 int age ;通过 __Block_byref_age_0 结构体的 __forwarding 指针找到其内部 age 的内存地址,并修改其值,完成修改。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
};

__block 的内存管理
当 block 在栈上时,并不会对 __block 变量产生强引用。
当 block 被拷贝到堆上时,会调用 block 内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对 __block 变量形成强引用(retain)。

五、block 的使用
1、作为对象的属性
用 block 作为属性,保存一段代码块,可以在任何想用它的地方随时使用。MVVM 设计模式中,在绑定 view 和 VM 的时候就常用到。

#import <Foundation/Foundation.h>
@interface Person : NSObject
// ARC下block可以用strong修饰,MRC下用copy修饰
@property (nonatomic, strong) void(^myName)(NSString * name);
@end

ViewController中使用即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p = [[Person alloc] init];
    p.myName = ^(NSString *name) {
        NSLog(@"你好,我的名字叫%@", name);
    };
    p.myName(@"Rac");
}
打印结果如下: image.png

2、作为方法的参数

#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat:(void (^)(void))block;
@end

#import "Person.h"
@implementation Person
- (void)eat:(void (^)(void))block {
    //1、保存这个block
    //2、做特定的事情
    //3、得到一个结果,将结果给block
    block();
}
@end

ViewController中调用即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p = [[Person alloc] init];
    [p eat:^{
        NSLog(@"我要吃东西");
    }];
}
打印结果如下: image.png

3、作为方法的返回值
block作为方法的返回值,可以通过点语法进行调用,进而实现链式编程功能,我们常用的自动布局第三方Mansory的实现就是这个原理。

#import <Foundation/Foundation.h>
@interface Person : NSObject
//打点调用,必须是getter方法,满足两点:1、有返回值;2、没有参数
- (void(^)(int))run;
// 持续打点调用(即链式编程),block返回当前类的对象即可
- (Person *(^)(int))runAgain;
@end

#import "Person.h"
@implementation Person
- (void (^)(int))run {
    return ^(int m){
        NSLog(@"我跑了%d米", m);
    };
}
- (Person *(^)(int))runAgain {
    return ^(int m){
        NSLog(@"我这次又跑了%d米", m);
        return self;
    };
}
@end

ViewController中调用即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p = [[Person alloc] init];
    p.run(10);
//    p.run(10)相当于:
//    void (^blocka)(int m) = p.run;
//    blocka(10);
    p.runAgain(10).runAgain(20).runAgain(30);
}
打印结果如下: image.png

六、block 循环引用问题
循环引用,肯定大家都知道的,通俗点说,就是A持有B,B持有A,互相强引用,导致双方(或多方)都无法正常释放,从而引起内存泄漏。
我这里也提供三种解决block循环引用的方法,当然还有其他的解决办法,原则就一个:打破这个循环链!
1、__weak

    __weak typeof (self) weakself = self;
    self.vblock = ^(){
        weakself.view.backgroundColor = [UIColor yellowColor];
        [weakself.navigationController popViewControllerAnimated:YES];
    };
    self.vblock();

2、__block
注意:blockSelf在代码块中用要完置为nil

    __block ViewController * vself = self;
    self.vblock = ^{
        vself.view.backgroundColor = [UIColor yellowColor];
        [vself.navigationController popViewControllerAnimated:YES];
        vself = nil;
    };
    self.vblock();

3、self作为block的参数传递进去

    self.sblock = ^(ViewController * vc) {
        vc.view.backgroundColor = [UIColor yellowColor];
        [vc.navigationController popViewControllerAnimated:YES];
    };
    self.sblock(self);

检测是否解决循环引用问题,只需查看dealloc方法是否被调用即可,因为当前对象被销毁时肯定会走dealloc方法,大家可自行测试。

上一篇下一篇

猜你喜欢

热点阅读