OC原理-block

407 阅读12分钟

一、基本用法

1.作为变量

//有参数有返回值的block
int(^block)(NSString *name)=^(NSString *name){
	NSLog(@"%@",name);
	return 100;
}
block(@"zht");
//无参数和返回值的block
void(^block1)(void)=^{
	NSLog(@"this is a block");
}
block1();

2.作为属性

//有参数有返回值的block
@property (nonatomic, copy) int (^blockName)(NSString *name);
//无参数和返回值的block
@property (nonatomic, copy) void (^blockName)(void);

3.作为参数

//有参数有返回值的block
-(void)testBlock:(int(^)(NSString *name))complateBlock;
//无参数和返回值的block
-(void)testBlock:(void(^)(void))complateBlock;

4.声明替换 -- typeDef

typedef int(^TestBlock1)(NSString *name);
typedef void(^TestBlock2)(void);

//那上面的代码分别可以这么写
TestBlock1 block1=^(NSString *name){
	NSLog(@"%@",name);
	return 100;
}
TestBlock2 block2=^{
	NSLog(@"this is a block");
}

//有参数有返回值的block
@property (nonatomic, copy) TestBlock1 block1;
//无参数和返回值的block
@property (nonatomic, copy) TestBlock2 block2;

//有参数有返回值的block
-(void)testBlock:(TestBlock1)complateBlock;
//无参数和返回值的block
-(void)testBlock:(TestBlock2)complateBlock;

二、本质

有下面的block代码

void(^block)(int,int) = ^(int a,int b){
     NSLog(@"this is a block");
};
block(10,20);

执行命令转化成c++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

转化后上面的两句代码变成

void(*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);

精简下

void(*block)(int,int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
block->FuncPtr(block, 10, 20);

下面我们看下__main_block_impl_0、__main_block_func_0和__main_block_desc_0_DATA

//block的本质结构
struct __main_block_impl_0 {
  struct __block_impl impl;//存储了block的信息
  struct __main_block_desc_0* Desc;//里面存储了block占用内存的大小
  //相当于block的构造函数  返回一个结构体对象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//这里便是了block的类型,我们稍后再说
    impl.Flags = flags;
    impl.FuncPtr = fp; //存储block代码块生成的函数的地址
    Desc = desc; //存储block的所需的内存大小
  }
};
//block信息结构体
struct __block_impl {
  void *isa; //看到了对象才有的isa指针。
  int Flags;
  int Reserved;
  void *FuncPtr;//代码块生成的函数
};
//定义了__main_block_desc_0结构体,并声明了__main_block_desc_0_DATA结构体变量,变量中存储了block需要多大内存 
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};


//这是是由block的代码块生成的函数。  block的参数也在这里体现出来了
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_fq_cwz6482s7clc_d15gl6nbm340000gn_T_main_d39147_mi_0);
}


定义一个block后,底层结构如下图 所以block声明和调用理解如下。

//定义block变量,这里调用__main_block_impl_0的构造函数,拿到返回值(其实就是一个__main_block_impl_0结构体对象),然后取地址复制给block变量
//1.把block中的代码块封装成的__main_block_func_0函数传入__main_block_impl_0的构造函数,最后由__block_impl->FuncPtr存储起来
//2.把{ 0, sizeof(struct __main_block_impl_0)存储到__main_block_impl_0->Desc
void(*block)(int,int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

//调用block,通过__block_impl->FuncPtr取到__main_block_func_0函数,再去执行。
block->FuncPtr(block, 10, 20);

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

三、变量捕获

1.局部auto变量捕获

(1)基本类型

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

运行结果 转化成C++后

int age = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));//这里把age的值传进了__main_block_impl_0结构体中
age = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

//block的本质结构 这里多了一个age的成员变量
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//这是是由block的代码块生成的函数。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; //取出__main_block_impl_0结构体中的age变量
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_fq_cwz6482s7clc_d15gl6nbm340000gn_T_main_5becdd_mi_0,age);
}

我们发现block的底层结构体里面多了一个age成员变量,执行代码void(*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));,利用__main_block_impl_0的构造函数把age的值传递给了__main_block_impl_0结构体中的age成员变量,注意这时候值是10,执行block代码块是再取出来__main_block_impl_0结构体中的age成员变量的值,所以打印结果是10,不是20。

(2)对象类型

//代码一
__strong Person * p = [[Person alloc] init];
p.name = @"zht";
void(^block)(void) = ^{
  NSLog(@"this is a block--%@",p.name);
};
block();
//代码二
__strong Person * p = [[Person alloc] init];
p.name = @"zht";
__weak Person *weakPerson = p;
void(^block)(void) = ^{
  NSLog(@"this is a block--%@",weakPerson.name);
};
block();

