Block的浅入深出

俗话说:动手成功,伸手落空。

话不多说,开干。


1.基本使用

我先直接贴上几种block的类型

// 无参数无返回值
void (^block1)(void) = ^{
};
block1();

// 无参有返回值
int (^block2)(void) = ^{
    return 0;
};
int res2 = block2();

// 有参无返回值
void (^block3)(int a) = ^(int b){
};
block3(1);

// 有参有返回值
int (^block4)(int a) = ^(int b){
    return 0;
};
int res4 = block4(4);
复制代码

不要忘了调用哦。

当然,我们在使用的时候,需要把block作为属性,那我们就先定义block:

// 定义block
typedef void (^MyBlock)(int a, int b);
// 声明
@property (nonatomic, copy) MyBlock myBlockOne;
// 实现
self.myBlockOne = ^int(int a, int b) {
    NSLog(@"%d",a + b);
};
// 调用
self.myBlockOne(10, 20);
复制代码

当然,我们还可以利用block捕获变量、代码传递、代码内联的特性,来做个链式编程:

// 创建一个NSArray分类
#import <UIkit/UIKit.h>

typedef id(^transitionItemBlock)(id item);
typedef NSArray *(^transitionArrayBlock)(transitionItemBlock item);

@interface NSArray (transitionExtension)

@property (nonatomic, copy, readonly) transitionArrayBlock transition;

@end

// 实现
@implementation NSArray (transitionExtension)

- (transitionArrayBlock)transition
{
    transitionArrayBlock transition = ^id(transitionItemBlock item) {
        NSMutableArray *items = [NSMutableArray array];
        for (id data in self) {
            [items addObject:item(data)];
        }
        return items;
    };
    return transition;
}

// 重写setter方法保证block不会被外部修改实现
- (void)setTransition:(transitionArrayBlock)transition {
}

@end

// 使用 - 将数组中的字典转换成对应的数据模型
NSArray <NSDictionary *> *orginalDatas = @[@{ ... }, @{ ... }, @{ ... }];
NSArray <Model *> *models = orginalDatas.transition(^id(id item) {
    return [[Model alloc] initWithDict:item];
});

复制代码

2.循环引用

思考:为什么会产生循环引用呢?

  1. 产生循环引用的原因就是AB之间互相强持有,导致无法释放。这里本质上是:self -> block -> self.someone
  2. 所以要打破循环引用我们就必须打破@1:self->block的强持有关系或者打破@2:block -> self.someone的强持有关系。

怎么解决循环引用

但是@1我们在声明的时候用weak关键字会收到一个这样的警告:Assigning block literal to a weak property; object will be released after assignment(将block赋值给弱属性;对象在赋值后将被释放)。所以不可以用weak来修饰。我们只能从@2想办法了,直接贴上吧:

// 方式1,大家都在用的常规操作
__weak __typeof(self) weakSelf = self;
self.block= ^{
    weakSelf.name = @"张三";
};
self.block();

// 方式2,当做参数进去
self.block = ^(ViewController *vc) {
    NSLog(@"%@",vc.name);
};
self.block(self);

// 方式3,__block
__block ViewController *vc = self;
self.block = ^{
    NSLog(@"%@",vc.name);
    vc = nil;
};
self.block();
复制代码

总而言之,我们不管怎么使用,发现都是代码块和self之间的通讯,所以我们还可以用通知或者代理。

在上述使用中,我们发现block不可以用weak进行修饰,这是不是说明block也是一个对象呢?接下来我们探索一下block内部是怎么实现的。


3.内部结构

现有如下代码:

int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
};     
block();
复制代码

我们在文件当前目录下执行这个命令:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xxx.m,我们就会在同目录下得到一份后缀为.cpp的文件。上述代码编译后.cpp文件中该部分:

int age = 20;
// 这是block的定义
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 这是block的调用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
复制代码

整理一下就是这样的:

int age = 20;
void (*block)(void) = &__main_block_impl_0(
						__main_block_func_0, 
						&__main_block_desc_0_DATA, 
						age
						);
// 这是block的调用
block->FuncPtr(block);
复制代码

我们发现block就是__main_block_impl_0这个结构体,所以我们的重点就是研究这个结构体。

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_impl
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
复制代码
  • __main_block_desc_0
static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size; // 结构体__main_block_impl_0 占用的内存大小
}
复制代码

