美文网首页iOS技术图谱
iOS技术图谱之Block

iOS技术图谱之Block

作者: iOS大蝠 | 来源:发表于2019-11-19 21:45 被阅读0次

    一、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 <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    
    @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 <stdio.h>
    
    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

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