转化成C++

//这里需要制定arc环境 用到的runtime版本
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m -o main-arm64.cpp

转化后

//代码一
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong p;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _p, int flags=0) : p(_p) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//代码二
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们看到__main_block_impl_0中的person的指针类型不一样,一个是__strong 一个是__weak ,这也解释了用__weak解决block循环引用的关键。除此之外__main_block_desc_0结构体也多了两个函数指针,我们稍后介绍这两个函数的作用。

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}

2.局部static变量捕获

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

运行结果 转化成C++后

static int age = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));//这里把age的地址值传进了__main_block_impl_0结构体中
age = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

//block的本质结构 这里多了一个指向int类型的指针用来存储age的内存地址
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *age = __cself->age; //取出__main_block_impl_0结构体中的age内存地址
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_fq_cwz6482s7clc_d15gl6nbm340000gn_T_main_849526_mi_0,(*age));
}

这里跟局部auto变量的区别是把age的内存地址传入了block,后面再修改age的值,执行block代码块时根据block内部的age的内存地址取到age最新的值,因此结果是20。

3.全局auto变量和static变量捕获

int age_ = 10;
static int height_ = 10;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"this is a block--%d---%d",age_,height_);
        };
        age_ = 20;
        height_ = 20;
        block();
    }
    return 0;
}

运行结果 转化成C++后

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); //这里根本没有对全局变量进行捕获
age_ = 20;
height_ = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);




int age_ = 10;
static int height_ = 10;
//block的本质结构  这里也没有添加成员变量
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;
  }
};

//执行block代码块也是直接访问的全部变量
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_fq_cwz6482s7clc_d15gl6nbm340000gn_T_main_203762_mi_0,age_,height_);
}

这里我们发现block根本不会去捕获全局变量,也根本没有必要去捕获。

4.总结

  • 对于基本数据类型的局部变量截获其值
  • 对于对象类型的局部变量 连同所有权修辞符一起截获
  • 对于局部静态变量指针形式截获
  • 不截获全局变量、静态全局变量

搞个面试题

@implementation Person
-(void)test{
    void(^block)(void)=^{
        NSLog(@"----%p---",self); //这里的self会不会被block捕获呢?
    };
    block();
}
@end

转化成C++后

//每个函数都会自动添加两个参数self和_cmd
static void _I_Person_test(Person * self, SEL _cmd) {
    void(*block)(void)=((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));//这里我们发现把self捕获了,而且是值传递。说明函数里面的self是局部变量。
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

再来个面试题

@interface Person()
@property(nonatomic,copy)NSString *name;
@end
@implementation Person
-(void)test{
    void(^block)(void)=^{
        NSLog(@"----%p---",_name);//这里会不会对_name进行捕获
    };
    block();
}
@end

转化成C++后

static void _I_Person_test(Person * self, SEL _cmd) {
    void(*block)(void)=((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));//这里我们发现把self捕获了,而且是值传递。说明函数里面的self是局部变量。
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {
  Person *self = __cself->self; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_fq_cwz6482s7clc_d15gl6nbm340000gn_T_Person_4ad387_mi_0,(*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)));//通过self再获取_name
}

这里不会捕获_name,捕获了self局部变量,然后通过self再来获取_name.

四、类型

void(^block)(void) = ^{
  NSLog(@"this is a block");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);

执行结果 上面我们可以再次证明block本质上是对象。

结论:block有三种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。三种类型分别是:NSGlobalBlock(_NSConcreteGlobalBlock)、NSStackBlock(_NSConcreteStackBlock)、NSMallocBlock(_NSConcreteMallocBlock)。 存储区域如下

执行下面代码,打印一下三个block的类型。

void(^block1)(void) = ^{
  NSLog(@"this is a block");
};
int age = 10;
void(^block2)(void) = ^{
  NSLog(@"this is a block--%d",age);
};
NSLog(@"%@  %@  %@",[block1 class],[block2 class],[^{
  NSLog(@"this is a block--%d",age);
} class]);

转化成C++