构造方法;

// 构造函数(类似于OC中的init方法) _age是外面传入的
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    
        //isa指向_NSConcreteStackBlock 说明这个block就是_NSConcreteStackBlock类型的
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
复制代码

有个大神画了一幅图:

16c09240319606d9.png

isa指针:指向表明该block类型的类。

flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。

reserved:保留变量,我的理解是表示block内部的变量数。

invoke:函数指针,指向具体的block实现的函数调用地址。

descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。

variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。
复制代码

这样一看就很清晰了。

我们知道了block的结构,接下应该分析的是block内部对变量的捕获方式。现有如下代码:

int a = 10;
self.block = ^{
    NSLog(@"%d",a);
};
a = 20;
self.block();

输出
2021-04-22 17:48:21.546838+0800 newTest[80807:6365980] 10
复制代码

继续查看.cpp文件:

int age = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 20;

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;

NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_d36452_mi_5);

复制代码

可以看到,直接把age的值10传到了结构体__main_block_impl_0中,后面再修改age = 20并不能改变block里面的值。

接下来测试一下static修饰的局部变量:

static int height = 30;
int age = 20;
self.block = ^{
    NSLog(@"age is %d height = %d",age,height);
};
age = 25;
height = 35;
self.block();

输出
2021-04-22 18:20:04.942832+0800 newTest[80942:6371469] age = 20 height = 35
复制代码

可以看到,这种情况下外部修改的值影响了block内部的值。我们来看看这里的.cpp:

static int height = 30;
int age = 20;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 25;
height = 35;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
复制代码

可以看到传值的时候,age是直接传的age,height传的是&height。

最后再看看全局变量的情况:

int age1 = 11;
static int height1 = 22;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) =  ^{
            NSLog(@"age1 = %d height1 = %d",age1,height1);
        };
        age1 = 25;
        height1 = 35;
        block();

    }
    return 0;
}

输出
2021-04-22 18:20:04.942832+0800 newTest[80942:6371469] age1 = 25 height1 = 35
复制代码

同样的查看.cpp:

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;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_4e8c40_mi_4,age1,height1);
}

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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        age1 = 25;
        height1 = 35;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        
    }
    return 0;
}
复制代码

可以看到是直接访问的age1和height1,压根没有捕获。

总结一下,因为局部变量离开作用域就销毁了。那如果是指针传递的话,可能导致访问的时候,该变量已经销毁了。程序就会出问题。而全局变量本来就是在哪里都可以访问的,所以无需捕获。


4.block的类型

要验证是不是block是不是对象十分简单,我们进行如下操作:

void (^block)(void) =  ^{
    NSLog(@"123");
};
NSLog(@"block.class = %@",[block class]);
NSLog(@"block.class.superclass = %@",[[block class] superclass]);
NSLog(@"block.class.superclass.superclass = %@",[[[block class] superclass] superclass]);
NSLog(@"block.class.superclass.superclass.superclass = %@",[[[[block class] superclass] superclass] superclass]);

日志是:
newTest[18429:234959] block.class = __NSGlobalBlock__
newTest[18429:234959] block.class.superclass = __NSGlobalBlock
newTest[18429:234959] block.class.superclass.superclass = NSBlock
newTest[18429:234959] block.class.superclass.superclass.superclass = NSObject
复制代码

可以看到,block的根类也是NSObject,和我们其他的类一样的。

事实上,block共有三种类型:

  • __NSGlobalBlock__ ( _NSConcreteGlobalBlock ):没有访问auto变量
  • __NSStackBlock__ ( _NSConcreteStackBlock ):访问了auto变量
  • __NSMallocBlock__ ( _NSConcreteMallocBlock ):__NSStackBlock__调用了copy

我们在之前的探索里面也有所理解。

接下来我们分几种情况讨论block在内存中的copy情况:

  • block作为函数返回值时:
// 定义Block
typedef void (^YZBlock)(void);

// 返回值为Block的函数
YZBlock myblock()
{
    int a = 6;
    return ^{
        NSLog(@"--------- %d",a);
    };
}

YZBlock Block = myblock();
Block();
NSLog(@" [Block class] = %@", [Block class]);

输出:
[25857:385868] --------- 6
[25857:385868]  [Block class] = __NSMallocBlock__
复制代码

