美文网首页
iOS-浅谈OC中的Block

iOS-浅谈OC中的Block

作者: 晴天ccc | 来源:发表于2019-06-25 15:28 被阅读0次

目录

  • 简介
    ----声明/定义一个Block变量
    ----使用typedef定义一个Block
  • Block基本使用
    ----Block变量的赋值格式
    ----Block作为属性
    ----Block作为OC函数参数
    ----Block作为OC函数参数拓展
    ----当Block作为返回值
  • Block在C/C++(底层)的数据结构
    ----观察Block的class等信息
    ----观察__main_block_func_0结构体
    ----观察Block代码具体执行区
  • Block的类型
    ----区分创建的Block是那种类型?
    ----三种类型内存分布
    ----为什么Block要调用copy把栈Block复制到堆区呢?
    ----在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上的几种情况
    ----最后总结

简介

BlockOC中一种比较特殊的对象,和其他OC对象一样使用,只是比较特殊。
Block用来封装一段函数/代码块和函数的调用环境,等到需要的时候再去调用。
封装:Block内部会把Block的参数,返回值,执行体封装成一个函数,并且存储该函数的内存地址。
调用环境:Block内部会捕获变量,并且存储这些捕获的变量。

Block定义

  • 声明/定义一个Block变量

返回值类型 (^方法名)(参数列表) = ^(参数列表){ 代码块 };
void (^myBlock1)(void);  //无返回值,无参数
void (^myBlock2)(NSObject, int); //无返回值,有参数
void(^myBlock3)(NSString *, NSString *);  // 形参变量名称可以省略,只留有变量类型即可  
NSString* (^myBlock4)(NSString *x,NSString *y); //有返回值和参数,并且在参数类型后面加入了参数名(仅为可读性)
  • 使用typedef定义一个Block

在实际使用Block的过程中,我们可能需要重复第声明多个相同返回值相同参数列表的Block变量,如果总是重复地编写一长串代码来声明变量会非常繁琐,所以我们可以使用typedef来定义Block类型。

typedef 返回类型 (^方法名)(参数列表)
typedef void(^ calculate)(int,int);

calculate minusBlock = ^(int value1,int value1){
        NSLog(@"I am a block");
        return value1 - value2;
 };
calculate sumBlock = ^(int value1,int value1){
        NSLog(@"I am a block");
        return value1 + value2;
 };

Block的基本使用

  • Block变量的赋值格式为:

Block变量 = ^(参数列表){函数体};
myBlock4 = ^(NSString *x, NSString *y){
  NSLog(@“x:%, y:%@“, x, y);
};

其中右边的返回值类型和参数类型为空的时候可以省略不写

void (^block)(void) = ^{};

备注:Block变量的赋值格式可以是: Block变量 = ^返回值类型(参数列表){函数体};
不过通常情况下都将返回值类型省略,因为编译器可以从存储代码块的变量中确定返回值的类型。

    // Block最简单的形式
    void (^ablock)(void);  // 定义一个Block
    ablock = ^(void){NSLog(@"I am a block");};  // 包一个代码块
    ablock();  // 在我们想要的时候进行调用

    // 带参数的Block
    void (^ sumBlock)(int,int) = ^(int value1,int value1){
      NSLog(@"两数之和为:%d",value1 + value2);
    };
    sumBlock();

    // 带参数和返回值的Block
    int (^ sumBlock)(int,int);
    sumBlock  = ^(int value1,int value1){
        NSLog(@"I am a block");
        return value1 + value2;
    };
    sumBlock();
  • 当Block作为属性时:

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

或者

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

这种用法最常见的就是平时我们在cell中的响应事件的处理,有时使用block来回调到VC去处理会更加方便


@interface TableViewCell : UITableViewCell
@property (copy,nonatomic) void (^aBlock)(void); // 无参数
@property (copy,nonatomic) void (^bBlock)(NSString * , NSDictionary *);  // 有参数
@end

@implementation TableViewCell

- (void)clickAction1{
    if(self. aBlock){
        self. aBlock();
    }
}

- (void)clickAction2{
    NSString * keyStr = @"YES";
    NSDictionary * dict = @{@"uid" : @"100008" };

    if(self. bBlock){
        self. bBlock(keyStr,dict);
    }
}

@end

@interface ViewController : UIViewController
@end

@implementation ViewController

