Block底层学习

Block底层本质

  • block就是Objective-C对闭包的实现,闭包就是一个没有名字的函数或者指向函数的指针。block本质上也是一个OC对象,它内部有isa指针;
  • block是封装了函数调用以及函数调用环境(参数)的OC对象;

我们来看一段代码

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void(^block)(int, int) = ^(int b,int c){
            NSLog(@"%d",a);
            NSLog(@"Hello World!");
        };       
        block(10,10);
    }
    return 0;
}

把上面这段代码转化为C++底层语言,转化后:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        //定义block变量
        void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        //执行block内部的代码
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
    }
    return 0;
}

在C++代码中,block代码块底层调用__main_block_impl_0。这句代码调用以下代码

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;
  }
};

我们发现,block的底层也是一个结构体。搜索struct __block_impl

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

block的第一个结构体成员是一个isa指针。这说明,block也是一个OC对象。
__main_block_desc_0结构体成员包括两个参数,如下:

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)};

__main_block_func_0函数内部封装了block执行逻辑的函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int b, int c) {
  int a = __cself->a; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_1);
        }

struct __block_impl结构体内,有一个成员void *FuncPtr,

Block变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。
先来看一段代码

#import 

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

执行上面这段代码,打印值是10,而不是20。之所以执行block(),结果是10,而不是20,这个就使用了变量捕获
我们来看下C++底层代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void (*block)(void) = ((void (*)())&__main_block_impl_0(
                                                                (void *)__main_block_func_0,
                                                                &__main_block_desc_0_DATA,
                                                                a));
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

在上面的代码中,编译时,在block内部已经捕获到a值。然后传递到__main_block_impl_0

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;
  }
};

在上面代码中,根据传递过来的值,赋值给NSLog(@"%d",a);。当a = 20;时,仅仅是改变int a = 10;的值。而block内部获取不到a的值。

Block底层学习_第1张图片
变量捕获

  • int a = 10;默认是auto修饰,是值传递;auto,自动变量,auto修饰的变量,内存会自动消失。所以,block内部不会捕获会自动消失的内存。
  • 如果int a = 10;使用static修饰,则传递的是地址,block内部捕获到变量的地址,如果在外部修改变量的值,则根据地址找到变量存储的值。
  • 全局变量并不会被捕获到block内部。在block内部会直接访问全部变量。
  • self是一个局部变量,在block内部,也会捕获self

Block类型

因为block是一个对象,所以block也是有类型的。block有三种类型,可以通过class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。

  • NSGlobalBlock
    存储在数据区域,没有访问auto。
  • NSStackBlock
    存储在栈区,会自动销毁,访问了auto。
  • NSMallocBlock
    存储在堆区,需要程序员销毁,NSStackBlock调用了copy(ARC环境下,block会自动调用copy,从栈上赋值到堆上,所以一般block类型是NSMallocBlock)。

ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:

  • block作为函数返回值时;

  • 将block赋值给__strong指针时;

  • block作为Cocoa API中方法名含有usingBlock的方法参数时;

  • block作为GCD API的方法参数时。

  • MRC下block属性的建议写法
    @property (copy,nonatomic) void (^block)(void);

  • ARC下block属性的建议写法
    @property (copy,nonatomic) void (^block)(void);
    @property (strong,nonatomic) void (^block)(void);

对象类型的auto变量以及Block的内存管理

类似于局部变量,有auto修饰的对象在block内部,也会存在block类型。来看一段代码

#import 

@interface Person : NSObject
@property (nonatomic,  assign) int age;
@end

#import "Person.h"

@implementation Person
- (void)dealloc{
    NSLog(@"delloc--Person");
}
@end

main.m
#import 
#import "Person.h"
typedef void(^HYBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HYBlock myBlock;
        {
            Person *p = [[Person alloc] init];
            p.age = 10;
            myBlock = ^{
                NSLog(@"%d",p.age);
            };
            myBlock();
        }
    }
    return 0;
}
  • 当block内部访问了对象类型的auto变量时
    如果block是在栈上,将不会对auto变量产生强引用。

  • 当block被拷贝到堆上
    ✔️会调用block内部的copy函数;
    ✔️copy函数内部会调用_Block_object_assign

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

✔️_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe unretained)做出相应的操作,形成强引用(retain)或者弱引用。

  • 如果block从堆上移除
    ✔️会调用blokc内部的dispose函数
    ✔️dispose函数内部会调用_Block_object_dispose函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

✔️_Block_object_dispose函数会自动释放引用的auto变量。

Block修饰符

我们知道不能再block内部修改外部变量的值,我们来看下原因:

#import 
typedef void(^HBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        HBlock  block= ^{
            NSLog(@"%d",a);
        };
        block();
    }
    return 0;
}

转化为C++代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        HBlock block= ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

在上面的代码中,定义了int a = 10;。而输出这个变量值是在下面这个函数中

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_5b923f_mi_0,a);
        }

因为变量a不是全局变量,只是局部变量,所以不能在另外一个函数,修改变量值。

  • 如果int a = 10;使用static修饰,可以在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;传递的是变量的地址值,在block内部,先找到变量的地址值,直接修改变量a的值,而不是直接修改变量值。

  • 如果int a = 10;是全局变量,则在当前文件的函数中,都可以修改变量值。
  • 使用static修饰变量,或者使用全局变量,则这个变量一直在内存中。如果使用__block修改,也可以在block内部修改变量值,并且,变量会自动释放,不会一直存在内存中。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
...
};

从上面的代码可以看出,使用__block修饰变量,在__main_block_impl_0内部,变量a为__Block_byref_a_0 *a; // by ref。而__Block_byref_a_0是一个对象(内部有isa指针)。在这个结构体内部,有成员变量,存储变量值。

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

当修改变量值时,利用__Block_byref_a_0指针先找到结构体,通过变量名找到__forwarding,在通过__forwarding找到变量,来修改变量值。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_b8c75c_mi_0,(a->__forwarding->a));
        }
  • 注意
    ✔️使用__block修饰int a,则在block内部a成为对象。
    ✔️创建NSMutableArray *array = [NSMutableArray array];,在block内部使用[array addObject:@123]是使用这个指针,而不是改变array的值。

block循环引用

循环引用是指两个或以上对象互相强引用,导致所有对象无法释放的现象。这是内存泄露的一种情况。

#import 

typedef void(^HYBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) HYBlock block;
@end

#import "Person.h"

@implementation Person
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end


#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 20;
        person.block = ^{
            NSLog(@"%d", person.age);
        };
        person.block();
    }
    return 0;
}

在上面的代码中,当执行person.block();时,Person对象并没有释放,产生循环引用。
我们来看下,产生循环引用的原因,首先转化为C++代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
...
};

在ARC环境下,HYBlock被拷贝到堆上,当内部调用person时,则在函数__main_block_impl_0内部,Person对象生成Person *__strong person;也即是强引用这个对象。Person对象强引用HYBlockHYBlock又强引用Person对象,则HYBlock不释放,Person对象也不会释放。

解决循环引用

  • 使用__weak,__unsafe_unretained解决;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
...
};

使用weak修饰对象,则在函数__main_block_impl_0内部,不在强引用Person对象。__unsafe_unretained同理,也不在强引用Person对象。

  • 使用__block解决(必须调用block);
#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       __block Person *person = [[Person alloc] init];
        person.age = 20;
        person.block = ^{
            NSLog(@"%d", person.age);
            person = nil;
        };
        person.block();
    }
    return 0;
}

你可能感兴趣的:(Block底层学习)