Block总结

基本语法

block在iOS开发中被视作对象,因此其生命周期会一直等到持有者的生命周期结束了才会结束。
block截获自动变量并没有实现对C语言数组的捕获。

实现

void (^blk)(void) = ^{
        printf("Block\n");
    };
    blk();

我们知道Block底层也是用C语言代码实现的,现在我们写以上Block语句,通过终端转换为源代码C++

clang -rewrite-objc main.m

查看源代码文件,然后

int main(int argc, const char * argv[]) {

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

blk变量的赋值实现在于这个函数__main_block_impl_0,它是一个构造函数,赋值给blk变量,等同于将__main_block_impl_0结构体实例指针赋值给blk变量,如上面提到过的 (__main_block_impl_0结构体,也就是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;
  }
};

C++语法结构体也可以像类一样有方法和变量
这个结构体__main_block_impl_0有两个变量,一个构造函数
第一个成员变量impl,看他的结构

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;//今后版本升级所需的区域
  void *FuncPtr;//函数指针
};

可理解为Block的类结构

关于isa指针,OC类和对象都有其isa指针,__main_block_impl_0结构体相当于基于objc_object结构体的OC类对象的结构体,它的信息存储于Block类中,关于该类的信息放置于NSConcreteStackBlock(相当于元类)中
第二个成员变量Desc为block的描述信息

static struct __main_block_desc_0 {
  size_t reserved;////今后版本升级所需的区域
  size_t Block_size;//Block的大小 //size_t = long unsigned int
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

下面来看看构造函数赋值

__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Block\n");
}

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

第一个参数fp为__main_block_func_0,这个函数参数cself 相当于OC实例方法中指向对象自身的变量self,即__cself为指向Block值的变量 ,他就是由BLOck语法转换的C语言函数指针
第二个参数DATA是作为静态全局变量初始化的__main_block_desc_0结构体实例指针

static struct __main_block_desc_0 __main_block_desc_0_DATA = 
{ 0, sizeof(struct __main_block_impl_0)};

这个DATA,就是__main_block_impl_0结构体实例的大小进行初始化。

  1. impl.isa的赋值&_NSConcreteStackBlock(我们猜测到它为设置在栈上的block),与之相似的几个类:
_NSConcreteStackBlock存储在栈中的 block,当函数返回时会被销毁
_NSConcreteGlobalBlock存储在全局/静态的 block,不会捕获任何外部变量
_NSConcreteMallocBlock存储在堆中的 block,当引用计数为0时会被销毁

详解可看Block之存储域 NSConcreteStackBlock,NSConcreteGlobalBlock,NSConcreteMallocBlock
实际上_NSConcreteStackBlock也就相当于objc_class结构体实例。也就是说Block实质就是OC对象。所以通过成员变量isa保持该类的结构体实例指针.在将Block作为OC对象处理时,关于该类的信息放置于NSConcreteStackBlock中 ,即NSConcreteStackBlock就相当于block的类

我们看到通过clang编译的代码block是在栈上,但其实只有打印才能真的得出block类型,其为NSConcreteGlobalBlock,没有捕获任何外部变量,该类型的block和函数一样 存放在 代码段 内存段。内存管理简单

 Block[866:46995] <__NSGlobalBlock__: 0x1094400a0>

  1. impl.Flags,附加标识位, 在copy和dispose等情况下可以用到
  2. impl.FuncPtr block的函数指针
  3. Desc block 的存储大小,一些初始化信息
  • 调用
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉类型转换,就剩

(*blk->impl.FuncPtr)(blk);

这句话也就是函数指针funcPtr参数为block自身

截取自动变量值

截取变量值如下

int main(int argc, const char * argv[]) {
    
    int val = 10;
    int dmy = 256;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
    };
    val = 2;
    fmt = "These values were changed. val = %d\n";
    blk();
    return 0;
}

