iOS中的block

1,244 阅读9分钟

block是什么,什么结构?

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

那么block到底什么样子的呢? 下面这段含有block的代码,转换成底层时:

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

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m

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


    int age = 10;
    void(*block)(int ,int) = ((void (*)(int, int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, age));
    age = 20;
    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
}

删除其中的类型转换,就能看到

block的类型是:__ViewController__viewDidLoad_block_impl_0;

而block的调用:block->FuncPtr(block , 3, 5)

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int age;
};

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

用图来说明就是:

block的变量捕获

提前补充一点不常为人知的:

通常情况下,局部变量自带auto,但大都隐藏不写; 比如 int age = 20; 等同于 auto int age = 20;

所以block捕获的变量分以下情况:

  • auto 修饰的变量,只是变量的值被block捕获而已,外界再修改变量,block内部无法同步;

  • static修饰的变量,该变量的内存地址被block捕获,外界修改变量时,block内部访问的是同一地址,所以也会被修改

  • 全局变量:写在方法外面的变量;block不用捕获,可直接拿到地址访问

如果是auto修饰的局部变量:

如果是static修饰的局部变量:

 auto int a = 10;
 static int b = 11;
 void(^block)(void) = ^{
     NSLog(@"hello, a = %d, b = %d", a,b);
 };

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int a;
  int *b;
};

b就是被static修饰后的,被block捕获后,在block里面是int *b;,说明捕获的是指针地址

如果是全局变量:

static int c = 10;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    void(^block)(void) = ^{
        NSLog(@"hello, c = %d", c);
    };

    block();
}

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
};

结构体里根本没有变量c或它的指针,说明全局变量不会被捕获。

多说一点,我们平时使用的self是如何捕获的呢?

对于self来说,block会将self捕获到block内部

以Person为例:

@implementation Person
- (void)personTest
{
    void (^block)(void) = ^{
        NSLog(@"%s %p",__func__,self);
    };
    block();
}
@end

struct __Person__personTest_block_impl_0 {
  struct __block_impl impl;
  struct __Person__personTest_block_desc_0* Desc;
  Person *self;
};

可以看出,block会捕获self

原因:self是局部变量; 每个方法都有默认的两个参数,

- (void)test{
 	NSLog(@"test");
}

等同于
- (void)test(Person *self ,SEL _cmd) {
 	NSLog(@"test");
}

转成C++时:
  void test(Person *self , SEL _cmd){
  NSLog(@"test");
}

block的类型

上面说到,block本质上就是OC对象,是因为有isa指针,那么他继承于谁呢?

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

NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
    
打印:
__NSMallocBlock__
__NSMallocBlock
NSBlock
NSObject
 
说明: 
__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject
block是对象,isa是继承与NSObject

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

__NSGlobalBlock___NSConcreteGlobalBlock )
__NSStackBlock__  ( _NSConcreteStackBlock )
__NSMallocBlock___NSConcreteMallocBlock )

内存分布如下:

程序区域:主要存放函数的二进制代码等(代码段)

数据区域:全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后有系统释放

堆区域: 由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在iOS 中 alloc 都是存放在堆中

栈区域:由编译器自动分配并释放,存放函数的参数值,局部变量等。
栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成

那如何区分block的类型呢?

在MRC环境下:(ARC环境编译器会做一些其他操作)

第一种:
void(^block1)(void) = ^{
    NSLog(@"没有访问变量");
};
block1();
NSLog(@"%@",[block1 class]);

打印:
 没有访问变量
 __NSGlobalBlock__
 
 第二种:
 auto int a = 10;
 void(^block2)(void) = ^{
     NSLog(@"访问auto变量, a = %d,", a);
 };
 block2();
 NSLog(@"%@",[block2 class]);
 
打印:
访问auto变量, a = 10,
__NSStackBlock__

第三种:
NSLog(@"%@",[[block2 copy] class]);
打印:__NSMallocBlock__

GlobalBlockStatckBlock 的销毁管理由系统处理;

MallocBlock的销毁由开发人员处理;

每一种类型的block调用copy后的结果如下所示:

因为在堆上的block才是开发人员管理的,而且现在基本都是在ARC环境中开发,编译器会自动做了一部分工作(主要是将栈上的block拷贝到堆上),那就看看block中的copy.

block中的copy

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

1、block作为函数返回值时

2、将block赋值给__strong指针时

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

4、block作为GCD API的方法参数时

MRC下block属性的建议写法:

@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法:

@property (strong, nonatomic) void (^block)(void);

@property (copy, nonatomic) void (^block)(void);

对象类型的auto变量

1、当block内部访问了对象类型的auto变量时,如果block是在栈上,将不会对auto变量产生强引用 (因为当block在栈上的时候,他自己都不能保证自己什么时候被释放,所以block也就不会对自动变量进行强引用了)

2、如果block被拷贝到堆上,会调用block内部的copy函数,

copy函数内部会调用_Block_object_assign函数,

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

3、如果block从堆上移除,会调用block内部的dispose函数,

dispose函数内部会调用_Block_object_dispose函数,

_Block_object_dispose函数会自动释放引用的auto变量(release

4、在多个block相互嵌套的时候,auto属性的释放取决于最后的那个强引用什么时候释放

typedef void (^Block)(void);

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    Block block;
    {
        Person *p = [[Person alloc]init];
        block = ^{
            NSLog(@"%s p= %@",__func__,p);
        };
        [p release];
    }
    block();
    NSLog(@"%s ",__func__);
}

在MRC下,p释放后,才会去执行block中的打印,因为p被释放了,所以会出现崩溃。

