iOS技术图谱之Block

一、Block的定义

约定:用法中的符号含义列举如下:

  • return_type 表示返回的对象/关键字等(可以是void,并省略)
  • block_name 表示block的名称
  • var_type 表示参数的类型(可以是void,并省略)
  • var_name 表示参数名称

1、Block声明及定义

//定义
return_type (^ blockname)(var_type) = ^return_type (var_type var_name){

};

//调用
block_name(var);

//返回类型为void
void (^block_name)(var_type) = ^void (var_type var_name){

};
//可以简写
void (^block_name)(var_type) = ^(var_type var_name){

};

//参数为void
return_type (^block_name)(void)= ^ return_type(void){

};
//可以简写
return_type (^block_name)(void)= ^ return_type{

};

//返回值和参数都为void
void (^block_name)(void)= ^void(void){

};
//可以简写
void (^block_name)(void)= ^{

};

2、使用typedef来声明Block

typedef return_type (^BlockTypeName)(var_type);

(1)用作属性

//声明
typedef void(^CompleteBlock)(Bool error,id response);
//block属性
@property (nonatomic, copy) CompleteBlock networkCompleteBlock;

(2)用作参数
//声明
typedef void (^ConfigBlock)(Config *config);
//block作参数
- (void)setNetworkConfig:(ConfigBlock)configblock {
  
};

3、Block用法

(1)局部位置声明一个Block型的变量

void (^globalBlockInMemory)(int number) = ^(int number){
     printf("%d \n",number);
};
globalBlockInMemory(90);

(2)@interface位置声明一个Block型的属性

//按钮点击Block
@property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);

(3)在定义方法时,声明Block型的形参

- (void)addClickedBlock:(void(^)(id obj))clickedAction;

4、Block中少见用法

(1)Block的内联用法

^return_type (var_type varName)
{
    //...
}(var);

(2)Block的递归调用

Block内部调用自身,递归调用是很多算法基础,特别是在无法提前预知循环终止条件的情况下。注意:由于Block内部引用了自身,这里必须使用__block避免循环引用问题。

__block return_type (^blockName)(var_type) = [^return_type (var_type varName)
{
    if (returnCondition)
    {
        blockName = nil;
        return;
    }
    // ...
    // 【递归调用】
    blockName(varName);
} copy];

【初次调用】
blockName(varValue);

(3)Block作为返回值

方法的返回值是一个Block,可用于一些“工厂模式”的方法中:

- (return_type(^)(var_type))methodName
{
    return ^return_type(var_type param) {
        // ...
    };
}

Masonry框架里面的:

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

二、Block的应用

1、响应事件

情景:UIViewContoller有个UITableView并是它的代理,通过UITableView加载CellView。现在需要监听CellView中的某个按钮(可以通过tag值区分),并作出响应。

在CellView.h中@interface位置声明一个Block型的属性,为了设置激活事件调用Block,接着我们在CellView.m中作如下设置:

// 激活事件
#pragma mark - 按钮点击事件
- (IBAction)btnClickedAction:(UIButton *)sender {
    if (self.btnClickedBlock) {
        self.btnClickedBlock(sender);
    }
}

随后,在ViewController.m的适当位置(- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{...代理方法)中通过setter方法设置CellView的Block属性。Block写着当按钮被点击后要执行的逻辑。

// 响应事件
cell.btnClickedBlock = ^(UIButton *sender) {
    //标记消息已读
    [weakSelf requestToReadedMessageWithTag:sender.tag];
    //刷新当前cell
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};

2、传递数据

例如HYBNetworking网络框架中请求成功时传递接口返回数据对象的Block:

[HYBNetworking postWithUrl:kSearchProblem refreshCache:NO params:params success:^(id response) {
        
        typeof(weakSelf) strongSelf = weakSelf;
//        [KVNProgress dismiss];
        NSString *stringData = [response mj_JSONString];
        stringData = [DES3Util decrypt:stringData];
        NSLog(@"stirngData: %@", stringData);
       ...
}

3、链式语法

链式编程思想:核心思想为将block作为方法的返回值,且返回值的类型为调用者本身,并将该方法以setter的形式返回,这样就可以实现了连续调用,即为链式编程。

简单使用链式编程思想实现一个简单计算器的功能:

//  CaculateMaker.h
//  ChainBlockTestApp

#import 
#import 

@interface CaculateMaker : NSObject

@property (nonatomic, assign) CGFloat result;

- (CaculateMaker *(^)(CGFloat num))add;

@end
//  CaculateMaker.m
//  ChainBlockTestApp


#import "CaculateMaker.h"

@implementation CaculateMaker

- (CaculateMaker *(^)(CGFloat num))add;{
    return ^CaculateMaker *(CGFloat num){
        _result += num;
        return self;
    };
}

@end
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);

三、Block使用注意事项

1、截获基本类型局部变量与_ _block修饰符

先来看一段代码:

int c = 10;

static int d = 10;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int a = 10;
    
    static int b = 10;
    
    void (^CatchVarBlock)(void) = ^{
        NSLog(@"%d",a);
        //编译报错
//        a = 30;
        b = 30;
        NSLog(@"%d",b);
        c = 30;
        NSLog(@"%d",c);
        d = 30;
        NSLog(@"%d",d);
        
    };
    
    a = 20;
    b = 20;
    c = 20;
    d = 20;
    CatchVarBlock();
}