[self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass( [EP_Files_ProListCell class] ) bundle:nil] forCellReuseIdentifier:[EP_Files_ProListCell cellID]];

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    EP_Files_ProListCell * cell = [tableView dequeueReusableCellWithIdentifier:[EP_Files_ProListCell cellID]];
    cell. aBlock = ^{
        //do anything
    };
    cell. bBlock = ^(NSString *keyStr, NSDictionary *dicT){
        //do anything
    };
    return cell;
} 
@end

  • Block作为OC函数参数

既然BlockiOS中一种比较特殊的数据类型,那就代表Block可以作为函数参数进行传递。

    // 1、使用typedef定义Block类型
    typedef int(^MyBlock)(int, int);

    // 2、定义一个形参为Block的OC函数
    -(void)useBlockForOC:(MyBlock)aBlock{
        NSLog(@"result = %d", aBlock(300,200));
    }
 
    // 3、声明并赋值定义一个Block变量
    MyBlock addBlock = ^(int x, int y){
        return x+y;
    };
    
    // 4、以Block作为函数参数,把Block像对象一样传递
    [self useBlockForOC:addBlock];
    
    // 5、将第3点和第4点合并一起,以内联定义的Block作为函数参数
    [self useBlockForOC:^(int x, int y){
        return x+y;
    }];
  • Block作为OC函数参数拓展

有时候我们需要从一个方法中返回一个值时,但刚好需要经过GCD延时处理后赋值才返回,这种场景用return时不行的,因为GCD中的block返回值类型为空,那么这时候可以用block来回调返回值。

typedef void (^aBlock)(NSString *value);

- (void)test{
    [self doSomeThingWithBlock:^(NSString *value) {
        NSLog(@"%@",value);
    }];
}

- (void)doSomeThingWithBlock:(aBlock)block{
    NSString *value = @"1";
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        value = @"2";
        block(value);
    });
}
  • 当Block作为返回值时

我们经常使用的Masonry框架内部实现就大量用到block返回值来实现链式调用的语法

[_iconImg mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.left.bottom.right.mas_equalTo(0);
    }];

Block的底层数据结构

  • 观察Block的class等信息

NSLog(@"block.class = %@",[block class]);
NSLog(@"block.class.superclass = %@",[[block class] superclass]);
NSLog(@"block.class.superclass.superclass = %@",[[[block class] superclass] superclass]);
block.class = __NSMallocBlock__
block.class.superclass = NSBlock
block.class.superclass.superclass = NSObject

我们可以发现:

  • 它继承自NSBlock,而NSBlock又继承自NSObject。所有的OC对象都继承自NSObject,所以Block也是一个OC对象,下面会发现它里面也有isa指针,更加证明了这一点。
  • 转化成C/C++代码来观察

main函数中声明一个block对象

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 1、定义一个block
        void (^aBlock)(void);
        // 2、把block指向一个代码块
        aBlock = ^{
            NSLog(@"hello world");
          };
        // 3、调用执行block
        aBlock();
    }
    return 0;
}

转化成C/C++代码,来查看底层实现。

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;   // 最终的指针接收
};

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

// Block的具体代码执行
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_69540b_mi_0);
}

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; 

        // 1、定义一个block
        void (*aBlock)(void);
        // 2、把block指向一个代码块,调用__main_block_impl_0结构体内部的构造方法
        aBlock = ((void (*)())&__main_block_impl_0(
                                                   (void *)__main_block_func_0,
                                                   &__main_block_desc_0_DATA)
                                                  );
        // 3、调用执行block
        ((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

  • 观察__main_block_func_0结构体

从上面看出我们定义的aBlock最终指向给了__main_block_func_0这个结构体,所以Block本质是一个的结构体对象。
根据OC对象的知识,通过层层剥离,其实__main_block_func_0这个结构体也可以简化成以下组成:

// Block结构体
struct __main_block_impl_0 {
//    struct __block_impl impl;
    void *isa;  //说明block是一个oc对象
    int Flags;
    int Reserved;
    void *FuncPtr;  //所封装的函数的地址
//    struct __main_block_desc_0* Desc;
    size_t reserved;
    size_t Block_size;   //block的大小
// 构造函数 
    __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;
    }
};

我们可以发现Block本质是一个C++结构体:

  • isa指针,指向Block所属的类。证明Block本质上是一个OC对象
  • __block_impl impl的内存地址就是__main_block_impl_0的内存首地址。
  • *FuncPtr指针,指向该 Block的执行函数。
  • __main_block_desc_0保存了Block描述信息。
  • 还有Block的构造函数。
  • 还有Block捕获的变量(这里暂未展示)。
  • 观察block代码具体执行区
^{
   NSLog(@"hello world");
};

大括号里面的代码就是block的具体执行,在Block的初始化方法中:

// 2、把block指向一个代码块,调用__main_block_impl_0结构体内部的构造方法
aBlock = ((void (*)())&__main_block_impl_0(
                                           (void *)__main_block_func_0,  // 构造方法第一个参数,一个指针
                                           &__main_block_desc_0_DATA) 
                                          );

截取部分相关方法:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;  // 最终的指针接收
};