在ARC下,打印如下:

-[ViewController viewDidLoad]_block_invoke p= <Person: 0x600002cdce
-[ViewController viewDidLoad]
-[Person dealloc]

转成C++后,如下:

//block的底层结构
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  Person *p;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, Person *_p, int flags=0) : p(_p) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//block中的 block_desc_0的结构
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*);
}

//copy函数
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->p, 
    (void*)src->p, 
    3/*BLOCK_FIELD_IS_OBJECT*/);
    }

//dispose函数
static void __ViewController__viewDidLoad_block_dispose_0(
struct __ViewController__viewDidLoad_block_impl_0*src) {
_Block_object_dispose((void*)src->p, 
3/*BLOCK_FIELD_IS_OBJECT*/);
}

补充: 我们运行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m出错了,

我们需要支持ARC,指定运行时系统版本,xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

block的修饰符

__block

  • __block可以用于解决block内部无法修改auto变量值的问题
  • __block不能修饰全局变量、静态变量(static)
  • 编译器会将__block变量包装成一个对象

举例解释:


typedef void (^Block)(void);

__block int age = 10;
Block block = ^{
    NSLog(@"%s age = %d",__func__,age);
};
block();

如果删除age的修饰符__block,直接在block中修改age,Xcode会直接报错,Variable is not assignable (missing __block type specifier); block内部不能直接修改age的值

我们把代码转化为C++代码,然后在age使用__Block前后,对Block结构体进行分析:

__block所起到的作用就是只要观察到该变量被 block 所持有之后,age其实变成了OC对象,里面含有isa指针 如果在block中修改age,就会通过__main_block_impl_0找到其内部的__Block_byref_age_0 *age,然后找到结构体__Block_byref_age_0,然后对结构体中的age做出修改

通过NSLog就可以看出:


NSLog(@"%s age = %d",__func__,age);

 NSLog(
(NSString*)&__NSConstantStringImpl__var_folders____qf7vv781595b7dhbc7f1cj4c0000gn_T_ViewController_5492a8_mi_2,
 __func__,
 (age->__forwarding->age)
 );

最后是age->__forwarding->age

block由栈上拷贝到堆上,结构体__Block_byref_age_0中的forwarding指针指向大致如图:

__block的内存管理

1、当block在栈上时,并不会对__block变量产生强引用

2、当blockcopy到堆时, 会调用block内部的copy函数

copy函数内部会调用_Block_object_assign函数

_Block_object_assign函数会对__block变量形成强引用(retain

3、当block从堆中移除时,会调用block内部的dispose函数,

dispose函数内部会调用_Block_object_dispose函数,

_Block_object_dispose函数会自动释放引用的__block变量(release)

总结

1、在ARC环境下,Block被引用的时候,会被Copy一次,由栈区copy到了堆

2、在Block被copy的时候,Block内部被引用的变量也同样被copy一份到了堆上面

3、被__Block修饰的变量,在被Block引用的时候,会变成结构体也就是OC对象,里面的__forwarding也会由栈copy到堆上面

4、栈上__block变量结构体中__forwarding的指针指向堆上面__block变量结构体,堆上__block变量结构体中__forwarding指针指向自己

5、当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)

block的循环引用

在auto变量为OC对象的时候,在没有修饰符修饰的时候Block内部会强引用OC对象,而对象如果也持有Block的时候就会造成相互引用,也就是循环引用的问题。

@interface Person : NSObject

@property(nonatomic ,copy)void(^block)(void);

- (void)personTest;
@end

@implementation Person

- (void)personTest
{
    self.block = ^{
        NSLog(@"%s %@",__func__,self);
    };
}
@end

Person *person = [[Person alloc]init];
[person personTest];//产生循环引用

当在别处调用Person的personTest时,就会产生循环引用:person拥有属性block,而在block内部也引用着self,所以产生了循环引用。

大致如图:

解决这种问题:

第一种方式:(ARC下)
__weak typeof(self) weakSelf = self;
self.block = ^{
    printf("%p",weakSelf);
};

第二种方式:(MRC)
__unsafe __unretained id weakSelf = self;
self.block = ^{
    NSLog(@"%p",weakSelf);
};

转成底层C++就是弱引用了

struct __Person__personTest_block_impl_0 {
  struct __block_impl impl;
  struct __Person__personTest_block_desc_0* Desc;
  id weakSelf; 
};

补充一下:__unsafe __unretained用在MRC下,MRC下没有__weak

还有一种解决方式:用__block解决(必须要调用block)

__block id weakSelf = self;
self.block = ^{
    printf("%p",weakSelf);
    weakSelf = nil;
};

self.block();
struct __Person__personTest_block_impl_0 {
  struct __block_impl impl;
  struct __Person__personTest_block_desc_0* Desc;
  __Block_byref_weakSelf_0 *weakSelf; // by ref 
};

struct __Block_byref_weakSelf_0 {
  void *__isa;
__Block_byref_weakSelf_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id weakSelf; //指向外面的person对象
};

这种方式解决是因为有中间的__Block_byref_weakSelf_0 person对象拥有block,block内部拥有__Block_byref_weakSelf_0这个结构体,__Block_byref_weakSelf_0内部又有一个指针id weakSelf来指向person对象,因此就形成了一个循环引用的环。

大致如下图:

解决循环引用就是打破这个环,调用block之后,将weakSelf = nil;就是将__Block_byref_weakSelf_0内部的指 针id weakSelf置为nil,就能打破这个环了,就能解除循环引用问题。

大致如下图:

以上若有错误,欢迎指正。转载请注明出处。