打印结果:

2019-11-19 20:31:25.971462+0800 TestApp[3613:151099] 10
2019-11-19 20:31:25.971589+0800 TestApp[3613:151099] 30
2019-11-19 20:31:25.971687+0800 TestApp[3613:151099] 30
2019-11-19 20:31:25.971773+0800 TestApp[3613:151099] 30

(1)block所在函数中的,捕获局部变量。但是不能修改它,不然就是“编译错误”。
(2)可以改变全局变量、静态变量、全局静态变量。

  • 不能修改自动变量的值是因为:block捕获的是自动变量的const值,名字一样,不能修改。
  • 可以修改静态变量的值:静态变量属于类的,不是某一个变量。由于block内部不用调用self指针。所以block可以调用。
    解决block不能修改自动变量的值,这一问题的另外一个办法是使用__block修饰符。

2、截获OC对象

不同于基本类型,对应截获OC对象,Block会使得对象的引用计数加1。


@interface ViewController (){
    NSObject *instanceObj;
}
@end

NSObject *globalObj = nil;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    instanceObj = [[NSObject alloc] init];
    globalObj = [[NSObject alloc] init];
    
    static NSObject *staticObj = nil;
    staticObj = [[NSObject alloc] init];
    
    NSObject *localObj = [[NSObject alloc] init];
    __block NSObject *blockObj = [[NSObject alloc] init];
    
    typedef void (^TestBlock)(void);
    
    TestBlock testBlock1 = ^{
        NSLog(@"%@", globalObj);
        NSLog(@"%@", staticObj);
        NSLog(@"%@", self->instanceObj);
        NSLog(@"%@", localObj);
        NSLog(@"%@", blockObj);
    };
    
    testBlock1();
    
    NSLog(@"%d", [[globalObj valueForKey:@"retainCount"] intValue]);
    NSLog(@"%d", [[staticObj valueForKey:@"retainCount"]intValue]);
    NSLog(@"%d", [[instanceObj valueForKey:@"retainCount"]intValue]);
    NSLog(@"%d", [[localObj valueForKey:@"retainCount"]intValue]);
    NSLog(@"%d", [[blockObj valueForKey:@"retainCount"]intValue]);
    
}

打印结果:11121
总结:globalObj和staticObj在内存中的位置是确定的,所以Block copy时不会retain对象。

instanceObj在Block copy时也没有直接retain instanceObj对象本身,但会retain self。所以在Block中可以直接读写instanceObj变量。
localObj在Block copy时,系统自动retain对象,增加其引用计数。
blockObj在Block copy时也不会retain。

3、Block中的循环引用

一般来说我们总会在设置Block之后,在合适的时间回调Block,而不希望回调Block的时候Block已经被释放了,所以我们需要对Block进行copy,copy到堆中,以便后用。

Block可能会导致循环引用问题,因为block在拷贝到堆上的时候,会retain其引用的外部变量,那么如果block中如果引用了他的宿主对象,那很有可能引起循环引用,如:

- (void) dealloc {
    NSLog(@"no cycle retain");
} 