//代表block1
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;
  }
};
//代表block2
struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  int age;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//代表最后一个block
struct __main_block_impl_2 {
  struct __block_impl impl;
  struct __main_block_desc_2* Desc;
  int age;
  __main_block_impl_2(void *fp, struct __main_block_desc_2 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们奇怪的发现转成c++三个block的类型都是_NSConcreteStackBlock。我们一切要以运行时为准,C++中的block类型有可能在运行时还会被修改。

那么这三种类型的block是如何产生的呢! 但是上面代码中的block2里面也访问了auto变量age,为啥变成了__NSMallocBlock__类型,这是因为arc自动对block进行了copy,把block2变成了__NSMallocBlock__类型。我们把项目调成MRC,再次运行,发现block2就变成了__NSStackBlock__类型。

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,也就是类型由__NSStackBlock__变成__NSMallocBlock__:1.block作为函数返回值时。2.将block赋值给__strong指针时。3.block作为Cocoa API中方法名中含有usingBlock的方法参数时。4.block作为GCD的方法参数时。

第二种情况举例  block2位__NSMallocBlock__类型
int age = 10;
void(^block2)(void) = ^{
  NSLog(@"this is a block--%d",age);
};
NSLog(@"%@  %@",[block2 class],[^{
  NSLog(@"this is a block--%d",age); //该block为__NSStackBlock__类型
} class]);
第三种情况举例 
NSArray *array = @[@"1",@"2"];
[array enumerateObjectsUsingBlock:^(id __Nonnull obj,NSUInteger inx,BOOL * _Nonnull stop){
}];

这样做的目的是为了防止栈上的block销毁后再去访问block报错,所以先将block复制到堆上

对其他类型的block进行操作的结果:

  • 1、当block内部访问了对象类型的auto变量时,如果block在栈上,将不会对auto变量产生强引用。
  • 2、当block中访问了对象类型的auto变量且被拷贝到堆上时,会调用block内部的copy函数,copy函数内部调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修辞符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用或者弱引用
  • 3、当block中访问了对象类型的auto变量且从堆上移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量。

五、__block

1.原理

__block用于解决block内部无法修改局部auto变量值的问题。 __block不能修饰全局变量、静态变量。

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

转成C++后,让我们看下__block做了啥。

//age 转化后的结构体
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding; //这个指针最终指向它自己
 int __flags;
 int __size;
 int age;  //存储值
};
//block内存结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//这里已经有copy和dispose函数指针用来对__Block_byref_age_0 *age进行内存管理,也说了__block已经把变量包装成了一个对象
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

//创建代表age结构体
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
//把age结构体的地址值传入block结构体
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

//block代码块生成的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // 拿到代表age的结构体
  (age->__forwarding->age) = 20;//age->__forwarding拿到还是代表age的结构体,再获取age去修改。这里为啥不是age->age = 20呢?我们稍后再说
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_fq_cwz6482s7clc_d15gl6nbm340000gn_T_main_972bd2_mi_0,(age->__forwarding->age));//访问也是通过
}

总结:编译器会将__block变量包装成一个对象,也可以说是一个结构体,这个结构体里面存储这个变量的值。

2.内存管理

  • 当block在栈上时,不会对__block变量产生强引用。

  • 当block被拷贝到堆时,__block变量也会被复制到堆

    1.会调用block内部的copy函数

    2.copy函数内部会调用_Block_object_assign函数

    3._Block_object_assign函数会对堆上的__block变量形成强引用,这里跟捕获nsobject对象不同

-当block从堆中移除后

1.会调用block内部的dispose函数

2.disponse函数内部会调用_Block_object_dispose函数

3._Block_object_dispose函数会自动引用的__block变量。

现在看一个问题: (age->__forwarding->age) = 20;//这里为啥不是age->age = 20呢??? 这是为了确保不论访问栈上的block还是堆上的block,最后都是访问的堆上的__block中的age。

六、循环引用

   __weak typeof(self) weakSelf = self; // weakSelf(弱引用表) -> self
   self.block = ^{
       // strong-weak-dance
       __strong typeof(weakSelf) strongSelf = weakSelf;
       dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
           NSLog(@"%@",strongSelf.name);
       });
   };
   self.block();
  • 如果在block中使用self,那么block会持有该self,假如同时self也持有block,就造成了循环引用
  • 我们可以在block外面创建一个弱引用weakSelf来替代self,这样block保存的weakSelf不会对self的引用计数造成影响,这样就不会造成循环引用了
  • 但是block内部使用weakself可能会被延迟使用,有可能当bock使用weakself时候self已经被释放了,这个时候weakself也变成nil了
  • 为了保证block内部使用weakself正常,这个时候就需要在block内部创建一个strongself指向weakself,这样在block内部就可以通过strongself拿到self的值,而且strongself是block内部的临时变量,block会释放他,并且strongself和block的生命周期是一样的,所以可以保证block内部使用self正常。