上述代码如果在MRC下输出__NSStackBlock__,在ARC下,自动copy,所以是__NSMallocBlock__

  • 将block赋值给__strong指针时:
// 定义Block
typedef void (^YZBlock)(void);

int b = 20;
YZBlock Block2 = ^{
    NSLog(@"abc %d",b);
};
NSLog(@" [Block2 class] = %@", [Block2 class]);

输出:
[Block2 class] = __NSMallocBlock__
复制代码

上述代码如果在MRC下输出__NSStackBlock__,在ARC下,自动copy,所以是__NSMallocBlock__

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

就是Foundation下,苹果自带的一些方法,比如 数组的遍历enumerateObjectsUsingBlock:这个方法也是传入的是block,所以这个也是__NSMallocBlock__类型。

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

只要是GCD里面的方法参数是block时,它都是__NSMallocBlock__

MRC下block属性的建议写法:

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

ARC下block属性的建议写法:

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

小结:

  • 当block为__NSStackBlock__类型时候,是在栈空间,无论对外面使用的是strong还是weak都不会对外面的对象进行强引用。
  • 当block为__NSMallocBlock__类型时候,是在堆空间,block是内部的_Block_object_assign函数会根据strong或者weak对外界的对象进行强引用或者弱引用。
  • 当block内部访问了对象类型的auto变量时,如果block是在栈上,将不会对auto变量产生强引用。如果block被拷贝到堆上,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。
  • 如果block从堆上移除,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量(release)。

5.说一下__block

先举个小例子:

// 定义block
typedef void (^YZBlock)(void);

int age = 10;
YZBlock block = ^{
    NSLog(@"age = %d", age);
};
block();

输出
age = 10
复制代码

那我们想要修改age怎么办呢?

修改局部变量的三种方法:

  1. 写成全局变量:
// 定义block
typedef void (^YZBlock)(void);
 int age = 10;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YZBlock block = ^{
            age = 20;
            NSLog(@"block内部修改之后age = %d", age);
        };
        
        block();
        NSLog(@"block调用完 age = %d", age);
    }
    return 0;
}

输出:
block内部修改之后age = 20
block调用完 age = 20
复制代码

因为全局变量,是所有地方都可访问的,在block内部可以直接操作age的内存地址的。调用完block之后,全局变量age指向的地址的值已经被更改为20,所以是上面的打印结果。

  1. static修改局部变量:
// 定义block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       static int age = 10;
        YZBlock block = ^{
            age = 20;
            NSLog(@"block内部修改之后age = %d", age);
        };
        
        block();
        NSLog(@"block调用完 age = %d", age);
    }
    return 0;
}

输出
block内部修改之后age = 20
block调用完 age = 20
复制代码

看一下.cpp文件:

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; // bound by copy
    
    (*age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_5dbaa1_mi_0, (*age));
}
复制代码

可以看出,当局部变量用static修饰之后,这个block内部会有个成员是int *age,也就是说把age的地址捕获了。这样的话,当然在block内部可以修改局部变量age了。

以上两种方法,虽然可以达到在block内部修改局部变量的目的,但是,这样做,会导致内存无法释放。无论是全局变量,还是用static修饰,都无法及时销毁,会一直存在内存中。很多时候,我们只是需要临时用一下,当不用的时候,能销毁掉,那么第三种,也就是今天的主角 __block隆重登场。看代码:

typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       __block int age = 10;
        YZBlock block = ^{
            age = 20;
            NSLog(@"block内部修改之后age = %d",age);
        };
        
        block();
        NSLog(@"block调用完 age = %d",age);
    }
    return 0;
}

输出:
block内部修改之后age = 20
block调用完 age = 20
复制代码

看看.cpp:

  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    // 这里多了__Block_byref_age_0类型的结构体
  __Block_byref_age_0 *age; // by ref
    // fp是函数地址  desc是描述信息  __Block_byref_age_0 类型的结构体  *_age  flags标记
  __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; //fp是函数地址
    Desc = desc;
  }
};
复制代码

再仔细看结构体__Block_byref_age_0,可以发现第一个成员变量是isa指针,第二个是指向自身的指针__forwarding

// 结构体 __Block_byref_age_0
struct __Block_byref_age_0 {
    void *__isa; //isa指针
    __Block_byref_age_0 *__forwarding; // 指向自身的指针
    int __flags;
    int __size;
    int age; //使用值
};
复制代码