- (id) init {
    self = [super init];
    if (self) {

        #if TestCycleRetainCase1
        //会循环引用
        self.myblock = ^{
            [self doSomething];
        };
  
        #elif TestCycleRetainCase2
        //会循环引用
        __block TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #elif TestCycleRetainCase3
        //不会循环引用
        __weak TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #elif TestCycleRetainCase4
        //不会循环引用
        __unsafe_unretained TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #endif NSLog(@"myblock is %@", self.myblock);
    }
    return self;
} 

- (void) doSomething {
    NSLog(@"do Something");
}
  • MRC情况下,用__block可以消除循环引用。
  • ARC情况下,必须用弱引用才可以解决循环引用问题,iOS 5之后可以直接使用__weak,之前则只能使用__unsafe_unretained了,__unsafe_unretained缺点是指针释放后自己不会置空。

在上述使用 block中,虽说使用__weak,但是此处会有一个隐患,你不知道 self 什么时候会被释放,为了保证在block内不会被释放,我们添加__strong。更多的时候需要配合strongSelf使用,如下:

__weak __typeof(self) weakSelf = self; 
self.testBlock =  ^{
       __strong __typeof(weakSelf) strongSelf = weakSelf;
       [strongSelf test]; 
});

4、使用宏定义:避免循环引用

//----------------------强弱引用----------------------------
#ifndef weakify
#if DEBUG
#if __has_feature(objc_arc)
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
#endif
#else
#if __has_feature(objc_arc)
#define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
#endif
#endif
#endif

#ifndef strongify
#if DEBUG
#if __has_feature(objc_arc)
#define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
#endif
#else
#if __has_feature(objc_arc)
#define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
#endif
#endif
#endif

在设置Block体的时候,像如下这样使用即可。

@weakify(self);
[footerView setClickFooterBlock:^{
        @strongify(self);
        [self handleClickFooterActionWithSectionTag:section];
}];
5、所有的Block里面的self必须要weak一下?

很显然答案不都是,有些情况下是可以直接使用self的,比如调用系统的方法:

[UIView animateWithDuration:0.5 animations:^{
        NSLog(@"%@", self);
}];

因为这个block存在于静态方法中,虽然block对self强引用着,但是self却不持有这个静态方法,所以完全可以在block内部使用self。并不是 block 就一定会造成循环引用,是不是循环引用要看是不是相互持有强引用。

四、Block于内存管理

1、Block的三种类型

根据Block在内存中的位置分为三种类型:

  • NSGlobalBlock是位于全局区的block,它是设置在程序的数据区域(.data区)中。
  • NSStackBlock是位于栈区,超出变量作用域,栈上的Block以及 __block变量都被销毁。
  • NSMallocBlock是位于堆区,在变量作用域结束时不受影响。
    注意:在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

正如它们名字显示得一样,表明了block的三种存储方式:栈、全局、堆。获取block对象中的isa的值,可以得到上面其中一个,下面开始说明哪种block存储在栈、堆、全局。
(1)全局区:GlobalBlock
生成在全局区block有两种情况:

  • 定义全局变量的地方有block语法时
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
 
}
  • block语法的表达式中没有使用截获的变量时
int(^block)(int count) = ^(int count) {
        return count;
    };
 block(2);

(2)栈内存:StackBlock
这种情况,在非ARC下是无法编译的,在ARC下可以编译。

  • block语法的表达式中使用截获的自动变量时
NSInteger i = 10; 
block = ^{ 
     NSLog(@"%ld", i); 
};
block();

设置在栈上的block,如果其作用域结束,该block就被销毁。同样的,由于__block变量也配置在栈上,如果其作用域结束,则该__block变量也会被销毁。
另外,例如:

typedef void (^block_t)() ;  

-(block_t)returnBlock{  
    __block int add=10;  
    return ^{
        printf("add=%d\n",++add);
    };  
}  

(3)堆内存:MallocBlock
堆中的block无法直接创建,其需要由_NSConcreteStackBlock类型的block拷贝而来(也就是说block需要执行copy之后才能存放到堆中)。由于block的拷贝最终都会调用_Block_copy_internal函数。

void(^block)(void);

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

       __block NSInteger i = 10;
       block = [^{
           ++i;
       } copy];
       ++i; 
       block();
       NSLog(@"%ld", i);
   }
   return 0;
}

我们对这个生成在栈上的block执行了copy操作,Block和__block变量均从栈复制到堆上。上面的代码,有跟没有copy,在非ARC和ARC下一个是stack一个是Malloc。这是因为ARC下默认为Malloc(即使如此,ARC下还是有一些例外,下面会讲)。