通过clang为源代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

        printf(fmt, val);
    }

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)};
int main(int argc, const char * argv[]) {

    int val = 10;
    int dmy = 256;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
    val = 2;
    fmt = "These values were changed. val = %d\n";
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

首先看到block中的两个自动变量被加到__main_block_impl_0 结构体,成为成员变量
然后在初始化结构体实例的构造函数中,也多加了自动变量参数

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) 

然后blk赋值的初始化函数中,传参

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val))

就相当于

impl.isa = &_ NSConcreteStackBlock.
impl.Flags = 0;
impl.FuncPtr = _main_ block_func_ 0;
Desc = &_ _main_ block_ desc_0_DATA;
fmt = "val = %d\n";
val = 10;

所以在__main_block_impl_0结构体实例中,自动变量值被截获
总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量被保存到Block结构体实例中

  • 那截取自动变量为什么并没有实现C语言数组类型的截获

假如有个数组为char a[10] = {2};
要把它赋给Block结构体实例的另一个C语言数组char b[10] = a;
显然,C语言是不允许这样赋值的,所以用指针就可以

__block存储域类说明符

__block,我们用它来指定Block中想变更值的自动变量,也就是用它来修饰的自动变量,在block中值可以修改
好了,原理时刻

 __block int val = 10;
    void (^blk)(void) = ^{
        val = 1;
    };
    blk();
    NSLog(@"%@", blk);

源代码如下

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_5s_8gd2ptmx7b93yclhdly3896c0000gn_T_main_c59089_mi_0, blk);
    return 0;
}

我们看到主函数中,__block类型变量变为结构体实例变量,我们看改结构体声明如下

struct __Block_byref_val_0 {
  void *__isa; //对象指针
__Block_byref_val_0 *__forwarding;  //指向拷贝到堆上的指针
 int __flags;//标志位变量
 int __size;//结构体大小
 int val;//相当于原自动变量
};

为了对__Block_byref_val_0结构体进行内存管理,新加了copy和dispose函数

//当block从栈拷贝到堆时,调用
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
//当block从堆中释放时
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

copy操作到底会具体做什么呢?
会把 val 转成的__Block_byref_val_0 结构体 赋值给block的变量。同时会把 __Block_byref_val_0 的结构体中的 __forwarding指针指向拷贝到堆上的block结构体实例,就是栈上和拷贝到堆上的 的__Block_byref_val_0都用__forwarding指向堆上的自己。
这样在栈上修改 val变量的时候,实际修改的是堆上值,所以block内外的值是相互影响。

那么给block赋值的代码转换呢

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
  (val->__forwarding->val) = 1;
}

现在就相当于给堆上的val赋值为1

上面提到的为什么源代码和打印的Block类不一样呢?

Block和__block变量都转换为对应的结构体类型自动变量,即就是栈上生成的该结构体的实例,源代码中block设置在栈上,如果Block所属的作用域结束,变量就会被废弃,但为什么Block中变量超出变量作用域可存在,就是因为Blocks提供了将配置在栈上的Block复制到堆上,这样变量作用域结束时不受影响,所以打印的Block所属类为在堆上

自动拷贝

那么在ARC下到底是如何自动拷贝的呢?

typedef int (^blk_t)(int);
blk_t func(int rate) {
    return ^(int count){
        return rate * count;
    };
}

通过对应ARC的编译器可转换如下:

blk_t func(int rate)
{
    blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
    tmp = objc_retainBlock(tmp);
    return objc_autoreleaseReturnValue(tmp); 
}

通过上面两句话,在Runtime源码中可查看到其实现

id objc_retainBlock(id x) 
{
#if ARR_LOGGING
    objc_arr_log("objc_retain_block", x);
    ++CompilerGenerated.blockCopies;
#endif
    return (id)_Block_copy(x);
}

也就是说_Block_copy函数将栈上的Block复制到堆上,然后将堆上的地址作为指针赋值给变量tmp,然后将堆上的Block作为OC对象注册到AutoreleasingPool中,由自动释放池来管理

手动拷贝

  • 向方法或函数的参数中传递Block时
    有两种方法或函数不用手动复制
    • CoCoa框架的方法且方法名中有usingBlock等时
    • GCD的API
