美文网首页iOS-Block相关
block:block是什么、block的本质、block的类型

block:block是什么、block的本质、block的类型

作者: 意一ineyee | 来源:发表于2019-10-02 13:25 被阅读0次
一、block是什么
二、block的本质
三、block的类型

一、block是什么


block其实也是一个OC对象,它享有所有OC对象的待遇,只不过普通OC对象用来封装数据,而block用来封装函数以及函数的调用环境。所谓封装函数,是指block内部会把block的参数、返回值、执行体封装成一个函数,并且存储该函数的内存地址;所谓封装函数的调用环境,是指block内部会捕获变量,并且存储这些捕获的变量。

  • block的声明

block作为属性时,这样声明:

@property (nonatomic, copy) int (^block)(int a, int b);

block作为方法的参数时,这样声明:

- (void)fecthDataWithCompletionHandler:(void (^)(NSData *data, NSError *error))completionHandler;

如果项目中使用了大量相同类型的block,那为了使代码更简洁,我们可以先typedef一下block的类型,然后再声明。则上面两例可以写成这样:

typedef int (^Block)(int a, int b);

@property (nonatomic, copy) Block block;
typedef void (^Block)(NSData *data, NSError *error);

- (void)fecthDataWithCompletionHandler:(Block)completionHandler;
  • block的实现

箭头打头就代表block的实现,如果block没有返回值,可省略returnType,如果block没有参数,可省略params。

^returnType(params) {
    
    // block的执行体
};

不过通常我们都会把block的实现用一个变量记录下来,以便将来调用,就像函数那样。

returnType (^blockName)(params) = ^returnType(params) {
    
    // block的执行体
};
  • block的调用

像C语言那样加小括号就代表block的调用,如果block没有返回值,则不接收返回值,如果block没有参数,可省略params。

returnType v = blockName(params);
  • block代码的执行顺序

block代码的执行顺序永远都是:block的声明 --> block的实现 --> block的调用 --> 最后返回去真正去执行block的实现代码。举例如下:

#import "ViewController.h"

@interface ViewController ()

// 第一步:block的声明
@property (nonatomic, copy) int (^block)(int a, int b);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 第二步:block的实现
    self.block = ^int(int a, int b) {
        
        // 第四步:最后返回去真正去执行block的实现代码
        return a + b;
    };
    
    // 第三步:block的调用
    int num = self.block(1, 2);
    NSLog(@"%d", num);
}

@end

二、block的本质


我们简单创建一个block,并调用它。

// 创建一个block
void (^block)(void) = ^{
    
    NSLog(@"11");
};

// 调用block
block();

然后用clang编译器把这段OC代码转换成C/C++代码,来窥探一下block的本质。(伪代码)

// block的本质,是一个C++结构体
struct __block_impl_0 {
    struct __block_impl impl;
    struct __block_desc_0* Desc;
    
    /**
     * block构造函数
     *
     * @param fp block对应函数的内存地址
     * @param desc block描述信息结构体的内存地址
     *
     * @return 返回一个当前类型的结构体————即返回一个__block_impl_0类型的结构体
     */
    __block_impl_0(void *fp, struct __block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;// block所属的类
        impl.Flags = flags;
        impl.FuncPtr = fp;// 把block对应函数的内存地址存储在block内部
        Desc = desc;// 把block描述信息结构体的内存地址存储在block内部
    }
};

// block实现信息结构体
struct __block_impl {
    void *isa;// 指向block所属的类
    int Flags;
    int Reserved;
    void *FuncPtr;// 指向block对应的函数
};

// block描述信息结构体
struct __block_desc_0 {
  size_t reserved;
  size_t Block_size;// block的实际大小
};

