iOS block相关知识点

1.block

首先,我们了解下闭包的概念,闭包就是能够读取其他函数内部变量的函数,在很多语言上都有类似的实现,而block可以说就是OC上对闭包的实现,block实际上就是把代码块指向函数指针,并且能截获变量。

block的使用代码如下:

^{
    NSLog(@"Hello");
};

可以看到代码被^{}包起来了,其实^{}只是个语言标记,clang编译器会将其转换为block结构体。

2.内存结构

block目前最新的结构体定义如下:

struct Block_layout {
    void *isa;  // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__ 
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

可以看到block也具有isa指针,指向__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__ 所对应的地址,flags为对应的flag数值,有很多作用,invoke函数指针指向相应的代码块,第一个参数为block本身的指针。这里主要就是isa,他代表该block所对应的类型。

__NSGlobalBlock__:当Block中没有引用外部变量,或引用了全局变量,const 变量或static变量时为该类型,存放在data段里。

__NSMallocBlock__:当Block引用了外部的OC对象,Block对象或用__block修饰的变量时则为该类型,存放在堆里,进行copy操作的话,会增加引用计数。

__NSStackBlock__:当Block中使用了外部栈变量,则为该类型,存放在栈里,在ARC下,block赋值后会被__strong修饰,然后走objc_retainBlock->_Block_copy->_Block_copy_interna流程,被移到__NSMallocBlock__里,只有__weak声明的block才在栈里。

可以通过[block class]来获取到该block的类型。

3.截获变量

理解Block的关键,在于理解Block是如何处理外部变量的,Block中会截取这些类型的外部变量:

1.全局/静态变量

对于全局/静态变量,Block会直接引用这类变量,不会copy。

static int a = 13;
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"Outside Block, static int a address is %p", &a);
   ^{
        NSLog(@"Inside Block, static int a address is %p", &a);
    }();
}

这里打印出来的地址都是同一个,全局变量因为作用域的问题,在block内直接访问修改是没问题的,且不会保存到block的结构体内,而静态变量因为内存地址不变,所以传递到block内用的是指针地址,所以也可以直接访问和修改。

2.自动(auto)存储类型/局部变量

对于auto类型的变量,(Block类型、__block、NSObject类型除外),block内部是使用const copy了一份,并存放到block结构体内比如下面:

int b = 12;
    NSLog(@"Outside Block, address of int b is %p", &b);
   ^{
        NSLog(@"Inside Block, address of int b is %p", &b);
    }();

这里打印的是不同地址,block内部结构是:

struct __block_literal_2 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(struct __block_literal_2 *);
    struct __block_descriptor_2 *descriptor;
    const int ;   // 这里会有一份const copy
};

一般的,对于标量类型(int, float, bool等基本类型),struct,unions和函数指针类型,都会采用const copy的方式,将Block外部的变量拷贝到Block内部。

3.对象类型(Block/NSObject)

截取NSObject对象时,同样会做一份const copy,下面代码:

NSObject *obj = [NSObject new];
NSLog(@"Outside Block, address:%p-----instance:%@", &obj,obj);
^{
    NSLog(@"Inside Block, address:%p-----instance:%@", &obj,obj);
}();

打印了:
Outside Block, address:0x16f445368-----instance:<NSObject: 0x283ec45a0>
Inside Block, address:0x16f445360-----instance:<NSObject: 0x283ec45a0>

说明block只copy了指针,并没有copy对象,只是浅拷贝。这里有个细节,如何是修改对象的话,会直接报错,当是修改对象内部的成员变量是没问题的,因为修改成员变量并不会影响对象本身的内存地址,而且因为是浅拷贝,所以修改的值出了block也是生效的。比如下面这代码:

NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
blockType blk = ^{
    [str appendString:@" World"];
};
blk();
NSLog(@"Now str is %@", str);

是可以正常打印Hello World的,因为appendString并不会改变str指针本身,只是修改了指针指向的内容。

4.__block修饰的变量