typedef void (^blk_t)(void);
- (id)getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d", val);},
            [^{NSLog(@"blk1:%d", val);} copy],^{NSLog(@"blk1:%d", val);}, nil];
}
Block *blk0 = [[Block alloc] init];
    id obj = [blk0 getBlockArray];
    NSLog(@"%@", obj);
    blk_t abc = (blk_t)[obj objectAtIndex:1];
    abc();

在打印的数组结果为

019-08-13 17:16:50.189381+0800 blockClang[989:166307] (
“<NSMallocBlock: 0x101805930>”,
“<NSMallocBlock: 0x1018059b0>”,
“<NSStackBlock: 0x7ffeefbff3f8>”
)

__block变量存储域

Block从栈复制到堆上,那么使用 的__block变量呢?

__block变量也会被从栈复制到堆,并且被Block所持有,如果已经有其他Block持有这个__block变量,那么会增加其引用计数
如果Block被废弃,那么它所持有的__block变量也就被释放

截获对象

 blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = [^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        } copy];
    }
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);

上面这段代码blk有copy操作,被复制到堆上,block持有array致使array不会被释放.
但是其实没有copy操作也不会崩溃, 看下面这段源代码它自动调用了copy,dispose函数

typedef void (*blk_t)(id);


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
  id array = __cself->array; // bound by copy

            ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_5s_8gd2ptmx7b93yclhdly3896c0000gn_T_main_c28974_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 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};
int main(int argc, const char * argv[]) {

    blk_t blk;
    {
        id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));

        blk = ((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)) ;
    }
    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));

    return 0;
}

纵身一览,几乎都见过了
array变成了Block结构体的成员变量,当block 调用了 copy 方法,在 _Block_object_assign 中,将 array 赋值给 block 成员变量并持有

他跟__block变量都调用了copy,release函数,那么两人有什么不同呢
通过BLOCK. FIELD. IS OBJECT
BLOCK_ FIELD IS_ BYREF参数来区分到底是对象类型还是__block变量

栈上的Block复制到堆的情况

  • 当 block 调用 copy 方法时,如果 block 在栈上,会被拷贝到堆上;
  • 当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;

循环引用

在MRC下呢,__block说明符被用来避免Block中的循环引用。这是因为当Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有__block说明符的id类型或对象类型,则会被retain。
而在ARC下则用weak来避免循环引用

循环引用的问题,无非就是self持有block,block又持有了self,用weak修饰self即可解决问题
接下来,我们还是得讨论一下Block的内存管理问题。

id  obj = [[NSObject alloc] init];
    id  __weak  obj2 = obj;
    NSLog(@"%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(obj2)));
    blk_t blk = ^ {
        NSLog(@"%@", obj2);
    };
    NSLog(@"%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(obj2)));
    blk();
    NSLog(@"%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(obj2)));

我们实验,__weak,__block修饰obj2,打印三个引用计数都一样为2
如果是__strong,那么打印依次为2,4,4
为什么经过一趟block引用计数加2了呢,难道不是因为Block持有它引用计数加1吗

验证一波呢
有一段这样的代码
声明一个属性Block,

typedef void (^blk_t)(void);
@interface ViewController : UIViewController

@property (nonatomic, copy) blk_t blkRetainCount;

@end
@implementation ViewController
- (void)start {
    ViewController *table = [[ViewController alloc] init];
    id a = [[NSObject alloc] init];
    table.blkRetainCount = ^{
        NSLog(@"%@", a);
    };
    NSLog(@"%ld", (long)CFGetRetainCount((__bridge CFTypeRef)(a)));
}
@end

2019-08-14 16:25:04.676656+0800 Block[1862:133380] 3

目前打印结果为3,我们去重写一下属性blkRetainCount的set方法 只要block里使用了这个对象,引用计数就会加2,否则引用计数不变。

- (void)setBlkRetainCount:(blk_t)blkRetainCount {
    
}

这样呢,再打印一下计数就为2了

你可能感兴趣的:(Block总结)