可见block的本质就是一个C++的__block_impl_0结构体,该结构体内部有两个成员变量,第一个成员变量内部有一个isa指针指向block所属的类(这也证明block确实是一个OC对象),还有一个FuncPtr指针指向该block对应的函数;第二个成员变量内部则存储着该block的一些描述信息,如该block的实际大小、copy函数、dispose函数等。此外,该结构体内部还有一个block构造函数,它用来创建并初始化一个block——即一个__block_impl_0类型的结构体。当然我们也不能忘了,block内部还可以有更多的成员变量,它们就是block捕获的变量。

// 首先把block的参数、返回值、执行体封装成一个__block_func_0函数
void __block_func_0(struct __block_impl_0 *__cself) {
    
    // block的执行体
    NSLog("11");
}

// 然后把block的描述信息封装进一个__block_desc_0结构体
struct __block_desc_0 {
  size_t reserved;
  size_t Block_size;// block的实际大小
} __block_desc_0_DATA = {
    0,
    sizeof(struct __block_impl_0)// 计算block的实际大小
};


// 创建一个block
void (*block)(void) = &__block_impl_0(// &是指获得函数的地址,来调用
                                      __block_func_0,// 把函数的地址传进去
                                      &__block_desc_0_DATA// 把结构体的地址传进去
                                      );

// 调用block
block->impl.FuncPtr(block);

可见创建block的本质,确实就是把block的参数、返回值、执行体封装成一个__block_func_0函数,并且存储该函数的内存地址,然后block还会捕获变量,并且存储这些捕获的变量。

而调用block的本质,也确实就是找到block内部FuncPtr指针指向的函数来调用。

三、block的类型


1、全局block、栈block、堆block,堆block就是把栈block copy了一份到堆区

(注意:这一小节的示例代码都是在MRC下的)

block有三种类型:全局block(__NSGlobalBlock__)、栈block(__NSGlobalBlock__)、堆block(__NSGlobalBlock__),它们都继承自NSBlock。那什么是全局block?什么是栈block?什么又是堆block?全局block是指存储在全局区的block,栈block是指存储在栈区的block,堆block是指存储在堆区的block,所以说看一个block是什么类型,不是看它在代码的什么位置定义的,而是看它存储在哪块内存分区中——即系统把它存储在哪块内存分区中了。这在代码中有什么体现呢?也就是说我们如何通过代码一眼就能知道这个block是什么类型的呢?

  • 全局block

没有访问外界普通局部变量的block就是全局block,系统会把这样的block放在全局区。

// 普通全局变量
//int age = 25;
// 静态全局变量
//static int age = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 静态局部变量
        static int age = 25;
        
        void (^block)(void) = ^{
            // 访问外界的变量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%@", [block class]);// __NSGlobalBlock__
        NSLog(@"%@", [[block class] superclass]);// __NSGlobalBlock
        NSLog(@"%@", [[[block class] superclass] superclass]);// NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);// NSObject
    }
    return 0;
}
  • 栈block

访问了外界普通局部变量的block就是栈block,系统会把这样的block放在栈区,可见栈block和全局block是完全对立的。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = ^{
            // 访问外界的变量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%@", [block class]);// __NSStackBlock__
        NSLog(@"%@", [[block class] superclass]);// __NSStackBlock
        NSLog(@"%@", [[[block class] superclass] superclass]);// NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);// NSObject
    }
    return 0;
}
  • 堆block

对栈block执行一下copy操作,copy方法返回的就是一个堆block,所以说堆block就是把栈block copy了一份到堆区。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = [^{
            // 访问外界的变量
            NSLog(@"%d", age);
        } copy];// 需适时的[block release]一下
        
        NSLog(@"%@", [block class]);// __NSMallocBlock__
        NSLog(@"%@", [[block class] superclass]);// __NSMallocBlock
        NSLog(@"%@", [[[block class] superclass] superclass]);// NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);// NSObject
    }
    return 0;
}

你会发现在平常的开发中,我们用到的总是堆block,而不是栈block。这是因为ARC下系统会自动复制一份栈block到堆区,而MRC下则需要我们手动调用copy方法让系统复制一份栈block到堆区。那为什么非要复制一份栈block到堆区?栈block有什么问题吗?

