美文网首页
IOS Block 初探

IOS Block 初探

作者: maskerII | 来源:发表于2020-10-22 21:12 被阅读0次

    一、什么是Block ?
    Block 是将函数及其执行上下文封装起来的对象

    比如:

    - (void)blockTest1 {
       int num = 3;
       int(^myBlock)(int n) = ^int(int n){
           return num *n;
       };
       num = 1;
       myBlock(2);
    }
         
    

    将Objective-C代码转换为C\C++代码
    指令如下

    xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  输出的CPP文件
    

    通过 clang -rewrite-objc SWTestVC.m 命令编译该.m 文件

    blockTest1会被编译成如下形式:

    static void _I_SWTestVC_blockTest1(SWTestVC * self, SEL _cmd) {
        int num = 3;
        int(*myBlock)(int n) = ((int (*)(int))&__SWTestVC__blockTest1_block_impl_0((void *)__SWTestVC__blockTest1_block_func_0, &__SWTestVC__blockTest1_block_desc_0_DATA, num));
    
        num = 1;
        ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 2);
    }
    

    SWTestVC 是文件名,blockTest1 是方法名,可以忽略
    SWTestVC__blockTest1_block_impl_0 的结构体为

    struct __SWTestVC__blockTest1_block_impl_0 {
      struct __block_impl impl;
      struct __SWTestVC__blockTest1_block_desc_0* Desc;
      int num;
      __SWTestVC__blockTest1_block_impl_0(void *fp, struct __SWTestVC__blockTest1_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    _block_impl 结构体为

    struct __block_impl {
      void *isa;  // isa指针 
      int Flags;
      int Reserved;
      void *FuncPtr; // 函数指针
    };
    

    block 内部有 isa 指针,所以说其本质也是 OC 对象

    block内部,也就是 __SWTestVC__blockTest1_block_func_0
    如下

    static int __SWTestVC__blockTest1_block_func_0(struct __SWTestVC__blockTest1_block_impl_0 *__cself, int n) {
      int num = __cself->num; // bound by copy
    
            return num *n;
        }
    

    二、Block 变量截获

    1. 局部变量截获 是值截获
      比如:
    - (void)blockTest1 {
        int num = 3;
        int(^myBlock)(int n) = ^int(int n){
            return num *n;
        };
        num = 1;
        NSLog(@"%d",myBlock(2));
    }
    
    

    这里的输出是 6 而不是 2,原因就是对局部变量 num 的截获是值截获。 同样,在 block 里如果修改变量 num,也是无效的,甚至编译器会报错

    - (void)blockTest2 {
        NSMutableArray * arr = [[NSMutableArray alloc] initWithObjects:@"1",@"2", nil];
        void (^myBlock2)(void) = ^{
            NSLog(@"%@",arr);
        };
        
        [arr addObject:@"3"];
        
        myBlock2();
    }
    

    这里打印的是 1,2,3,而不是 1,2
    对于局部对象而言,也是截获值,而不是指针,在外部将其置为 nil,对 block 没有影响,而该对象调 用方法会影响~

    也就是说 block创建时,会捕获指针arr的值,也就arr所指向的内存的内容,数组的addObject操作,会改变这块内存的内容,但arr = nil,是对指针操作,指针不再指向这块内存,不会对这块内存的内容产生影响~

    思考:

    
    - (void)blockTest5 {
        NSMutableArray * arr = [[NSMutableArray alloc] initWithObjects:@"1",@"2", nil];
        void (^myBlock5)(void) = ^{
            NSLog(@"%@",arr); // 局部变量
            [arr addObject:@"4"];
        };
        [arr addObject:@"6"];
        
        NSMutableArray * tempArray = arr;
        arr = [[NSMutableArray alloc] initWithObjects:@"4",@"5", nil];
        [arr addObject:@"3"];
        
        [tempArray addObject:@"7"];
        arr = nil;
        myBlock5();
        
        
    }
    
    

    输出结果 : 1,2,6,7

    1. 局部静态变量截获,是指针截获
    - (void)blockTest6{
        static int m = 3;
        int (^myBlock6)(int) = ^ int(int n){
            return  m*n;
        };
        m = 1;
        NSLog(@"%d",myBlock6(2));
    }
    
    

    输出为 2,意味着 m = 1 这里的修改 m 值是有效的,即是指针截获。 同样,在 block 里去修改变量 m,也是有效的。

    - (void)blockTest7 {
        static NSArray * arr = nil;
        arr = [[NSArray alloc] initWithObjects:@"1",@"2", nil];
        void (^myBlock7)(void) = ^{
            NSLog(@"blockTest7 %@",arr); // 局部变量
        };
        arr = @[@"1",@"2",@"3"];
    //    arr = nil;
        myBlock7();
    }
    

    输出 1,2,3
    局部对象变量,也是指针截获

    1. 全局变量,静态全局变量截获:不截获,直接取值

    我们同样用 clang 编译看下结果

    
    static NSInteger num3 = 300;
    NSInteger num4 = 3000;
    - (void)blockTest3 {
        NSInteger num = 30;
        static NSInteger num2 = 3;
        __block NSInteger num5 = 30000;
        
        void(^myBlock3)(void) = ^{
            NSLog(@"%zd",num); // 局部变量
            NSLog(@"%zd",num2); // 静态变量
            NSLog(@"%zd",num3); // 静态全局变量
            NSLog(@"%zd",num4); // 全局变量
            NSLog(@"%zd",num5); // __block修饰变量
        };
        
        myBlock3();
    }
    
    

    编译后

    
    struct __SWTestVC__blockTest3_block_impl_0 {
      struct __block_impl impl;
      struct __SWTestVC__blockTest3_block_desc_0* Desc;
      NSInteger num;   // 局部变量
      NSInteger *num2;  // 静态变量
      __Block_byref_num5_0 *num5; // by ref // __block 变量
      __SWTestVC__blockTest3_block_impl_0(void *fp, struct __SWTestVC__blockTest3_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    

    可以看到局部变量被编译成值形式,而静态变量被编成指针形式,全局变量并未截获。而__block 修饰的变 量也是以指针形式截获的,并且生成了一个新的结构体对象:

    struct __Block_byref_num5_0 {
      void *__isa;
    __Block_byref_num5_0 *__forwarding;
     int __flags;
     int __size;
     NSInteger num5;
    };
    

    该对象有个属性:num5,即我们用__block 修饰的变量。 这里__forwarding 是指向自身的(栈 block)。 一般情况下,如果我们要对 block 截获的局部变量进行赋值操作需添加__block 修饰符,而对全局变量,静态变量是不需要添加__block 修饰符的。

    另外,block 里访问 self 或成员变量都会去截获 self。

    - (void)blockTest8 {
    
        self.t = 2;
        self.h = 3;
        void (^myBlock8)(void) = ^ void (void){
            NSLog(@"blockTest8 %d %d",self.t,self.h);
        };
        self.t = 5;
        myBlock8();
       
    

    输出 5,3

    三、Block的几种形式

    分为全局Block(_NSConcreteGlobalBlock)、栈Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三种形式,其中栈Block存储在栈(stack)区,堆Block存储在堆(heap)区,全局Block存储在已初始化数据(.data)区

    1. 不使用外部变量的Block是全局Block

    比如:

    - (void)blockTest11 {
        NSLog(@"%@",[^{NSLog(@"globalBlock");} class]);
    }
    

    输出:NSGlobalBlock

    1. 使用外部变量并且未进行 copy 操作的 block 是栈 block

    比如:

    - (void)blockTest12 {
        int m = 12;
        NSLog(@"%@", [^{NSLog(@"stackBlock %d",m);} class]);
    }
    

    输出:NSStackBlock

    日常开发常用于这种情况:

    - (void)blockTest15 {
        [self testWithBlock:^{
            NSLog(@"blockTest15 %@",self);
        }];
    }
    
    - (void)testWithBlock:(void(^)(void))block {
        block();
        NSLog(@"%@",[block class]);
    }
    
    

    输出: NSStackBlock

    1. 对栈Block进行Copy操作,就是堆Block,对全局Block进行Copy操作,还是全局Block

    比如 对1中的全局block进行copy操作,即赋值:

    - (void)blockTest13 {
        void (^myBlock13)(void) = ^{
            NSLog(@"globalBlock");
        };
        NSLog(@"%@",[myBlock13 class]);
    }
    

    输出: NSGlobalBlock
    仍是全局Block

    而对 2 中的栈 block 进行赋值操作:

    - (void)blockTest14 {
        int m = 12;
        void (^myBlock)(void) = ^ {
            NSLog(@"mallocBlock %d",m);
        };
        
        NSLog(@"%@",[myBlock class]);
    }
    

    输出:NSMallocBlock
    是堆Block

    对栈Block Copy后,并不代表栈Block消失了,左边的Block是堆Block,右边被Copy的Block还是栈Block

    比如:

    - (void)blockTest16 {
        [self testWithBlock2:^{
            NSLog(@"%@",self);
        }];
    }
    
    - (void)testWithBlock2:(void(^)(void))block {
        block();
        void (^tempBlock)(void) = block;
        NSLog(@"%@ %@",[tempBlock class],[block class]);
    }
    

    输出:NSMallocBlock NSStackBlock

    即如果对栈 Block 进行 copy,将会 copy 到堆区,对堆 Block 进行 copy,将会增加引用计数,对全局 Block 进行 copy,因为是已经初始化的,所以什么也不做。

    另外,__block 变量在 copy 时,由于__forwarding 的存在,栈上的__forwarding 指针会指向堆上的__forwarding 变量,而堆上的__forwarding 指针指向其自身,所以,如果对__block 的修改,实际上是在修改堆上的__block 变量。

    即__forwarding 指针存在的意义就是,无论在任何内存位置,都可以顺利地访问同一个__block 变量。

    另外由于 block 捕获的__block 修饰的变量会去持有变量,那么如果用__block 修饰 self,且 self 持有 block,并且 block 内部使用到__block 修饰的 self 时,就会造成多循环引用,即 self 持有 block,block 持 有__block 变量,而__block 变量持有 self,造成内存泄漏。

    比如:

    - (void)blockTest17 {
        __block typeof(self) weakSelf = self;
        self.testBlock = ^{
            NSLog(@"%@",weakSelf);
            
        };
        self.testBlock();
    }
    

    如果要解决这种循环引用,可以主动断开__block 变量对 self 的持有,即在 block 内部使用完 weakself 后, 将其置为 nil,但这种方式有个问题,如果 block 一直不被调用,那么循环引用将一直存在。 所以,我们最好还是用__weak 来修饰 self

    
    - (void)blockTest18 {
        __weak typeof(self) weakSelf = self;
        self.testBlock = ^{
            __strong typeof(self) self = weakSelf;
            NSLog(@"%@",self);
        };
        self.testBlock();
    }
    
    

    相关文章

      网友评论

          本文标题:IOS Block 初探

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