这里最复杂的就是__block修饰的变量,除了全局变量和静态变量外,当我们想在block内修改变量的值时,需要使用__block修饰。看下如下代码:

    __block int b = 13;
    NSLog(@"Outside Block, address of __block int b is %p, b = %d", &b, b);
    blockType1 blk = ^{
        b++;
        NSLog(@"Inside Block, address of __block int b is %p, b = %d", &b, b);
    };
    blk();
    NSLog(@"After Block, address of __block int b is %p, b = %d", &b, b);

这里变量b在进入block后,指针变了,这里实际上被转换成_block_byref_i结构了,比如:

__block int  i = 10;
i = 11;

使用clang改写变成了:

struct _block_byref_i {
  void *isa;
  struct _block_byref_i *forwarding;
  int flags;   //refcount;
  int size;
  int captured_i;
} i = { NULL, &i, 0, sizeof(struct _block_byref_i), 10 };

i.forwarding->captured_i = 11;

可以看到,__block int i被改写为了struct _block_byref_i 结构体,这里关键属性就是forwardingforwarding指向一个block结构体,当block在栈上时,forwarding指向的是自身block,当blockcopy到堆上时,forwarding指向的是堆上的block地址,而堆上的block的forwarding指向自身。如下图所示:

所以前面的变量b才会出现指针地址被改变的问题。

4.用途

block有以下几种用法:

1.作为属性

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

这里block使用了copy来修饰,其实在ARC下,使用strongcopy效果是一样的,底层实现都是走_Block_copy方法,创建后的block__NSMallocBlock__类型的。

如果使用weak修饰的话,创建后的block__NSStackBlock__类型的,但是就有可能被栈释放,所以我们一般用strong或者copy

另外如果该block的实现里没有捕获外部变量或者捕获了全局变量,静态变量,那么类型会优先变成__NSGlobalBlock__,不受修饰影响。

2.作为参数

- (void)testBlock:(void(^)(void))block {

}

//1.传入成员属性
[self testBlock:self.block];
//2.传入临时创建的block,并捕获外部变量
[self testBlock:^{
    NSLog(@"%@",num);
}];
//3.传入临时创建的block,不捕获外部变量
[self testBlock:^{

}];

在上面的3中调用方式中,第一种传入成员属性,因为传的是指针地址,所以该block等同于self.block的内存分布。

第二种传入临时创建的block,该block因为捕获了外部变量,如果该变量非全局或静态变量,则该block__NSStackBlock__类型。

第三种因为没有捕获任何外部变量,所以该block__NSGlobalBlock__类型,生命周期等同于app,也就是会一直常驻内存。

3.作为返回值

//1.返回成员属性
- (void(^)(void))returnBlock {
    return self.block;
}
//2.返回临时创建的block,并捕获外部变量
- (void(^)(void))returnBlock {
    NSNumber *num = @0;
    return ^{
        NSLog(@"%@",num);
    };
}
//3.返回传入临时创建的block,不捕获外部变量
- (void(^)(void))returnBlock {
    NSNumber *num = @0;
    return ^{

    };
}

以上3种返回值,其block的内存分布基本与作为参数时相关,唯一不同的是,第二种block引用了外部变量,本来是__NSStackBlock__类型的,但是因为需要return,会被__strong修饰,所以变成了__NSMallocBlock__

5.循环引用

当对象内部的成员变量引用了block,而block又引用了对象时,会导致循环引用。
下面几种情况都会造成循环引用:

这里self->self.strBlk->self.array->self,是最常见的循环引用,能被leak和xcode检测出来,当是如果这里多嵌套几层self->self.ival->self.strBlk->self.array->self,也会导致循环引用,但是leak会检测不出来,xcode也不会报警。

上面这种循环引用就比较难发现了,这里在MRC下是不会循环引用了,当是在ARC下会出现,因为self->self.blk->blockSelf->self,主要是__block修饰的self,实际是个block结构体,但是内部会持有self。

参考资料:
https://blog.csdn.net/u013378438/article/details/87167133
https://www.jianshu.com/p/ee9756f3d5f6
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/
https://www.jianshu.com/p/fa76921131f9

发表评论

电子邮件地址不会被公开。 必填项已用*标注