void (^block)(void);
void test() {
    
    // 普通局部变量
    int age = 25;
    
    block = ^{
        // 访问外界的变量
        NSLog(@"%d", age);// -272632440,不是25
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
    }
    return 0;
}

试看上面这段代码,我们定义了一个block,并且它的实现部分访问了外界的普通局部变量,所以它是一个栈block。而我们知道栈内存是由系统自己管理的,在出了相应的作用域后栈内存就会自动释放,可以供别人使用了,你的数据有可能就被别人替换掉。那么test方法执行完后,block超出作用域,已经被系统释放掉了,此时虽然说我们还能通过block这个全局变量去访问那块内存,但那块内存里存的很有可能已经是别人的数据了,所以这个调用本身其实已经没有意义了。

总结一下:为什么要把栈block到copy到堆区?

block刚被创建出来时,若不是全局block就是栈block,而栈内存又是系统自动管理的,一旦超出变量的作用域,变量对应的内存就会被释放,所以如果不把栈block复制到堆区,就很有可能我们在调用栈block的时候它已经被销毁了,那就是瞎调用了,会导致数据错乱。

额外的考虑,拿来玩儿

上面我们知道了对栈block执行copy操作是在堆区复制出了一个新的block,那对全局block和堆block执行copy操作呢?

  • 全局block执行copy操作
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 静态局部变量
        static int age = 25;
        
        void (^block)(void) = ^{
            // 访问外界的变量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%p", block); // 0x100001060
        NSLog(@"%p", [block copy]); // 0x100001060
    }
    return 0;
}
  • 堆block执行copy操作
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = [^{
            // 访问外界的变量
            NSLog(@"%d", age);
        } copy];
        
        NSLog(@"%p", block); // 0x102005650
        NSLog(@"%p", [block copy]); // 0x102005650
    }
    return 0;
}

可见:

block类型 执行copy操作后的效果
全局block 什么也不做,不会产生新的block
栈block 复制一份栈block到堆区
堆block 仅仅是block的引用计数加1,不会产生新的block

2、ARC下系统会在某些情况下自动copy一份栈block到堆区

(注意:这一小节的示例代码都是在ARC下的)

  • block赋值给一个强指针时(即__strong修饰的指针),系统会自动把该栈block复制到堆区
typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
        
        Block block = ^{

            NSLog(@"%d", age);
        };
        
        // 等价于
//      __strong Block block = ^{
//
//            NSLog(@"%d", age);
//        };

        NSLog(@"%@", [block class]);// __NSMallocBlock__,是一个堆block
    }
    return 0;
}

上面代码中block变量是个强指针,等号右边的block本来是个栈block,但是在赋值给强指针时系统会自动把该栈block复制到堆区,所以就返回了一个堆block。当然如果我们把block变量变成弱指针,那block自然就还是栈block,系统不会自动复制了。

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
                 
        __weak Block block = ^{

            NSLog(@"%d", age);
        };

        NSLog(@"%@", [block class]);// __NSStackBlock__,是一个栈block
    }
    return 0;
}
  • block作为函数的返回值时,系统会自动把该栈block复制到堆区
typedef void(^Block)(void);
Block test() {
    int age = 25;
    
    return ^{
        
        NSLog(@"%d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%@", [test() class]);// __NSMallocBlock__,是一个堆block
    }
    return 0;
}

上面代码中test函数返回的block本来是个栈block,但是在作为函数的返回值系统会自动把该栈block复制到堆区,所以调用test函数时就得到了一个堆block。

  • GCD方法里的block,系统都会自动复制到堆区
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    
});

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
});

// 等等......
  • Foundation框架usingBlock方法里的block,系统都会自动复制到堆区
[[NSArray alloc] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
}];

[[[NSDictionary alloc] init] enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    
    
}];

// 等等......

相关文章

网友评论

    本文标题:block:block是什么、block的本质、block的类型

    本文链接:https://www.haomeiwen.com/subject/ajjquctx.html