看一下main函数的.cpp:

// 1.这是原始的代码 __Block_byref_age_0
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

----------------------------------------------------------------------
        
//这是简化之后的代码 __Block_byref_age_0
__Block_byref_age_0 age = {
     0, //赋值给 __isa
     (__Block_byref_age_0 *)&age,//赋值给 __forwarding,也就是自身的指针
      0, // 赋值给__flags
      sizeof(__Block_byref_age_0),//赋值给 __size
      10 // age 使用值
    };
        
// 2.这是原始的 block代码
YZBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

----------------------------------------------------------------------
        
// 3.这是简化之后的 block代码
YZBlock block = (&__main_block_impl_0(
             		__main_block_func_0,
           		&__main_block_desc_0_DATA,
	           	 &age,
            	570425344));
        
 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        //简化为
block->FuncPtr(block);
复制代码

其中__Block_byref_age_0结构体中的第二个(__Block_byref_age_0 *)&age赋值给上面代码结构体__Block_byref_age_0中的第二个__Block_byref_age_0 *__forwarding,所以__forwarding里面存放的是指向自身的指针。

// 这是简化之后的代码 __Block_byref_age_0
__Block_byref_age_0 age = {
       0,                          //赋值给 __isa
       (__Block_byref_age_0 *)&age,//赋值给 __forwarding,也就是自身的指针
       0,                          // 赋值给__flags
       sizeof(__Block_byref_age_0),//赋值给 __size
       10                          // age 使用值
};


// 结构体 __Block_byref_age_0
struct __Block_byref_age_0 {
    void *__isa; //isa指针
    __Block_byref_age_0 *__forwarding; // 指向自身的指针
    int __flags;
    int __size;
    int age; //使用值
};
复制代码

调用的时候,先通过__forwarding找到指针,然后去取出age值:(age->__forwarding->age));

小结:

  • __block可以用于解决block内部无法修改auto变量值的问题
  • __block不能修饰全局变量、静态变量(static)
  • 编译器会将__block变量包装成一个对象。调用的是,从__Block_byref_age_0的指针找到age所在的内存,然后修改值

6.内存管理

看代码:

// 定义block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
     
        NSObject *obj = [[NSObject alloc]init];
        YZBlock block = ^{
            NSLog(@"%p",obj);
        };
         block();
    }
    return 0;
}
复制代码

还是看.cpp:

block内存管理.png

从栈上拷贝到堆上,结构体__main_block_desc_0中有copy和dispose。

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


static void __main_block_copy_0(struct __main_block_impl_0*dst, 
struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, 
(void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
复制代码

copy会调用 __main_block_copy_0,其内部的_Block_object_assign会根据代码中的修饰符 strong或者weak而对其进行强引用或者弱引用。

查看__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //strong 强引用
  NSObject *__strong obj;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

可以看出修饰符是strong,所以,调用_Block_object_assign时候,会对其进行强引用。

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

  • 当block被copy到堆时:

    • 会调用block内部的copy函数

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

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

  • 当block从堆中移除时:

    • 会调用block内部的dispose函数

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

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

__block__forwarding指针:

//结构体__Block_byref_obj_0中有__forwarding
 struct __Block_byref_obj_0 {
  		void *__isa;
		__Block_byref_obj_0 *__forwarding;
		 int __flags;
 		int __size;
 		void (*__Block_byref_id_object_copy)(void*, void*);
 		void (*__Block_byref_id_object_dispose)(void*);
 		NSObject *__strong obj;
};

// 访问的时候
age->__forwarding->age
复制代码

为啥什么不直接用age,而是age->__forwarding->age呢?

这是因为,如果__block变量在栈上,就可以直接访问,但是如果已经拷贝到了堆上,访问的时候,还去访问栈上的,就会出问题,所以,先根据__forwarding找到堆上的地址,然后再取值。

总结

  • 当block在栈上时,对它们都不会产生强引用

  • 当block拷贝到堆上时,都会通过copy函数来处理它们:__block变量(假设变量名叫做a):_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

  • 对象类型的auto变量(假设变量名叫做p):_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

  • 当block从堆上移除时,都会通过dispose函数来释放它们:__block变量(假设变量名叫做a),_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

  • 对象类型的auto变量(假设变量名叫做p):_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

大佬轻喷:完

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享