block在ARC和非ARC下有巨大差别。多数情况下,ARC下会默认把栈block被会直接拷贝生成到堆上。那么,什么时候栈上的Block会复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 将方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

block在ARC和非ARC下的巨大差别

  • 在 ARC 中,捕获外部了变量的 block 的类会是 NSMallocBlock 或者 NSStackBlock,如果 block 被赋值给了某个变量,在这个过程中会执行 _Block_copy 将原有的 NSStackBlock 变成 NSMallocBlock;但是如果 block 没有被赋值给某个变量,那它的类型就是 NSStackBlock;没有捕获外部变量的 block 的类会是 NSGlobalBlock 即不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。
  • 在非 ARC 中,捕获了外部变量的 block 的类会是 NSStackBlock,放置在栈上,没有捕获外部变量的 block 时与 ARC 环境下情况相同。
  • 无论当前环境是ARC还是MRC,只要block没有访问外部变量,block始终在全局区
  • MRC情况下
    • block如果访问外部变量,block在栈里
    • 不能对block使用retain,否则不能保存在堆里
    • 只有使用copy,才能放到堆里
  • ARC情况下
    • block如果访问外部变量,block在堆里
    • block可以使用copy和strong,并且block是一个对象

2、Block的复制

  • 在全局block调用copy什么也不做
  • 在栈上调用copy那么复制到堆上
  • 在堆上调用block 引用计数增加

不管block配置在何处,用copy方法复制都不会引起任何问题。在ARC环境下,如果不确定是否要copy这个block,那尽管copy即可。

最后的强调,在 ARC 开启的情况下,除非上面的例外,默认只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

五、Block的底层原理

通过clang -rewrite-objc main.m命令,来来编译一下block的文件:

#include 

int main(int argc, char * argv[]) {
    @autoreleasepool {
        typedef void (^blk_t)(void);
        blk_t block = ^{
            printf("Hello, World!\n");
        };
        block();
//        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这里只选取部分关键代码。

不难看出int main(int argc, char * argv[]) {就是主函数的实现。

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        typedef void (*blk_t)(void);
        blk_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
}

其中,__main_block_impl_0是block的一个C++的实现(最后面的_0代表是main中的第几个block),也就是说也是一个结构体。

(1) __main_block_impl_0
__main_block_impl_0定义如下:

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

(2) __block_impl
如上,其中__block_impl的定义如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

其结构体成员如下:

isa,指向所属类的指针,也就是block的类型
Flags,标志变量,在实现block的内部操作时会用到
Reserved,保留变量
FuncPtr,block执行时调用的函数指针
可以看出,它包含了isa指针(包含isa指针的皆为对象),也就是说block也是一个对象(runtime里面,对象和类都是用结构体表示)。

(3) __main_block_desc_0
__main_block_desc_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)};

其结构成员含义如下:

reserved:保留字段
Block_size:block大小(sizeof(struct __main_block_impl_0))
以上代码在定义__main_block_desc_0结构体时,同时创建了__main_block_desc_0_DATA,并给它赋值,以供在main函数中对__main_block_impl_0进行初始化。

(4) __main_block_func_0
如上的main函数中,__main_block_func_0也是block的一个C++的实现:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
            printf("Hello, World!\n");
        }

总结:

  • __main_block_impl_0的 isa 指针指向了_NSConcreteStackBlock。
  • 从main函数的main.cpp中看,__main_block_impl_0的 FuncPtr 指向了函数__main_block_func_0。
  • __main_block_impl_0的 Desc 也指向了定义__main_block_desc_0时就创建的__main_block_desc_0_DATA,其中纪录了block结构体大小等信息。

block的变量传递

  • 如果block访问的外部变量是局部变量,那么就是值传递,外界改了,不会影响里面
  • 如果block访问的外部变量是__block或者static修饰,或者是全局变量,那么就是指针传递,block里面的值和外界同一个变量,外界改变,里面也会改变
  • 验证一下是不是这样
  • 通过Clang来将main.m文件编译为C++
  • 在终端输入如下命令clang -rewrite-objc main.m
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,         &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,         &__main_block_desc_0_DATA, a));
可以看到在编译后的代码最后可以发现被__block修饰过得变量使用的是&a,而局部变量是a

你可能感兴趣的:(iOS技术图谱之Block)