// 内部构造函数
__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;
}

// Block的代码具体执行区
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_j2_bxd08__54lzbn226msl27gww0000gn_T_main_69540b_mi_0);
}

我们可以发现:

  • __main_block_func_0封装的是Block内部具体执行函数。
  • 通过__main_block_impl_0内部构造方法将__main_block_func_0的地址传给参数参数*fp
  • 在构造方法中*fp最终传递给了__block_impl结构体中的FuncPtr 方法
  • 又通过上面得知__block_impl impl的内存地址就是__main_block_impl_0的内存首地址。

总结:其实相当于通过指针传递的方式把整段代码执行传递进入了block中。Block是一个封装了函数调用以及函数调用环境的OC对象

Block的类型

通过调用class方法查看其类型以及继承链发现:block有三种类型。

(__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject)
(__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject)
(__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject)
  • 如何区分创建的Block是那种类型?

如果创建了一个block,但是没有访问auto变量,block__NSGlobalBlock__类型:

void (^block1)(void) = ^ {
};

如果创建了一个block,访问了auto变量,block__NSStackBlock__类型:

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

需要在MRC环境下验证,在ARC环境下,编译器可能会将栈上的block复制到堆上

如果一个__NSStackBlock__类型block,调用了copy方法,block变成__NSMallocBlock__类型:

int age = 10;
void (^block3)(void) = [^{
    NSLog(@"age = %d",age);
} copy];

总结如下图


  • Block三种类型内存分布
  • 各个类型Block调用copy的结果:

  • 为什么Block要调用copy把栈Block复制到堆区呢?

Block刚被创建出来时候,若不是GlobalBlock就是StackBlock,栈内存是系统自动管理,超过作用域就会释放。
如果不把栈Block复制到堆区,很有可能调用栈Block的时候它已经被销毁,就会导致数据错乱。

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

1、block作为函数返回值时。
2、将block赋值给__strong指针时。
3、block作为Cocoa API中方法名含有usingBlock的方法参数时。
4、block作为GCD API的方法参数时。
5、block访问了auto变量。

  • Block三种类型的最后总结

block分类三种类型,通过是否访问auto变量和调用copy决定.
不同类型的block调用copy效果不同。
平时使用block时一般会用copy修饰。因为放在栈区的block的内存由系统管理,调用block时可能出现内部存储的数据错乱问题,所以需要拷贝到堆区进行使用。

ARCblock属性的建议写法:

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

MRCblock属性的建议写法:

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

部分地方参考文章:https://www.jianshu.com/p/a347faca2643

相关文章

  • iOS-浅谈OC中的Block

    KVO简介和基本使用 KVO全称是Key-Value Observing,俗称“键值监听”,可用于监听某个对象属性...

  • 浅谈OC中block

    Block的循环引用:block强引用self,self强引用block。解决方案:对self进行弱引用。 内部修...

  • swift 调用 OC中的block

    OC中声明block; OC中实现block swift中实现

  • 浅谈OC中Block的本质

    Block简介 block是将函数及其执行上下文封装起来的一个对象 在block实现的内部,有很多变量,因为blo...

  • iOS原生&JS交互

    OC 获取js中的key OC调用js方法 JS调用OC中不带参数的block JS调用OC中带参数的block ...

  • Swift之闭包

    前言 闭包类似于OC的block,但是比OC的block应用面更广 在OC中block是匿名函数 在swift中函...

  • iOS-浅谈OC中Block的变量捕获和__block修饰符

    KVC简介和基本使用 KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某...

  • Swift 之闭包

    闭包 闭包类似于 OC 的 block,但是比 OC 的 block 应用面更广 在 OC 中 block 是匿名...

  • iOS Block本质笔记

    OC中定义block block访问外部参数 OC转C++分析 block的变量捕获机制 为了保证block能够正...

  • iOS-浅谈OC中的Runloop

    LLDB是Xcode自带的调试器,可以通过一些命令获得想要的信息 常用打印 读取内存 修改内存中的值 格式 字节大小

网友评论

      本文标题:iOS-浅谈OC中的Block

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